activerecord-reputation-system 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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