activerecord-reputation-system 1.5.1 → 2.0.0

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.
Files changed (26) hide show
  1. data/README.md +29 -198
  2. data/lib/activerecord-reputation-system.rb +1 -0
  3. data/lib/reputation_system.rb +6 -6
  4. data/lib/reputation_system/base.rb +16 -6
  5. data/lib/reputation_system/{evaluation.rb → evaluation_methods.rb} +32 -13
  6. data/lib/reputation_system/models/evaluation.rb +73 -0
  7. data/lib/reputation_system/models/reputation.rb +211 -0
  8. data/lib/reputation_system/models/reputation_message.rb +49 -0
  9. data/lib/reputation_system/network.rb +2 -0
  10. data/lib/reputation_system/query_builder.rb +4 -4
  11. data/lib/reputation_system/{reputation.rb → reputation_methods.rb} +5 -15
  12. data/lib/reputation_system/{scope.rb → scope_methods.rb} +2 -2
  13. data/lib/reputation_system/version.rb +1 -1
  14. data/spec/reputation_system/base_spec.rb +46 -6
  15. data/spec/reputation_system/{evaluation_spec.rb → evaluation_methods_spec.rb} +52 -9
  16. data/spec/{models/rs_evaluation_spec.rb → reputation_system/models/evaluation_spec.rb} +8 -8
  17. data/spec/{models/rs_reputation_message_spec.rb → reputation_system/models/reputation_message_spec.rb} +10 -10
  18. data/spec/reputation_system/models/reputation_spec.rb +136 -0
  19. data/spec/reputation_system/{reputation_spec.rb → reputation_methods_spec.rb} +2 -2
  20. data/spec/reputation_system/{scope_spec.rb → scope_methods_spec.rb} +0 -0
  21. data/spec/spec_helper.rb +2 -4
  22. metadata +15 -14
  23. data/lib/models/rs_evaluation.rb +0 -69
  24. data/lib/models/rs_reputation.rb +0 -204
  25. data/lib/models/rs_reputation_message.rb +0 -46
  26. data/spec/models/rs_reputation_spec.rb +0 -119
