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
@@ -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