activerecord-reputation-system 1.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.
@@ -0,0 +1,35 @@
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 'rails/generators'
18
+ require 'rails/generators/migration'
19
+ require 'rails/generators/active_record'
20
+
21
+ class ReputationSystemGenerator < Rails::Generators::Base
22
+ include Rails::Generators::Migration
23
+
24
+ desc "Creates migration files required by reputation system gem."
25
+
26
+ self.source_paths << File.join(File.dirname(__FILE__), 'templates')
27
+
28
+ def self.next_migration_number(path)
29
+ ActiveRecord::Generators::Base.next_migration_number(path)
30
+ end
31
+
32
+ def create_migration_files
33
+ migration_template 'create_reputation_system.rb', 'db/migrate/create_reputation_system.rb'
34
+ end
35
+ end
@@ -0,0 +1,59 @@
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 CreateReputationSystem < ActiveRecord::Migration
18
+ def self.up
19
+ create_table :rs_evaluations do |t|
20
+ t.string :reputation_name
21
+ t.references :source, :polymorphic => true
22
+ t.references :target, :polymorphic => true
23
+ t.float :value, :default => 0
24
+ t.timestamps
25
+ end
26
+
27
+ add_index :rs_evaluations, :reputation_name
28
+ add_index :rs_evaluations, [:target_id, :target_type]
29
+ add_index :rs_evaluations, [:source_id, :source_type]
30
+
31
+ create_table :rs_reputations do |t|
32
+ t.string :reputation_name
33
+ t.float :value, :default => 0
34
+ t.string :aggregated_by
35
+ t.references :target, :polymorphic => true
36
+ t.boolean :active, :default => true
37
+ t.timestamps
38
+ end
39
+
40
+ add_index :rs_reputations, :reputation_name
41
+ add_index :rs_reputations, [:target_id, :target_type]
42
+
43
+ create_table :rs_reputation_messages do |t|
44
+ t.references :sender, :polymorphic => true
45
+ t.integer :receiver_id
46
+ t.float :weight, :default => 1
47
+ t.timestamps
48
+ end
49
+
50
+ add_index :rs_reputation_messages, [:sender_id, :sender_type]
51
+ add_index :rs_reputation_messages, :receiver_id
52
+ end
53
+
54
+ def self.down
55
+ drop_table :rs_evaluations
56
+ drop_table :rs_reputations
57
+ drop_table :rs_reputation_messages
58
+ end
59
+ end
@@ -0,0 +1,52 @@
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
+ # the same source cannot evaluate the same target more than once.
25
+ validates_uniqueness_of :source_id, :scope => [:reputation_name, :source_type, :target_id, :target_type]
26
+ validate :source_must_be_defined_for_reputation_in_network
27
+
28
+ def self.find_by_reputation_name_and_source_and_target(reputation_name, source, target)
29
+ RSEvaluation.find(:first,
30
+ :conditions => {:reputation_name => reputation_name.to_s,
31
+ :source_id => source.id,
32
+ :source_type => source.class.name,
33
+ :target_id => target.id,
34
+ :target_type => target.class.name
35
+ })
36
+ end
37
+
38
+ def self.create_evaluation(reputation_name, value, source, target)
39
+ reputation_name = reputation_name.to_sym
40
+ RSEvaluation.create!(:reputation_name => reputation_name.to_s, :value => value,
41
+ :source_id => source.id, :source_type => source.class.name,
42
+ :target_id => target.id, :target_type => target.class.name)
43
+ end
44
+
45
+ protected
46
+
47
+ def source_must_be_defined_for_reputation_in_network
48
+ unless source_type == ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize
49
+ errors.add(:source_type, "#{source_type} is not source of #{reputation_name} reputation")
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,181 @@
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_save :change_zero_value_in_case_of_product_process
29
+ before_create
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
+ RSReputation.find_by_reputation_name_and_target_id_and_target_type(reputation_name.to_s, target.id, target.class.name)
37
+ end
38
+
39
+ # All external access to reputation should use this since they are created lazily.
40
+ def self.find_or_create_reputation(reputation_name, target, process)
41
+ rep = find_by_reputation_name_and_target(reputation_name, target)
42
+ rep ? rep : create_reputation(reputation_name, target, process)
43
+ end
44
+
45
+ def self.create_reputation(reputation_name, target, process)
46
+ create_options = {:reputation_name => reputation_name.to_s, :target_id => target.id,
47
+ :target_type => target.class.name, :aggregated_by => process.to_s}
48
+ default_value = ReputationSystem::Network.get_reputation_def(target.class.name, reputation_name)[:init_value]
49
+ create_options.merge!(:value => default_value) if default_value
50
+ rep = create(create_options)
51
+ initialize_reputation_value(rep, target, process)
52
+ end
53
+
54
+ def self.update_reputation_value_with_new_source(rep, source, weight, process)
55
+ weight = 1 unless weight # weight is 1 by default.
56
+ size = rep.received_messages.size
57
+ valueBeforeUpdate = size > 0 ? rep.value : nil
58
+ newValue = source.value
59
+ case process.to_sym
60
+ when :sum
61
+ rep.value += (newValue * weight)
62
+ when :average
63
+ rep.value = (rep.value * size + newValue * weight) / (size + 1)
64
+ when :product
65
+ rep.value *= (newValue * weight)
66
+ else
67
+ raise ArgumentError, "#{process} process is not supported yet"
68
+ end
69
+ rep.save!
70
+ RSReputationMessage.add_reputation_message_if_not_exist(source, rep)
71
+ propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
72
+ end
73
+
74
+ def self.update_reputation_value_with_updated_source(rep, source, oldValue, weight, process)
75
+ weight = 1 unless weight # weight is 1 by default.\
76
+ size = rep.received_messages.size
77
+ valueBeforeUpdate = size > 0 ? rep.value : nil
78
+ newValue = source.value
79
+ case process.to_sym
80
+ when :sum
81
+ rep.value = rep.value + (newValue - oldValue) * weight
82
+ when :average
83
+ rep.value = rep.value + ((newValue - oldValue) * weight) / size
84
+ when :product
85
+ rep.value = (rep.value * newValue) / oldValue
86
+ else
87
+ raise ArgumentError, "#{process} process is not supported yet"
88
+ end
89
+ rep.save!
90
+ propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target
91
+ end
92
+
93
+ def normalized_value
94
+ if self.active == 1 || self.active == true
95
+ max = RSReputation.max(self.reputation_name, self.target_type)
96
+ min = RSReputation.min(self.reputation_name, self.target_type)
97
+ if max && min
98
+ range = max - min
99
+ range == 0 ? 0 : (self.value - min) / range
100
+ else
101
+ 0
102
+ end
103
+ else
104
+ 0
105
+ end
106
+ end
107
+
108
+ protected
109
+
110
+ # Updates reputation value for new reputation if its source already exist.
111
+ def self.initialize_reputation_value(receiver, target, process)
112
+ name = receiver.reputation_name
113
+ unless ReputationSystem::Network.is_primary_reputation?(target.class.name, name)
114
+ sender_defs = ReputationSystem::Network.get_reputation_def(target.class.name, name)[:source]
115
+ sender_defs.each do |sd|
116
+ sender_targets = target.get_attributes_of(sd)
117
+ sender_targets.each do |st|
118
+ update_reputation_if_source_exist(sd, st, receiver, process) if receiver.target
119
+ end
120
+ end
121
+ end
122
+ receiver
123
+ end
124
+
125
+ # Propagates updated reputation value to the reputations whose source is the updated reputation.
126
+ def self.propagate_updated_reputation_value(sender, oldValue)
127
+ sender_name = sender.reputation_name.to_sym
128
+ receiver_defs = ReputationSystem::Network.get_reputation_def(sender.target.class.name, sender_name)[:source_of]
129
+ if receiver_defs
130
+ receiver_defs.each do |rd|
131
+ receiver_targets = sender.target.get_attributes_of(rd)
132
+ receiver_targets.each do |rt|
133
+ scope = sender.target.evaluate_reputation_scope(rd[:scope])
134
+ srn = ReputationSystem::Network.get_scoped_reputation_name(rt.class.name, rd[:reputation], scope)
135
+ process = ReputationSystem::Network.get_reputation_def(rt.class.name, srn)[:aggregated_by]
136
+ rep = find_by_reputation_name_and_target(srn, rt)
137
+ if rep
138
+ weight = ReputationSystem::Network.get_weight_of_source_from_reputation_name_of_target(rt, sender_name, srn)
139
+ unless oldValue
140
+ update_reputation_value_with_new_source(rep, sender, weight, process)
141
+ else
142
+ update_reputation_value_with_updated_source(rep, sender, oldValue, weight, process)
143
+ end
144
+ # If r is new then value update will be done when it is initialized.
145
+ else
146
+ create_reputation(srn, rt, process)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ def self.update_reputation_if_source_exist(sd, st, receiver, process)
154
+ scope = receiver.target.evaluate_reputation_scope(sd[:scope])
155
+ srn = ReputationSystem::Network.get_scoped_reputation_name(st.class.name, sd[:reputation], scope)
156
+ source = find_by_reputation_name_and_target(srn, st)
157
+ if source
158
+ update_reputation_value_with_new_source(receiver, source, sd[:weight], process)
159
+ RSReputationMessage.add_reputation_message_if_not_exist(source, receiver)
160
+ end
161
+ end
162
+
163
+ def self.max(reputation_name, target_type)
164
+ RSReputation.maximum(:value,
165
+ :conditions => {:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true})
166
+ end
167
+
168
+ def self.min(reputation_name, target_type)
169
+ RSReputation.minimum(:value,
170
+ :conditions => {:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true})
171
+ end
172
+
173
+ def change_zero_value_in_case_of_product_process
174
+ self.value = 1 if self.value == 0 && self.aggregated_by == "product"
175
+ end
176
+
177
+ def remove_associated_messages
178
+ RSReputationMessage.delete_all(:sender_type => self.class.name, :sender_id => self.id)
179
+ RSReputationMessage.delete_all(:receiver_id => self.id)
180
+ end
181
+ end
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,28 @@
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 'reputation_system/base'
18
+ require 'reputation_system/query'
19
+ require 'reputation_system/normalization'
20
+ require 'reputation_system/evaluation'
21
+ require 'reputation_system/network'
22
+ require 'reputation_system/reputation'
23
+ require 'reputation_system/scope'
24
+ require 'models/rs_evaluation'
25
+ require 'models/rs_reputation'
26
+ require 'models/rs_reputation_message'
27
+
28
+ ActiveRecord::Base.send(:include, ReputationSystem::Base)
@@ -0,0 +1,70 @@
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
+ module Base
19
+ def self.included(klass)
20
+ klass.extend ClassMethods
21
+ end
22
+
23
+ def get_attributes_of(reputation)
24
+ of = reputation[:of]
25
+ attrs = reputation[:of] == :self ? self : self.instance_eval(of.to_s) if of.is_a?(String) || of.is_a?(Symbol)
26
+ attrs = self.instance_exec(self, &of) if of.is_a?(Proc)
27
+ attrs = [attrs] unless attrs.is_a? Array
28
+ attrs
29
+ end
30
+
31
+ def evaluate_reputation_scope(scope)
32
+ if scope
33
+ if self.respond_to? scope
34
+ self.send(scope)
35
+ else
36
+ scope
37
+ end
38
+ end
39
+ end
40
+
41
+ module ClassMethods
42
+ def has_reputation(reputation_name, options)
43
+ has_valid_input = reputation_name && options[:aggregated_by] && options[:source]
44
+
45
+ raise ArgumentError, "has_reputation method received invalid arguments." unless has_valid_input
46
+ # Overwrites reputation if the same reputation name is declared in the same model.
47
+ # TODO: This should raise exception instead while allowing Rails app to reload in dev mode.
48
+ ReputationSystem::Network.remove_reputation_def(name, reputation_name) if has_reputation_for?(reputation_name)
49
+
50
+ # If it is first time to be called
51
+ unless ancestors.include?(ReputationSystem::Reputation)
52
+ has_many :reputations, :as => :target, :class_name => "RSReputation", :dependent => :destroy
53
+ include ReputationSystem::Query
54
+ include ReputationSystem::Normalization
55
+ include ReputationSystem::Reputation
56
+ include ReputationSystem::Scope
57
+ end
58
+
59
+ ReputationSystem::Network.add_reputation_def(name, reputation_name, options)
60
+
61
+ # evaluation related methods are defined only for primary reputations
62
+ include ReputationSystem::Evaluation if ReputationSystem::Network.is_primary_reputation?(name, reputation_name) && !ancestors.include?(ReputationSystem::Evaluation)
63
+ end
64
+
65
+ def has_reputation_for?(reputation_name)
66
+ ReputationSystem::Network.has_reputation_for?(name, reputation_name)
67
+ end
68
+ end
69
+ end
70
+ end