@@ -0,0 +1,73 @@
1
+ ##
2
+ # Copyright 2012 Twitter, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ ##
16
+
17
+ module ReputationSystem
18
+ class Evaluation < ActiveRecord::Base
19
+ self.table_name = 'rs_evaluations'
20
+
21
+ belongs_to :source, :polymorphic => true
22
+ belongs_to :target, :polymorphic => true
23
+ has_one :sent_messages, :as => :sender, :class_name => 'ReputationSystem::ReputationMessage', :dependent => :destroy
24
+
25
+ attr_accessible :reputation_name, :value, :source, :source_id, :source_type, :target, :target_id, :target_type
26
+
27
+ # Sets an appropriate source type in case of Single Table Inheritance.
28
+ before_validation :set_source_type_for_sti
29
+
30
+ # the same source cannot evaluate the same target more than once.
31
+ validates_uniqueness_of :source_id, :scope => [:reputation_name, :source_type, :target_id, :target_type]
32
+ validate :source_must_be_defined_for_reputation_in_network
33
+
34
+ def self.find_by_reputation_name_and_source_and_target(reputation_name, source, target)
35
+ source_type = get_source_type_for_sti(source, target.class.name, reputation_name)
36
+ ReputationSystem::Evaluation.find(:first,
37
+ :conditions => {:reputation_name => reputation_name.to_s,
38
+ :source_id => source.id,
39
+ :source_type => source_type,
40
+ :target_id => target.id,
41
+ :target_type => target.class.name
42
+ })
43
+ end
44
+
45
+ def self.create_evaluation(reputation_name, value, source, target)
46
+ ReputationSystem::Evaluation.create!(:reputation_name => reputation_name.to_s, :value => value,
47
+ :source_id => source.id, :source_type => source.class.name,
48
+ :target_id => target.id, :target_type => target.class.name)
49
+ end
50
+
51
+ protected
52
+
53
+ def self.get_source_type_for_sti(source, target_type, reputation_name)
54
+ valid_source_type = ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize
55
+ source_class = source.class
56
+ while source_class && valid_source_type != source_class.name && source_class.name != "ActiveRecord::Base"
57
+ source_class = source_class.superclass
58
+ end
59
+ source_class ? source_class.name : nil
60
+ end
61
+
62
+ def set_source_type_for_sti
63
+ sti_source_type = self.class.get_source_type_for_sti(source, target_type, reputation_name)
64
+ self.source_type = sti_source_type if sti_source_type
65
+ end
66
+
67
+ def source_must_be_defined_for_reputation_in_network
68
+ unless source_type == ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize
69
+ errors.add(:source_type, "#{source_type} is not source of #{reputation_name} reputation")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,211 @@
1
+ ##
2
+ # Copyright 2012 Twitter, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ ##
16
+
17
+ module ReputationSystem
18
+ class Reputation < ActiveRecord::Base
19
+ self.table_name = 'rs_reputations'
20
+
21
+ belongs_to :target, :polymorphic => true
22
+ has_many :received_messages, :class_name => 'ReputationSystem::ReputationMessage', :foreign_key => :receiver_id, :dependent => :destroy do
23
+ def from(sender)
24
+ self.find_by_sender_id_and_sender_type(sender.id, sender.class.to_s)
25
+ end
26
+ end
27
+ has_many :sent_messages, :as => :sender, :class_name => 'ReputationSystem::ReputationMessage', :dependent => :destroy
28
+
29
+ attr_accessible :reputation_name, :value, :aggregated_by, :active, :target, :target_id, :target_type, :received_messages
30
+
31
+ before_validation :set_target_type_for_sti
32
+ before_save :change_zero_value_in_case_of_product_process
33
+
34
+ VALID_PROCESSES = ['sum', 'average', 'product']
35
+ validates_inclusion_of :aggregated_by, :in => VALID_PROCESSES, :message => "Value chosen for aggregated_by is not valid process"
36
+ validates_uniqueness_of :reputation_name, :scope => [:target_id, :target_type]
37
+
38
+ def self.find_by_reputation_name_and_target(reputation_name, target)
39
+ target_type = get_target_type_for_sti(target, reputation_name)
40
+ ReputationSystem::Reputation.find_by_reputation_name_and_target_id_and_target_type(reputation_name.to_s, target.id, target_type)
41
+ end
42
+
43
+ # All external access to reputation should use this since they are created lazily.
44
+ def self.find_or_create_reputation(reputation_name, target, process)
45
+ rep = find_by_reputation_name_and_target(reputation_name, target)
46
+ rep ? rep : create_reputation(reputation_name, target, process)
47
+ end
48
+
49
+ def self.create_reputation(reputation_name, target, process)
50
+ create_options = {:reputation_name => reputation_name.to_s, :target_id => target.id,
51
+ :target_type => target.class.name, :aggregated_by => process.to_s}
52
+ rep = create(create_options)
53
+ initialize_reputation_value(rep, target, process)
54
+ end
55
+
56
+ def self.update_reputation_value_with_new_source(rep, source, weight, process)
57
+ weight ||= 1 # weight is 1 by default.
58
+ size = rep.received_messages.size
59
+ valueBeforeUpdate = size > 0 ? rep.value : nil
60
+ newValue = source.value
61
+ case process.to_sym
62
+ when :sum
63
+ rep.value += (newValue * weight)
64
+ when :average
65
+ rep.value = (rep.value * size + newValue * weight) / (size + 1)
66
+ when :product
67
+ rep.value *= (newValue * weight)
68
+ else
69
+ raise ArgumentError, "#{process} process is not supported yet"
70
+ end
71
+ save_succeeded = rep.save
72
+ ReputationSystem::ReputationMessage.add_reputation_message_if_not_exist(source, rep)
73
+ propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
74
+ save_succeeded
75
+ end
76
+
77
+ def self.update_reputation_value_with_updated_source(rep, source, oldValue, newSize, weight, process)
78
+ weight ||= 1 # weight is 1 by default.
79
+ oldSize = rep.received_messages.size
80
+ valueBeforeUpdate = oldSize > 0 ? rep.value : nil
81
+ newValue = source.value
82
+ if newSize == 0
83
+ rep.value = process.to_sym == :product ? 1 : 0
84
+ else
85
+ case process.to_sym
86
+ when :sum
87
+ rep.value += (newValue - oldValue) * weight
88
+ when :average
89
+ rep.value = (rep.value * oldSize + (newValue - oldValue) * weight) / newSize
90
+ when :product
91
+ rep.value = (rep.value * newValue) / oldValue
92
+ else
93
+ raise ArgumentError, "#{process} process is not supported yet"
94
+ end
95
+ end
96
+ save_succeeded = rep.save
97
+ propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
98
+ save_succeeded
99
+ end
100
+
101
+ def normalized_value
102
+ if self.active == 1 || self.active == true
103
+ max = ReputationSystem::Reputation.max(self.reputation_name, self.target_type)
104
+ min = ReputationSystem::Reputation.min(self.reputation_name, self.target_type)
105
+ if max && min
106
+ range = max - min
107
+ range == 0 ? 0 : (self.value - min) / range
108
+ else
109
+ 0
110
+ end
111
+ else
112
+ 0
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ # Updates reputation value for new reputation if its source already exist.
119
+ def self.initialize_reputation_value(receiver, target, process)
120
+ name = receiver.reputation_name
121
+ unless ReputationSystem::Network.is_primary_reputation?(target.class.name, name)
122
+ sender_defs = ReputationSystem::Network.get_reputation_def(target.class.name, name)[:source]
123
+ sender_defs.each do |sd|
124
+ sender_targets = target.get_attributes_of(sd)
125
+ sender_targets.each do |st|
126
+ update_reputation_if_source_exist(sd, st, receiver, process) if receiver.target
127
+ end
128
+ end
129
+ end
130
+ receiver
131
+ end
132
+
133
+ # Propagates updated reputation value to the reputations whose source is the updated reputation.
134
+ def self.propagate_updated_reputation_value(sender, oldValue)
135
+ receiver_defs = ReputationSystem::Network.get_reputation_def(sender.target.class.name, sender.reputation_name)[:source_of]
136
+ receiver_defs.each do |rd|
137
+ targets = sender.target.get_attributes_of(rd)
138
+ targets.each do |target|
139
+ scope = sender.target.evaluate_reputation_scope(rd[:scope])
140
+ send_reputation_message_to_receiver(rd[:reputation], sender, target, scope, oldValue)
141
+ end
142
+ end if receiver_defs
143
+ end
144
+
145
+ def self.send_reputation_message_to_receiver(reputation_name, sender, target, scope, oldValue)
146
+ srn = ReputationSystem::Network.get_scoped_reputation_name(target.class.name, reputation_name, scope)
147
+ process = ReputationSystem::Network.get_reputation_def(target.class.name, srn)[:aggregated_by]
148
+ receiver = find_by_reputation_name_and_target(srn, target)
149
+ if receiver
150
+ weight = ReputationSystem::Network.get_weight_of_source_from_reputation_name_of_target(target, sender.reputation_name, srn)
151
+ update_reputation_value(receiver, sender, weight, process, oldValue)
152
+ # If r is new then value update will be done when it is initialized.
153
+ else
154
+ create_reputation(srn, target, process)
155
+ end
156
+ end
157
+
158
+ def self.update_reputation_value(receiver, sender, weight, process, oldValue)
159
+ unless oldValue
160
+ update_reputation_value_with_new_source(receiver, sender, weight, process)
161
+ else
162
+ newSize = receiver.received_messages.size
163
+ update_reputation_value_with_updated_source(receiver, sender, oldValue, newSize, weight, process)
164
+ end
165
+ end
166
+
167
+ def self.update_reputation_if_source_exist(sd, st, receiver, process)
168
+ scope = receiver.target.evaluate_reputation_scope(sd[:scope])
169
+ srn = ReputationSystem::Network.get_scoped_reputation_name(st.class.name, sd[:reputation], scope)
170
+ source = find_by_reputation_name_and_target(srn, st)
171
+ if source
172
+ update_reputation_value_with_new_source(receiver, source, sd[:weight], process)
173
+ ReputationSystem::ReputationMessage.add_reputation_message_if_not_exist(source, receiver)
174
+ end
175
+ end
176
+
177
+ def self.max(reputation_name, target_type)
178
+ ReputationSystem::Reputation.maximum(:value,
179
+ :conditions => {:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true})
180
+ end
181
+
182
+ def self.min(reputation_name, target_type)
183
+ ReputationSystem::Reputation.minimum(:value,
184
+ :conditions => {:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true})
185
+ end
186
+
187
+ def self.get_target_type_for_sti(target, reputation_name)
188
+ target_class = target.class
189
+ defs = ReputationSystem::Network.get_reputation_defs(target_class.name)[reputation_name.to_sym]
190
+ while target_class && target_class.name != "ActiveRecord::Base" && defs && defs.empty?
191
+ target_class = target_class.superclass
192
+ defs = ReputationSystem::Network.get_reputation_defs(target_class.name)[reputation_name.to_sym]
193
+ end
194
+ target_class ? target_class.name : nil
195
+ end
196
+
197
+ def set_target_type_for_sti
198
+ sti_target_type = self.class.get_target_type_for_sti(target, reputation_name)
199
+ self.target_type = sti_target_type if sti_target_type
200
+ end
201
+
202
+ def change_zero_value_in_case_of_product_process
203
+ self.value = 1 if self.value == 0 && self.aggregated_by == "product"
204
+ end
205
+
206
+ def remove_associated_messages
207
+ ReputationSystem::ReputationMessage.delete_all(:sender_type => self.class.name, :sender_id => self.id)
208
+ ReputationSystem::ReputationMessage.delete_all(:receiver_id => self.id)
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,49 @@
1
+ ##
2
+ # Copyright 2012 Twitter, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ ##
16
+
17
+ module ReputationSystem
18
+ class ReputationMessage < ActiveRecord::Base
19
+ self.table_name = 'rs_reputation_messages'
20
+ belongs_to :sender, :polymorphic => true
21
+ belongs_to :receiver, :class_name => 'ReputationSystem::Reputation'
22
+
23
+ attr_accessible :weight, :sender, :receiver
24
+
25
+ # The same sender cannot send massage to the same receiver more than once.
26
+ validates_uniqueness_of :receiver_id, :scope => [:sender_id, :sender_type]
27
+ validate :sender_must_be_evaluation_or_reputation
28
+
29
+ after_destroy :delete_sender_if_evaluation
30
+
31
+ def self.add_reputation_message_if_not_exist(sender, receiver)
32
+ rm = create(:sender => sender, :receiver => receiver)
33
+ receiver.received_messages.push rm if rm.valid?
34
+ end
35
+
36
+ protected
37
+
38
+ def delete_sender_if_evaluation
39
+ sender.destroy if sender.is_a?(ReputationSystem::Evaluation)
40
+ end
41
+
42
+ def sender_must_be_evaluation_or_reputation
43
+ unless sender.is_a?(ReputationSystem::Evaluation) || sender.is_a?(ReputationSystem::Reputation)
44
+ errors.add(:sender, "must be an evaluation or a reputation")
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -34,6 +34,7 @@ module ReputationSystem
34
34
  reputation_def = reputation_defs[reputation_name.to_sym]
