merit 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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