activerecord-reputation-system 1.0.0

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