35
35
  if reputation_def == {}
36
36
  begin
37
+ # This recursion finds reputation definition in the ancestor in case of STI.
37
38
  klass = class_name.constantize.superclass
38
39
  reputation_def = get_reputation_def(klass.name, reputation_name) if klass
39
40
  rescue NameError
@@ -49,6 +50,7 @@ module ReputationSystem
49
50
  options[:source] = convert_to_array_if_hash(options[:source])
50
51
  options[:source_of] ||= []
51
52
  options[:source_of] = convert_to_array_if_hash(options[:source_of])
53
+ options[:aggregated_by] = options[:aggregated_by] || :sum
52
54
  assign_self_as_default_value_for_of_attr(options[:source])
53
55
  assign_self_as_default_value_for_of_attr(options[:source_of])
54
56
  reputation_defs[reputation_name] = options
@@ -27,8 +27,8 @@ module ReputationSystem
27
27
  def build_select_statement(table_name, reputation_name, select=nil, srn=nil, normalize=false)
28
28
  select = sanitize_sql_array(["%s.*", table_name]) unless select
29
29
  if normalize
30
- max = RSReputation.max(srn, self.name)
31
- min = RSReputation.min(srn, self.name)
30
+ max = ReputationSystem::Reputation.max(srn, self.name)
31
+ min = ReputationSystem::Reputation.min(srn, self.name)
32
32
  range = max - min
