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
@@ -1,69 +0,0 @@
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
- class RSEvaluation < ActiveRecord::Base
18
- belongs_to :source, :polymorphic => true
19
- belongs_to :target, :polymorphic => true
20
- has_one :sent_messages, :as => :sender, :class_name => 'RSReputationMessage', :dependent => :destroy
21
-
22
- attr_accessible :reputation_name, :value, :source, :source_id, :source_type, :target, :target_id, :target_type
23
-
24
- # Sets an appropriate source type in case of Single Table Inheritance.
25
- before_validation :set_source_type_for_sti
26
-
27
- # the same source cannot evaluate the same target more than once.
28
- validates_uniqueness_of :source_id, :scope => [:reputation_name, :source_type, :target_id, :target_type]
29
- validate :source_must_be_defined_for_reputation_in_network
30
-
31
- def self.find_by_reputation_name_and_source_and_target(reputation_name, source, target)
32
- source_type = get_source_type_for_sti(source, target.class.name, reputation_name)
33
- RSEvaluation.find(:first,
34
- :conditions => {:reputation_name => reputation_name.to_s,
35
- :source_id => source.id,
36
- :source_type => source_type,
37
- :target_id => target.id,
38
- :target_type => target.class.name
39
- })
40
- end
41
-
42
- def self.create_evaluation(reputation_name, value, source, target)
43
- RSEvaluation.create!(:reputation_name => reputation_name.to_s, :value => value,
44
- :source_id => source.id, :source_type => source.class.name,
45
- :target_id => target.id, :target_type => target.class.name)
46
- end
47
-
48
- protected
49
-
50
- def self.get_source_type_for_sti(source, target_type, reputation_name)
51
- valid_source_type = ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize
52
- temp = source.class
53
- while temp && valid_source_type != temp.name && temp.name != "ActiveRecord::Base"
54
- temp = temp.superclass
55
- end
56
- temp ? temp.name : nil
57
- end
58
-
59
- def set_source_type_for_sti
60
- temp = self.class.get_source_type_for_sti(source, target_type, reputation_name)
61
- self.source_type = temp if temp
62
- end
63
-
64
- def source_must_be_defined_for_reputation_in_network
65
- unless source_type == ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize
66
- errors.add(:source_type, "#{source_type} is not source of #{reputation_name} reputation")
67
- end
68
- end
69
- end
@@ -1,204 +0,0 @@
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
- class RSReputation < ActiveRecord::Base
18
- belongs_to :target, :polymorphic => true
19
- has_many :received_messages, :class_name => 'RSReputationMessage', :foreign_key => :receiver_id, :dependent => :destroy do
20
- def from(sender)
21
- self.find_by_sender_id_and_sender_type(sender.id, sender.class.to_s)
22
- end
23
- end
24
- has_many :sent_messages, :as => :sender, :class_name => 'RSReputationMessage', :dependent => :destroy
25
-
26
- attr_accessible :reputation_name, :value, :aggregated_by, :active, :target, :target_id, :target_type, :received_messages
27
-
28
- before_validation :set_target_type_for_sti
29
- before_save :change_zero_value_in_case_of_product_process
30
-
31
- VALID_PROCESSES = ['sum', 'average', 'product']
32
- validates_inclusion_of :aggregated_by, :in => VALID_PROCESSES, :message => "Value chosen for aggregated_by is not valid process"
33
- validates_uniqueness_of :reputation_name, :scope => [:target_id, :target_type]
34
-
35
- def self.find_by_reputation_name_and_target(reputation_name, target)
36
- target_type = get_target_type_for_sti(target, reputation_name)
37
- RSReputation.find_by_reputation_name_and_target_id_and_target_type(reputation_name.to_s, target.id, target_type)
38
- end
39
-
40
- # All external access to reputation should use this since they are created lazily.
41
- def self.find_or_create_reputation(reputation_name, target, process)
42
- rep = find_by_reputation_name_and_target(reputation_name, target)
43
- rep ? rep : create_reputation(reputation_name, target, process)
44
- end
45
-
46
- def self.create_reputation(reputation_name, target, process)
47
- create_options = {:reputation_name => reputation_name.to_s, :target_id => target.id,
48
- :target_type => target.class.name, :aggregated_by => process.to_s}
49
- default_value = ReputationSystem::Network.get_reputation_def(target.class.name, reputation_name)[:init_value]
50
- create_options.merge!(:value => default_value) if default_value
51
- rep = create(create_options)
52
- initialize_reputation_value(rep, target, process)
53
- end
54
-
55
- def self.update_reputation_value_with_new_source(rep, source, weight, process)
56
- weight ||= 1 # weight is 1 by default.
57
- size = rep.received_messages.size
58
- valueBeforeUpdate = size > 0 ? rep.value : nil
59
- newValue = source.value
60
- case process.to_sym
61
- when :sum
62
- rep.value += (newValue * weight)
63
- when :average
64
- rep.value = (rep.value * size + newValue * weight) / (size + 1)
65
- when :product
66
- rep.value *= (newValue * weight)
67
- else
68
- raise ArgumentError, "#{process} process is not supported yet"
69
- end
70
- save_succeeded = rep.save
71
- RSReputationMessage.add_reputation_message_if_not_exist(source, rep)
72
- propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
73
- save_succeeded
74
- end
75
-
76
- def self.update_reputation_value_with_updated_source(rep, source, oldValue, weight, process)
77
- weight ||= 1 # weight is 1 by default.
78
- size = rep.received_messages.size
79
- valueBeforeUpdate = size > 0 ? rep.value : nil
80
- newValue = source.value
81
- case process.to_sym
82
- when :sum
83
- rep.value += (newValue - oldValue) * weight
84
- when :average
85
- rep.value += ((newValue - oldValue) * weight) / size
86
- when :product
87
- rep.value = (rep.value * newValue) / oldValue
88
- else
89
- raise ArgumentError, "#{process} process is not supported yet"
90
- end
91
- save_succeeded = rep.save
92
- propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
93
- save_succeeded
94
- end
95
-
96
- def normalized_value
97
- if self.active == 1 || self.active == true
98
- max = RSReputation.max(self.reputation_name, self.target_type)
99
- min = RSReputation.min(self.reputation_name, self.target_type)
100
- if max && min
101
- range = max - min
102
- range == 0 ? 0 : (self.value - min) / range
103
- else
104
- 0
105
- end
106
- else
107
- 0
108
- end
109
- end
110
-
111
- protected
112
-
113
- # Updates reputation value for new reputation if its source already exist.
114
- def self.initialize_reputation_value(receiver, target, process)
115
- name = receiver.reputation_name
116
- unless ReputationSystem::Network.is_primary_reputation?(target.class.name, name)
117
- sender_defs = ReputationSystem::Network.get_reputation_def(target.class.name, name)[:source]
118
- sender_defs.each do |sd|
119
- sender_targets = target.get_attributes_of(sd)
120
- sender_targets.each do |st|
121
- update_reputation_if_source_exist(sd, st, receiver, process) if receiver.target
122
- end
123
- end
124
- end
125
- receiver
126
- end
127
-
128
- # Propagates updated reputation value to the reputations whose source is the updated reputation.
129
- def self.propagate_updated_reputation_value(sender, oldValue)
130
- receiver_defs = ReputationSystem::Network.get_reputation_def(sender.target.class.name, sender.reputation_name)[:source_of]
131
- receiver_defs.each do |rd|
132
- targets = sender.target.get_attributes_of(rd)
133
- targets.each do |target|
134
- scope = sender.target.evaluate_reputation_scope(rd[:scope])
135
- send_reputation_message_to_receiver(rd[:reputation], sender, target, scope, oldValue)
136
- end
137
- end if receiver_defs
138
- end
139
-
140
- def self.send_reputation_message_to_receiver(reputation_name, sender, target, scope, oldValue)
141
- srn = ReputationSystem::Network.get_scoped_reputation_name(target.class.name, reputation_name, scope)
142
- process = ReputationSystem::Network.get_reputation_def(target.class.name, srn)[:aggregated_by]
143
- receiver = find_by_reputation_name_and_target(srn, target)
144
- if receiver
145
- weight = ReputationSystem::Network.get_weight_of_source_from_reputation_name_of_target(target, sender.reputation_name, srn)
146
- update_reputation_value(receiver, sender, weight, process, oldValue)
147
- # If r is new then value update will be done when it is initialized.
148
- else
149
- create_reputation(srn, target, process)
150
- end
151
- end
152
-
153
- def self.update_reputation_value(receiver, sender, weight, process, oldValue)
154
- unless oldValue
155
- update_reputation_value_with_new_source(receiver, sender, weight, process)
156
- else
157
- update_reputation_value_with_updated_source(receiver, sender, oldValue, weight, process)
158
- end
159
- end
160
-
161
- def self.update_reputation_if_source_exist(sd, st, receiver, process)
162
- scope = receiver.target.evaluate_reputation_scope(sd[:scope])
163
- srn = ReputationSystem::Network.get_scoped_reputation_name(st.class.name, sd[:reputation], scope)
164
- source = find_by_reputation_name_and_target(srn, st)
165
- if source
166
- update_reputation_value_with_new_source(receiver, source, sd[:weight], process)
167
- RSReputationMessage.add_reputation_message_if_not_exist(source, receiver)
168
- end
169
- end
170
-
171
- def self.max(reputation_name, target_type)
172
- RSReputation.maximum(:value,
173
- :conditions => {:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true})
174
- end
175
-
176
- def self.min(reputation_name, target_type)
177
- RSReputation.minimum(:value,
178
- :conditions => {:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true})
179
- end
180
-
181
- def self.get_target_type_for_sti(target, reputation_name)
182
- temp = target.class
183
- defs = ReputationSystem::Network.get_reputation_defs(temp.name)[reputation_name.to_sym]
184
- while temp && temp.name != "ActiveRecord::Base" && defs && defs.empty?
185
- temp = temp.superclass
186
- defs = ReputationSystem::Network.get_reputation_defs(temp.name)[reputation_name.to_sym]
187
- end
188
- temp ? temp.name : nil
189
- end
190
-
191
- def set_target_type_for_sti
192
- temp = self.class.get_target_type_for_sti(target, reputation_name)
193
- self.target_type = temp if temp
194
- end
195
-
196
- def change_zero_value_in_case_of_product_process
197
- self.value = 1 if self.value == 0 && self.aggregated_by == "product"
198
- end
199
-
200
- def remove_associated_messages
201
- RSReputationMessage.delete_all(:sender_type => self.class.name, :sender_id => self.id)
202
- RSReputationMessage.delete_all(:receiver_id => self.id)
203
- end
204
- end
@@ -1,46 +0,0 @@
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
- class RSReputationMessage < ActiveRecord::Base
18
- belongs_to :sender, :polymorphic => true
19
- belongs_to :receiver, :class_name => 'RSReputation'
20
-
21
- attr_accessible :weight, :sender, :receiver
22
-
23
- # The same sender cannot send massage to the same receiver more than once.
24
- validates_uniqueness_of :receiver_id, :scope => [:sender_id, :sender_type]
25
- validate :sender_must_be_evaluation_or_reputation
26
-
27
- after_destroy :delete_sender_if_evaluation
28
-
29
- def self.add_reputation_message_if_not_exist(sender, receiver)
30
- rm = create(:sender => sender, :receiver => receiver)
31
- receiver.received_messages.push rm if rm.valid?
32
- end
33
-
34
- protected
35
-
36
- def delete_sender_if_evaluation
37
- sender.destroy if sender.is_a?(RSEvaluation)
38
- end
39
-
40
- def sender_must_be_evaluation_or_reputation
41
- unless sender.is_a?(RSEvaluation) || sender.is_a?(RSReputation)
42
- errors.add(:sender, "must be an evaluation or a reputation")
43
- end
44
- end
45
-
46
- end
@@ -1,119 +0,0 @@
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
- require 'spec_helper'
18
-
19
- describe RSReputation do
20
- before(:each) do
21
- @user = User.create!(:name => 'jack')
22
- end
23
-
24
- context "Validation" do
25
- it "should have value 0 by default in case of non product process" do
26
- r = RSReputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum')
27
- r.value.should == 0
28
- end
29
-
30
- it "should be able to change value to 0 if process is not product process" do
31
- r = RSReputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum', :value => 10)
32
- r.value = 0
33
- r.save!
34
- r.reload
35
- r.value.should == 0
36
- end
37
-
38
- it "should have value 1 by default in case of product process" do
39
- r = RSReputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'product')
40
- r.value.should == 1
41
- end
42
-
43
- it "should be able to create reputation with process 'sum', 'average' and 'product'" do
44
- RSReputation.create(:reputation_name => "karma1", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum').should be_valid
45
- RSReputation.create(:reputation_name => "karma2", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'average').should be_valid
46
- RSReputation.create(:reputation_name => "karma3", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'product').should be_valid
47
- end
48
-
49
- it "should not be able to create reputation with process other than 'sum', 'average' and 'product'" do
50
- RSReputation.create(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'invalid').should_not be_valid
51
- end
52
-
53
- it "should not be able to create reputation of the same name for the same target" do
54
- RSReputation.create(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum').should be_valid
55
- RSReputation.create(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum').should_not be_valid
56
- end
57
- end
58
-
59
- context "Callback" do
60
- describe "#set_target_type_for_sti" do
61
- it "should assign target class name as target type if not STI" do
62
- question = Question.create!(:text => 'Does this work?', :author_id => @user.id)
63
- question.add_evaluation(:total_votes, 5, @user)
64
- rep = RSReputation.find_by_reputation_name_and_target(:total_votes, question)
65
- rep.target_type.should == question.class.name
66
- end
67
- it "should assign target's ancestors class name where reputation is declared if STI" do
68
- designer = Designer.create! :name => 'hiro'
69
- programmer = Programmer.create! :name => 'katsuya'
70
- programmer.add_evaluation(:leadership, 1, designer)
71
- rep = RSReputation.find_by_reputation_name_and_target(:leadership, programmer)
72
- rep.target_type.should == Person.name
73
- end
74
- end
75
- end
76
-
77
- context "Association" do
78
- before :each do
79
- @question = Question.create!(:text => 'What is Twitter?', :author_id => @user.id)
80
- @question.add_evaluation(:total_votes, 5, @user)
81
- end
82
-
83
- it "should delete associated received messages" do
84
- rep = RSReputation.find_by_target_id_and_target_type(@question.id, 'Question')
85
- RSReputationMessage.find_by_receiver_id(rep.id).should_not be_nil
86
- rep.destroy
87
- RSReputationMessage.find_by_receiver_id(rep.id).should be_nil
88
- end
89
-
90
- it "should delete associated sent messages" do
91
- rep = RSReputation.find_by_target_id_and_target_type(@user.id, 'User')
92
- RSReputationMessage.find_by_sender_id_and_sender_type(rep.id, rep.class.name).should_not be_nil
93
- rep.destroy
94
- RSReputationMessage.find_by_sender_id_and_sender_type(rep.id, rep.class.name).should be_nil
95
- end
96
- end
97
-
98
- describe "#normalized_value" do
99
- before :each do
100
- @user2 = User.create!(:name => 'dick')
101
- @user3 = User.create!(:name => 'foo')
102
- question = Question.new(:text => "Does this work?", :author_id => @user.id)
103
- @r1 = RSReputation.create!(:reputation_name => "karma", :value => 2, :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum')
104
- @r2 = RSReputation.create!(:reputation_name => "karma", :value => 6, :target_id => @user2.id, :target_type => @user2.class.to_s, :aggregated_by => 'sum')
105
- @r3 = RSReputation.create!(:reputation_name => "karma", :value => 10, :target_id => @user3.id, :target_type => @user3.class.to_s, :aggregated_by => 'sum')
106
- @r4 = RSReputation.create!(:reputation_name => "karma", :value => 10, :target_id => question.id, :target_type => question.class.to_s, :aggregated_by => 'sum')
107
- end
108
-
109
- it "should return correct normalized value" do
110
- @r1.normalized_value.should be_within(DELTA).of(0)
111
- @r2.normalized_value.should be_within(DELTA).of(0.5)
112
- @r3.normalized_value.should be_within(DELTA).of(1)
113
- end
114
-
115
- it "should return 0 if max and min are the same" do
116
- @r4.normalized_value.should be_within(DELTA).of(0)
117
- end
118
- end
119
- end