merit 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- merit (1.1.0)
4
+ merit (1.1.1)
5
5
  ambry (~> 0.3.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -148,11 +148,7 @@ end
148
148
 
149
149
  # To-do list
150
150
 
151
- * `Merit::BadgeRules.new.defined_rules` should be cached on initialization,
152
- instead of initialized per controllers `after_filter` and
153
- `merit_action.check_rules`.
154
151
  * target_object should be configurable (now it's singularized controller name)
155
- * Translate comments from spanish in `rules_badge.rb`.
156
152
  * Should namespace app/models into Merit module.
157
153
  * :value parameter (for star voting for example) should be configurable
158
154
  (depends on params[:value] on the controller).
@@ -23,6 +23,15 @@ class Badge
23
23
  end
24
24
  end
25
25
 
26
+ def self.find_by_name_and_level(name, level)
27
+ badges = Badge.by_name(name)
28
+ badges = badges.by_level(level) unless level.nil?
29
+ if !(badge = badges.first)
30
+ raise ::Merit::BadgeNotFound, "No badge '#{name}'#{level.nil? ? '' : " with level #{level}"} found. Define it in 'config/initializers/merit.rb'."
31
+ end
32
+ badge
33
+ end
34
+
26
35
  # Grant badge to sash
27
36
  # Accepts :allow_multiple boolean option, defaults to false
28
37
  def grant_to(object_or_sash, *args)
@@ -49,11 +58,13 @@ class Badge
49
58
  end
50
59
  end
51
60
 
61
+ private
62
+
52
63
  def sash_from(object_or_sash)
53
64
  if object_or_sash.kind_of?(Sash)
54
65
  object_or_sash
55
66
  else
56
- object_or_sash.sash || object_or_sash.create_sash_and_scores
67
+ object_or_sash._sash
57
68
  end
58
69
  end
59
70
  end
@@ -1,9 +1,25 @@
1
1
  require "merit/models/#{Merit.orm}/merit_action"
2
2
 
3
+ # MeritAction general schema
4
+ # ______________________________________________________________
5
+ # source | action | target
6
+ # user_id | method,value | model,id | processed
7
+ # ______________________________________________________________
8
+ # 1 | comment nil | List 8 | true
9
+ # 1 | vote 3 | List 12 | true
10
+ # 3 | follow nil | User 1 | false
11
+ # X | create nil | User #{generated_id} | false
12
+ # ______________________________________________________________
13
+ #
14
+ # Rules relate to merit_actions by action name ('controller#action' string)
3
15
  class MeritAction
4
16
  attr_accessible :user_id, :action_method, :action_value, :had_errors,
5
17
  :target_model, :target_id, :processed, :log
6
18
 
19
+ def self.check_unprocessed_rules
20
+ where(:processed => false).map &:check_rules
21
+ end
22
+
7
23
  # Check rules defined for a merit_action
8
24
  def check_rules
9
25
  unless had_errors
@@ -13,24 +29,38 @@ class MeritAction
13
29
  processed!
14
30
  end
15
31
 
32
+ def target(to)
33
+ @target ||= (to == :action_user) ? action_user : other_target(to)
34
+ end
35
+
36
+ def target_object(model_name = nil)
37
+ # Grab custom model_name from Rule, or target_model from MeritAction triggered
38
+ klass = model_name || target_model
39
+ klass.singularize.camelize.constantize.find_by_id(target_id)
40
+ rescue => e
41
+ Rails.logger.warn "[merit] no target_object found: #{e}"
42
+ end
43
+
44
+ def log_activity(str)
45
+ self.update_attribute :log, "#{self.log}#{str}|"
46
+ end
47
+
48
+ private
49
+
16
50
  def check_badge_rules
17
- badge_rules = Merit::BadgeRules.new.defined_rules[action_str] || []
18
- badge_rules.each { |rule| rule.grant_or_delete_badge(self) }
51
+ rules = AppBadgeRules[action_str] || []
52
+ rules.each { |rule| rule.apply_badges(self) }
19
53
  end
20
54
 
21
55
  def check_point_rules
22
- point_rules = Merit::PointRules.new.defined_rules[action_str] || []
23
- point_rules.each { |rule| rule.grant_points(self) }
56
+ rules = AppPointRules[action_str] || []
57
+ rules.each { |rule| rule.apply_points(self) }
24
58
  end
25
59
 
26
60
  def action_str
27
61
  "#{target_model}\##{action_method}"
28
62
  end
29
63
 
30
- def target(to)
31
- @target ||= (to == :action_user) ? action_user : other_target(to)
32
- end
33
-
34
64
  def action_user
35
65
  begin
36
66
  Merit.user_model.find(user_id)
@@ -49,20 +79,6 @@ class MeritAction
49
79
  end
50
80
  end
51
81
 
52
- # Action's target object
53
- def target_object(model_name = nil)
54
- # Grab custom model_name from Rule, or target_model from MeritAction triggered
55
- klass = model_name || target_model
56
- klass.singularize.camelize.constantize.find_by_id(target_id)
57
- rescue => e
58
- Rails.logger.warn "[merit] no target_object found: #{e}"
59
- end
60
-
61
- def log!(str)
62
- self.log = "#{self.log}#{str}|"
63
- self.save
64
- end
65
-
66
82
  # Mark merit_action as processed
67
83
  def processed!
68
84
  self.processed = true
@@ -4,7 +4,7 @@ class CreateMeritActions < ActiveRecord::Migration
4
4
  t.integer :user_id # source
5
5
  t.string :action_method
6
6
  t.integer :action_value
7
- t.boolean :had_errors
7
+ t.boolean :had_errors, :default => false
8
8
  t.string :target_model
9
9
  t.integer :target_id
10
10
  t.boolean :processed, :default => false
@@ -1,7 +1,7 @@
1
1
  require 'merit/rule'
2
- require 'merit/rules_badge'
3
- require 'merit/rules_points'
4
- require 'merit/rules_rank'
2
+ require 'merit/rules_badge_methods'
3
+ require 'merit/rules_points_methods'
4
+ require 'merit/rules_rank_methods'
5
5
  require 'merit/controller_extensions'
6
6
  require 'merit/model_additions'
7
7
 
@@ -48,6 +48,10 @@ module Merit
48
48
  end
49
49
 
50
50
  ActiveSupport.on_load(:action_controller) do
51
+ # Load application defined rules
52
+ ::Merit::AppBadgeRules = BadgeRules.new.defined_rules
53
+ ::Merit::AppPointRules = PointRules.new.defined_rules
54
+
51
55
  include Merit::ControllerExtensions
52
56
  end
53
57
  end
@@ -1,31 +1,38 @@
1
1
  module Merit
2
- # This module sets up an after_filter to update merit_actions table.
3
- # It executes on every action, and checks rules only if
4
- # 'controller_name#action_name' has defined badge or point rules
2
+ # Sets up an app-wide after_filter, and inserts merit_action entries if
3
+ # there are defined rules (for badges or points) for current
4
+ # 'controller_name#action_name'
5
5
  module ControllerExtensions
6
6
  def self.included(base)
7
7
  base.after_filter do |controller|
8
- action = "#{controller_name}\##{action_name}"
9
- badge_rules = BadgeRules.new
10
- point_rules = PointRules.new
11
- if badge_rules.defined_rules[action].present? || point_rules.defined_rules[action].present?
12
- merit_action_id = MeritAction.create(
13
- :user_id => send(Merit.current_user_method).try(:id),
14
- :action_method => action_name,
15
- :action_value => params[:value],
16
- :had_errors => target_object.try(:errors).try(:present?) || false,
17
- :target_model => controller_name,
18
- :target_id => target_id
19
- ).id
8
+ return unless rules_defined?
20
9
 
21
- # Check rules in after_filter?
22
- if Merit.checks_on_each_request
23
- badge_rules.check_new_actions
24
- end
10
+ MeritAction.create(
11
+ :user_id => send(Merit.current_user_method).try(:id),
12
+ :action_method => action_name,
13
+ :action_value => params[:value],
14
+ :had_errors => had_errors?,
15
+ :target_model => controller_name,
16
+ :target_id => target_id
17
+ ).id
18
+
19
+ if Merit.checks_on_each_request
20
+ MeritAction.check_unprocessed_rules
25
21
  end
26
22
  end
27
23
  end
28
24
 
25
+ private
26
+
27
+ def rules_defined?
28
+ action = "#{controller_name}\##{action_name}"
29
+ AppBadgeRules[action].present? || AppPointRules[action].present?
30
+ end
31
+
32
+ def had_errors?
33
+ target_object.try(:errors).try(:present?) || false
34
+ end
35
+
29
36
  def target_object
30
37
  target_obj = instance_variable_get(:"@#{controller_name.singularize}")
31
38
  if target_obj.nil?
@@ -36,7 +43,7 @@ module Merit
36
43
 
37
44
  def target_id
38
45
  target_id = params[:id] || target_object.try(:id)
39
- # using friendly_id if id nil or string (), and target_object found
46
+ # using friendly_id if id is nil or string but an object was found
40
47
  if target_object.present? && (target_id.nil? || !(target_id =~ /^[0-9]+$/))
41
48
  target_id = target_object.id
42
49
  end
@@ -3,8 +3,9 @@ module Merit
3
3
 
4
4
  module ClassMethods
5
5
  def has_merit(options = {})
6
- # :dependent => destroy may raise
7
- # ERROR: update or delete on table "sashes" violates foreign key constraint "users_sash_id_fk"
6
+ # MeritableModel#sash_id is more stable than Sash#meritable_model_id
7
+ # That's why MeritableModel belongs_to Sash. Can't use
8
+ # :dependent => destroy as it may raise FK constraint exceptions. See:
8
9
  # https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1079-belongs_to-dependent-destroy-should-destroy-self-before-assocation
9
10
  belongs_to :sash
10
11
 
@@ -22,31 +23,17 @@ module Merit
22
23
  end
23
24
  end
24
25
 
25
- # Add instance methods to meritable models
26
- # Using define_method on meritable classes to not pollute every model
27
-
28
26
  # Delegate relationship methods from meritable models to their sash
29
- %w(badge_ids badges points).each do |method|
30
- define_method(method) do
31
- _sash = sash || create_sash_and_scores
32
- _sash.send method
33
- end
34
- end
35
-
36
- define_method(:add_points) do |num_points, log = 'Manually through `add_points`', category = 'default'|
37
- _sash = sash || create_sash_and_scores
38
- _sash.add_points num_points, log, category
39
- end
40
- define_method(:substract_points) do |num_points, log = 'Manually through `substract_points`', category = 'default'|
41
- _sash = sash || create_sash_and_scores
42
- _sash.substract_points num_points, log, category
27
+ # _sash initializes a sash if doesn't have one yet.
28
+ # From Rails 3.2 we can override association methods to do so
29
+ # transparently, but merit supports Rails ~> 3.0.0. See:
30
+ # http://blog.hasmanythrough.com/2012/1/20/modularized-association-methods-in-rails-3-2
31
+ %w(badge_ids badges points add_points substract_points).each do |method|
32
+ delegate method, to: :_sash
43
33
  end
44
-
45
- # Create sash if doesn't have
46
- define_method(:create_sash_and_scores) do
47
- if self.sash.blank?
34
+ define_method(:_sash) do
35
+ if sash.nil?
48
36
  self.sash = Sash.create
49
- self.sash.scores << Merit::Score.create
50
37
  self.save(:validate => false)
51
38
  end
52
39
  self.sash
@@ -9,7 +9,6 @@ module Merit
9
9
  end
10
10
 
11
11
  class Point < ActiveRecord::Base
12
- self.table_name = :merit_score_points
13
12
  belongs_to :score, :class_name => 'Merit::Score'
14
13
  end
15
14
  end
@@ -1,7 +1,15 @@
1
+ # Sash is a container for reputation data for meritable models. It's an
2
+ # indirection between meritable models and badges and scores (one to one
3
+ # relationship).
4
+ #
5
+ # It's existence make join models like badges_users and scores_users
6
+ # unnecessary. It should be transparent at the application.
1
7
  class Sash < ActiveRecord::Base
2
8
  has_many :badges_sashes, :dependent => :destroy
3
9
  has_many :scores, :dependent => :destroy, :class_name => 'Merit::Score'
4
10
 
11
+ after_create :create_scores
12
+
5
13
  def badges
6
14
  badge_ids.collect { |b_id| Badge.find(b_id) }
7
15
  end
@@ -33,4 +41,10 @@ class Sash < ActiveRecord::Base
33
41
  def substract_points(num_points, log = 'Manually granted through `add_points`', category = 'default')
34
42
  add_points -num_points, log, category
35
43
  end
44
+
45
+ private
46
+
47
+ def create_scores
48
+ self.scores << Merit::Score.create
49
+ end
36
50
  end
@@ -28,25 +28,25 @@ module Merit
28
28
 
29
29
  # Grant badge if rule applies. If it doesn't, and the badge is temporary,
30
30
  # then remove it.
31
- def grant_or_delete_badge(action)
31
+ def apply_badges(action)
32
32
  unless (sash = sash_to_badge(action))
33
- Rails.logger.warn "[merit] no sash found on Rule#grant_or_delete_badge for action #{action.inspect}"
33
+ Rails.logger.warn "[merit] no sash found on Rule#apply_badges for action #{action.inspect}"
34
34
  return
35
35
  end
36
36
 
37
37
  if applies? action.target_object(model_name)
38
38
  if badge.grant_to(sash, :allow_multiple => self.multiple)
39
39
  to_action_user = (to.to_sym == :action_user ? '_to_action_user' : '')
40
- action.log!("badge_granted#{to_action_user}:#{badge.id}")
40
+ action.log_activity "badge_granted#{to_action_user}:#{badge.id}"
41
41
  end
42
42
  elsif temporary?
43
43
  if badge.delete_from(sash)
44
- action.log!("badge_removed:#{badge.id}")
44
+ action.log_activity "badge_removed:#{badge.id}"
45
45
  end
46
46
  end
47
47
  end
48
48
 
49
- def grant_points(action)
49
+ def apply_points(action)
50
50
  unless (sash = sash_to_badge(action))
51
51
  Rails.logger.warn "[merit] no sash found on Rule#grant_points"
52
52
  return
@@ -54,7 +54,7 @@ module Merit
54
54
 
55
55
  if applies? action.target_object(model_name)
56
56
  sash.add_points self.score, action.inspect[0..240]
57
- action.log!("points_granted:#{self.score}")
57
+ action.log_activity "points_granted:#{self.score}"
58
58
  end
59
59
  end
60
60
 
@@ -68,21 +68,12 @@ module Merit
68
68
  else
69
69
  target = action.target(to)
70
70
  end
71
- if target
72
- target.sash || target.create_sash_and_scores
73
- end
71
+ target._sash if target
74
72
  end
75
73
 
76
74
  # Get rule's related Badge.
77
75
  def badge
78
- if @badge.nil?
79
- badges = Badge.by_name(badge_name)
80
- badges = badges.by_level(level) unless level.nil?
81
- if !(@badge = badges.first)
82
- raise BadgeNotFound, "No badge '#{badge_name}'#{level.nil? ? '' : " with level #level"} found. Define it in 'config/initializers/merit.rb'."
83
- end
84
- end
85
- @badge
76
+ @badge ||= Badge.find_by_name_and_level(badge_name, level)
86
77
  end
87
78
  end
88
79
  end
@@ -0,0 +1,29 @@
1
+ module Merit
2
+ module BadgeRulesMethods
3
+ # Define rule for granting badges
4
+ def grant_on(action, *args, &block)
5
+ options = args.extract_options!
6
+
7
+ actions = Array.wrap(action)
8
+
9
+ rule = Rule.new
10
+ rule.badge_name = options[:badge]
11
+ rule.level = options[:level]
12
+ rule.to = options[:to] || :action_user
13
+ rule.multiple = options[:multiple] || false
14
+ rule.temporary = options[:temporary] || false
15
+ rule.model_name = options[:model_name] || actions[0].split('#')[0]
16
+ rule.block = block
17
+
18
+ actions.each do |action|
19
+ defined_rules[action] ||= []
20
+ defined_rules[action] << rule
21
+ end
22
+ end
23
+
24
+ # Currently defined rules
25
+ def defined_rules
26
+ @defined_rules ||= {}
27
+ end
28
+ end
29
+ end
@@ -22,7 +22,8 @@ module Merit
22
22
  defined_rules[options[:to]].merge!({ options[:level] => rule })
23
23
  end
24
24
 
25
- # Check rules defined for a merit_action
25
+ # Not part of merit after_filter. To be called asynchronously:
26
+ # Merit::RankRules.new.check_rank_rules
26
27
  def check_rank_rules
27
28
  defined_rules.each do |scoped_model, level_and_rules|
28
29
  level_and_rules = level_and_rules.sort
@@ -4,7 +4,7 @@ Gem::Specification.new do |s|
4
4
  s.description = "Manage badges, points and rankings (reputation) of resources in a Rails application."
5
5
  s.homepage = "http://github.com/tute/merit"
6
6
  s.files = `git ls-files`.split("\n").reject{|f| f =~ /^\./ }
7
- s.version = '1.1.0'
7
+ s.version = '1.1.1'
8
8
  s.authors = ["Tute Costa"]
9
9
  s.email = 'tutecosta@gmail.com'
10
10
  s.add_dependency 'ambry', '~> 0.3.0'
@@ -45,12 +45,6 @@ class MeritUnitTest < ActiveSupport::TestCase
45
45
  assert badge_sash.notified_user
46
46
  end
47
47
 
48
- # TODO: Test and refactor:
49
- # Rule: grant_or_delete_badge(action), sash_to_badge
50
- # Badge: delete_from
51
- # MeritAction: target(to), action_user, other_target(to), target_object(model_name = nil)
52
-
53
-
54
48
  test "Badge#grant_to allow_multiple option" do
55
49
  badge = Badge.create(:id => 99, :name => 'test-badge')
56
50
  sash = Sash.create(:id => 99)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: merit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-24 00:00:00.000000000 Z
12
+ date: 2012-11-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ambry
@@ -180,9 +180,9 @@ files:
180
180
  - lib/merit/models/mongoid/merit_action.rb
181
181
  - lib/merit/models/mongoid/sash.rb
182
182
  - lib/merit/rule.rb
183
- - lib/merit/rules_badge.rb
184
- - lib/merit/rules_points.rb
185
- - lib/merit/rules_rank.rb
183
+ - lib/merit/rules_badge_methods.rb
184
+ - lib/merit/rules_points_methods.rb
185
+ - lib/merit/rules_rank_methods.rb
186
186
  - merit.gemspec
187
187
  - test/dummy-mongoid/Rakefile
188
188
  - test/dummy-mongoid/app/controllers/application_controller.rb
@@ -1,64 +0,0 @@
1
- module Merit
2
- # La configuración para especificar cuándo aplicar cada badge va en
3
- # app/models/merit/badge_rules.rb, con la siguiente sintaxis:
4
- #
5
- # grant_on 'users#create', :badge => 'just', :level => 'registered' do
6
- # # Nothing, or code block which evaluates to boolean
7
- # end
8
- #
9
- # También se puede asignar medallas desde métodos en controladores:
10
- #
11
- # Badge.find(3).grant_to(current_user)
12
- #
13
- # Merit crea una tabla Badges similar a:
14
- # ___________________________________________________
15
- # id | name | level | image
16
- # 1 | creador | inspirado | creador-inspirado.png
17
- # 2 | creador | blogger | creador-blogger.png
18
- # 2 | creador | best-seller | creador-bestseller.png
19
- # ___________________________________________________
20
- #
21
- # Y llena una tabla de acciones, del estilo de:
22
- # ______________________________________________________________
23
- # source (user_id) | action (method, value) | target (model, id) | processed
24
- # 1 | comment nil | List 8 | true
25
- # 1 | vote 3 | List 12 | true
26
- # 3 | follow nil | User 1 | false
27
- # X | create nil | User #{generated_id} | false
28
- # ______________________________________________________________
29
- #
30
- # Luego chequea las condiciones sincronizadamente, o mediante un proceso en
31
- # background, por ejemplo cada 5 minutos (Merit::BadgeRules#check_new_actions).
32
- module BadgeRulesMethods
33
- # Define rule for granting badges
34
- def grant_on(action, *args, &block)
35
- options = args.extract_options!
36
-
37
- actions = action.kind_of?(String) ? [action] : action
38
-
39
- rule = Rule.new
40
- rule.badge_name = options[:badge]
41
- rule.level = options[:level]
42
- rule.to = options[:to] || :action_user
43
- rule.multiple = options[:multiple] || false
44
- rule.temporary = options[:temporary] || false
45
- rule.model_name = options[:model_name] || actions[0].split('#')[0]
46
- rule.block = block
47
-
48
- actions.each do |action|
49
- defined_rules[action] ||= []
50
- defined_rules[action] << rule
51
- end
52
- end
53
-
54
- # Check non processed actions and grant badges if applies
55
- def check_new_actions
56
- MeritAction.where(:processed => false).map &:check_rules
57
- end
58
-
59
- # Currently defined rules
60
- def defined_rules
61
- @defined_rules ||= {}
62
- end
63
- end
64
- end