33
33
  if range < DELTA
34
34
  sanitize_sql_array(["%s, (0) AS normalized_%s", select, reputation_name])
@@ -42,8 +42,8 @@ module ReputationSystem
42
42
 
43
43
  def build_select_statement_with_reputation_only(table_name, reputation_name, srn=nil, normalize=false)
44
44
  if normalize
45
- max = RSReputation.max(srn, self.name)
46
- min = RSReputation.min(srn, self.name)
45
+ max = ReputationSystem::Reputation.max(srn, self.name)
46
+ min = ReputationSystem::Reputation.min(srn, self.name)
47
47
  range = max - min
48
48
  if range < DELTA
49
49
  sanitize_sql_array(["(0) AS normalized_%s", reputation_name])
@@ -15,41 +15,31 @@
15
15
  ##
16
16
 
17
17
  module ReputationSystem
18
- module Reputation
19
- def reputation_value_for(reputation_name, *args)
20
- warn "[DEPRECATION] `reputation_value_for` will be deprecated in version 2.0.0. Please use `reputation_for` instead."
21
- reputation_for(reputation_name, *args)
22
- end
23
-
18
+ module ReputationMethods
24
19
  def reputation_for(reputation_name, *args)
25
20
  find_reputation(reputation_name, args.first).value
26
21
  end
27
22
 
28
- def normalized_reputation_value_for(reputation_name, *args)
29
- warn "[DEPRECATION] `normalized_reputation_value_for` will be deprecated in version 2.0.0. Please use `normalized_reputation_for` instead."
30
- normalized_reputation_for(reputation_name, *args)
31
- end
32
-
33
23
  def normalized_reputation_for(reputation_name, *args)
34
24
  find_reputation(reputation_name, args.first).normalized_value
35
25
  end
36
26
 
37
27
  def activate_all_reputations
38
- RSReputation.find(:all, :conditions => {:target_id => self.id, :target_type => self.class.name, :active => false}).each do |r|
28
+ ReputationSystem::Reputation.find(:all, :conditions => {:target_id => self.id, :target_type => self.class.name, :active => false}).each do |r|
39
29
  r.active = true
40
30
  r.save!
41
31
  end
42
32
  end
43
33
 
44
34
  def deactivate_all_reputations
45
- RSReputation.find(:all, :conditions => {:target_id => self.id, :target_type => self.class.name, :active => true}).each do |r|
35
+ ReputationSystem::Reputation.find(:all, :conditions => {:target_id => self.id, :target_type => self.class.name, :active => true}).each do |r|
46
36
  r.active = false
47
37
  r.save!
48
38
  end
49
39
  end
50
40
 
51
41
  def reputations_activated?(reputation_name)
52
- r = RSReputation.find(:first, :conditions => {:reputation_name => reputation_name.to_s, :target_id => self.id, :target_type => self.class.name})
42
+ r = ReputationSystem::Reputation.find(:first, :conditions => {:reputation_name => reputation_name.to_s, :target_id => self.id, :target_type => self.class.name})
53
43
  r ? r.active : false
54
44
  end
55
45
 
@@ -66,7 +56,7 @@ module ReputationSystem
66
56
  raise ArgumentError, "#{reputation_name} is not valid" if !self.class.has_reputation_for?(reputation_name)
67
57
  srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope)
68
58
  process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by]
69
- RSReputation.find_or_create_reputation(srn, self, process)
59
+ ReputationSystem::Reputation.find_or_create_reputation(srn, self, process)
70
60
  end
71
61
  end
72
62
  end
@@ -15,7 +15,7 @@
15
15
  ##
16
16
 
17
17
  module ReputationSystem
18
- module Scope
18
+ module ScopeMethods
19
19
  def self.included(klass)
20
20
  klass.extend ClassMethods
21
21
  end
@@ -34,4 +34,4 @@ module ReputationSystem
34
34
  end
35
35
  end
36
36
  end
37
- end
37
+ end
@@ -15,5 +15,5 @@
15
15
  ##
16
16
 
17
17
  module ReputationSystem
18
- VERSION = "1.5.1"
18
+ VERSION = "2.0.0"
19
19
  end