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,88 @@
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 Evaluation
19
+ def add_evaluation(reputation_name, value, source, *args)
20
+ raise ArgumentError, "#{reputation_name.to_s} is not defined for #{self.class.name}" unless ReputationSystem::Network.has_reputation_for?(self.class.name, reputation_name)
21
+ scope = args.first
22
+ srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope)
23
+ process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by]
24
+ evaluation = RSEvaluation.create_evaluation(srn, value, source, self)
25
+ rep = RSReputation.find_or_create_reputation(srn, self, process)
26
+ RSReputation.update_reputation_value_with_new_source(rep, evaluation, 1, process)
27
+ end
28
+
29
+ def update_evaluation(reputation_name, value, source, *args)
30
+ raise ArgumentError, "#{reputation_name.to_s} is not defined for #{self.class.name}" unless ReputationSystem::Network.has_reputation_for?(self.class.name, reputation_name)
31
+ scope = args.first
32
+ srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope)
33
+ evaluation = RSEvaluation.find_by_reputation_name_and_source_and_target(srn, source, self)
34
+ if evaluation.nil?
35
+ raise ArgumentError, "Given instance of #{source.class.name} has not evaluated #{reputation_name} of the instance of #{self.class.name} yet."
36
+ else
37
+ oldValue = evaluation.value
38
+ evaluation.value = value
39
+ evaluation.save!
40
+ process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by]
41
+ rep = RSReputation.find_by_reputation_name_and_target(srn, self)
42
+ RSReputation.update_reputation_value_with_updated_source(rep, evaluation, oldValue, 1, process)
43
+ end
44
+ end
45
+
46
+ def add_or_update_evaluation(reputation_name, value, source, *args)
47
+ scope = args.first
48
+ evaluation = RSEvaluation.find_by_reputation_name_and_source_and_target(reputation_name, source, self)
49
+ if evaluation.nil?
50
+ self.add_evaluation(reputation_name, value, source, scope)
51
+ else
52
+ self.update_evaluation(reputation_name, value, source, scope)
53
+ end
54
+ end
55
+
56
+ def delete_evaluation(reputation_name, source, *args)
57
+ raise ArgumentError, "#{reputation_name.to_s} is not defined for #{self.class.name}" unless ReputationSystem::Network.has_reputation_for?(self.class.name, reputation_name)
58
+ scope = args.first
59
+ srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope)
60
+ evaluation = RSEvaluation.find_by_reputation_name_and_source_and_target(srn, source, self)
61
+ unless evaluation.nil?
62
+ process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by]
63
+ oldValue = evaluation.value
64
+ evaluation.value = process == :product ? 1 : 0
65
+ rep = RSReputation.find_by_reputation_name_and_target(srn, self)
66
+ RSReputation.update_reputation_value_with_updated_source(rep, evaluation, oldValue, 1, process)
67
+ evaluation.destroy
68
+ end
69
+ end
70
+
71
+ def delete_evaluation!(reputation_name, source, *args)
72
+ raise ArgumentError, "#{reputation_name.to_s} is not defined for #{self.class.name}" unless ReputationSystem::Network.has_reputation_for?(self.class.name, reputation_name)
73
+ scope = args.first
74
+ srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope)
75
+ evaluation = RSEvaluation.find_by_reputation_name_and_source_and_target(srn, source, self)
76
+ if evaluation.nil?
77
+ raise ArgumentError, "Given instance of #{source.class.name} has not evaluated #{reputation_name} of the instance of #{self.class.name} yet."
78
+ else
79
+ process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by]
80
+ oldValue = evaluation.value
81
+ evaluation.value = process == :product ? 1 : 0
82
+ rep = RSReputation.find_by_reputation_name_and_target(srn, self)
83
+ RSReputation.update_reputation_value_with_updated_source(rep, evaluation, oldValue, 1, process)
84
+ evaluation.destroy
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,222 @@
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 Network
19
+ class << self
20
+ def has_reputation_for?(class_name, reputation_name)
21
+ reputation_defs = get_reputation_defs(class_name)
22
+ reputation_defs[reputation_name.to_sym] && reputation_defs[reputation_name.to_sym][:source]
23
+ end
24
+
25
+ def get_reputation_defs(class_name)
26
+ network[class_name.to_sym] = {} unless network[class_name.to_sym]
27
+ network[class_name.to_sym]
28
+ end
29
+
30
+ def get_reputation_def(class_name, reputation_name)
31
+ reputation_defs = get_reputation_defs(class_name)
32
+ reputation_defs[reputation_name.to_sym] = {} unless reputation_defs[reputation_name.to_sym]
33
+ reputation_defs[reputation_name.to_sym]
34
+ end
35
+
36
+ def add_reputation_def(class_name, reputation_name, options)
37
+ reputation_defs = get_reputation_defs(class_name)
38
+ options[:source] = convert_to_array_if_hash(options[:source])
39
+ options[:source_of] = [] unless options[:source_of]
40
+ options[:source_of] = convert_to_array_if_hash(options[:source_of])
41
+ assign_self_as_default_value_for_of_attr(options[:source])
42
+ assign_self_as_default_value_for_of_attr(options[:source_of])
43
+ reputation_defs[reputation_name] = options
44
+ options[:source].each do |s|
45
+ src_class_name = derive_class_name_from_attribute(class_name, s[:of])
46
+ if has_reputation_for?(src_class_name, s[:reputation])
47
+ derive_source_of_from_source(class_name, reputation_name, s, src_class_name)
48
+ else
49
+ # Because the source class might not have been initialized at this time.
50
+ derive_source_of_from_source_later(class_name, reputation_name, s, src_class_name)
51
+ end
52
+ end unless is_primary_reputation?(class_name, reputation_name)
53
+ perform_derive_later(class_name, reputation_name)
54
+ construct_scoped_reputation_options(class_name, reputation_name, options)
55
+ end
56
+
57
+ def remove_reputation_def(class_name, reputation_name)
58
+ reputation_defs = get_reputation_defs(class_name)
59
+ reputation_defs.delete(reputation_name.to_sym)
60
+ end
61
+
62
+ def is_primary_reputation?(class_name, reputation_name)
63
+ options = get_reputation_def(class_name, reputation_name)
64
+ options[:source].is_a?(Symbol)
65
+ end
66
+
67
+ def add_scope_for(class_name, reputation_name, scope)
68
+ options = get_reputation_def(class_name, reputation_name)
69
+ if has_scope?(class_name, reputation_name, scope)
70
+ raise ArgumentError, "#{scope.to_s} is already defined for #{reputation_name.to_s}"
71
+ else
72
+ options[:scopes].push scope.to_sym if options[:scopes]
73
+ create_scoped_reputation_def(class_name, reputation_name, scope, options)
74
+ end
75
+ end
76
+
77
+ def has_scopes?(class_name, reputation_name)
78
+ !get_reputation_def(class_name, reputation_name)[:scopes].nil?
79
+ end
80
+
81
+ def has_scope?(class_name, reputation_name, scope)
82
+ scopes = get_reputation_def(class_name, reputation_name)[:scopes]
83
+ scopes && scopes.include?(scope.to_sym)
84
+ end
85
+
86
+ def get_scoped_reputation_name(class_name, reputation_name, scope)
87
+ scope = scope.to_sym if scope
88
+ validate_scope_necessity(class_name, reputation_name, scope)
89
+ validate_scope_existence(class_name, reputation_name, scope)
90
+ "#{reputation_name.to_s}#{"_#{scope.to_s}" if scope}"
91
+ end
92
+
93
+ def get_weight_of_source_from_reputation_name_of_target(target, source_name, reputation_name)
94
+ source = get_reputation_def(target.class.name, reputation_name)[:source]
95
+ if source.is_a?(Array)
96
+ source.each do |s|
97
+ scope = target.evaluate_reputation_scope(s[:scope]) if s[:scope]
98
+ of = target.get_attributes_of(s)
99
+ srn = get_scoped_reputation_name((of.is_a?(Array) ? of[0] : of ).class.name, s[:reputation], scope)
100
+ source = s if srn.to_sym == source_name.to_sym
101
+ end
102
+ end
103
+ source[:weight]
104
+ end
105
+
106
+ protected
107
+
108
+ def network
109
+ @network = {} unless @network
110
+ @network
111
+ end
112
+
113
+ def data_for_derive_later
114
+ @data_for_derive_later = {} unless @data_for_derive_later
115
+ @data_for_derive_later
116
+ end
117
+
118
+ def create_scoped_reputation_def(class_name, reputation_name, scope, options)
119
+ raise ArgumentError, "#{reputation_name.to_s} does not have scope." unless has_scopes?(class_name, reputation_name)
120
+ scope_options = {}
121
+ reputation_def = get_reputation_def(class_name, reputation_name)
122
+ if is_primary_reputation?(class_name, reputation_name)
123
+ scope_options[:source] = options[:source]
124
+ else
125
+ scope_options[:source] = []
126
+ reputation_def[:source].each do |s|
127
+ rep = {}
128
+ rep[:reputation] = s[:reputation]
129
+ # Passing "this" is not pretty but in some case "instance_exec" method
130
+ # does not give right context for some reason.
131
+ # This could be ruby bug. Needs further investigation.
132
+ rep[:of] = lambda { |this| instance_exec(this, scope.to_s, &s[:of]) } if s[:of].is_a? Proc
133
+ scope_options[:source].push rep
134
+ end
135
+ end
136
+ source_of = reputation_def[:source_of]
137
+ source_of.each do |so|
138
+ if so[:defined_for_scope].nil? || (so[:defined_for_scope] && so[:defined_for_scope].include?(scope.to_sym))
139
+ scope_options[:source_of] ||= []
140
+ scope_options[:source_of].push so
141
+ end
142
+ end if source_of
143
+ scope_options[:aggregated_by] = options[:aggregated_by]
144
+ srn = get_scoped_reputation_name(class_name, reputation_name, scope)
145
+ network[class_name.to_sym][srn.to_sym] = scope_options
146
+ end
147
+
148
+ def construct_scoped_reputation_options(class_name, reputation_name, options)
149
+ scopes = get_reputation_def(class_name, reputation_name)[:scopes]
150
+ scopes.each do |scope|
151
+ create_scoped_reputation_def(class_name, reputation_name, scope, options)
152
+ end if scopes
153
+ end
154
+
155
+ def derive_source_of_from_source(class_name, reputation_name, source, src_class_name)
156
+ if source[:of] && source[:of].is_a?(Symbol) && source[:of] != :self
157
+ klass = src_class_name.to_s.constantize
158
+ of_value = class_name.tableize
159
+ of_value = of_value.chomp('s') unless klass.instance_methods.include?(of_value.to_s) || klass.instance_methods.include?(of_value.to_sym)
160
+ else
161
+ of_value = "self"
162
+ end
163
+ reputation_def = get_reputation_def(src_class_name, source[:reputation])
164
+ reputation_def[:source_of] ||= []
165
+ unless reputation_def[:source_of].any? {|elem| elem[:reputation] == reputation_name.to_sym}
166
+ reputation_def[:source_of] << {:reputation => reputation_name.to_sym, :of => of_value.to_sym}
167
+ end
168
+ end
169
+
170
+ def derive_source_of_from_source_later(class_name, reputation_name, source, src_class_name)
171
+ reputation = source[:reputation].to_sym
172
+ src_class_name = src_class_name.to_sym
173
+ data = data_for_derive_later
174
+ data[src_class_name] ||= {}
175
+ data[src_class_name][reputation] ||= {}
176
+ data[src_class_name][reputation].merge!(:source => source, :class_name => class_name, :reputation_name => reputation_name)
177
+ end
178
+
179
+ def perform_derive_later(src_class_name, reputation)
180
+ src_class_name = src_class_name.to_sym
181
+ reputation = reputation.to_sym
182
+ data = data_for_derive_later
183
+ if data[src_class_name] && data[src_class_name][reputation]
184
+ class_name = data[src_class_name][reputation][:class_name]
185
+ source = data[src_class_name][reputation][:source]
186
+ reputation_name = data[src_class_name][reputation][:reputation_name]
187
+ derive_source_of_from_source(class_name, reputation_name, source, src_class_name)
188
+ data[src_class_name].delete(reputation)
189
+ end
190
+ end
191
+
192
+ def derive_class_name_from_attribute(class_name, attribute)
193
+ if attribute && attribute != :self && attribute != "self"
194
+ attribute.to_s.camelize.chomp('s')
195
+ else
196
+ class_name
197
+ end
198
+ end
199
+
200
+ def convert_to_array_if_hash(tar)
201
+ tar = [tar] if tar.is_a? Hash
202
+ tar
203
+ end
204
+
205
+ def assign_self_as_default_value_for_of_attr(tar)
206
+ tar = tar.each { |s| s[:of] = :self unless s[:of] } if tar.is_a? Array
207
+ end
208
+
209
+ def validate_scope_necessity(class_name, reputation_name, scope)
210
+ if scope.nil? && has_scopes?(class_name, reputation_name)
211
+ raise ArgumentError, "Evaluations of #{reputation_name} must have scope specified."
212
+ end
213
+ end
214
+
215
+ def validate_scope_existence(class_name, reputation_name, scope)
216
+ if !scope.nil? && !has_scope?(class_name, reputation_name, scope)
217
+ raise ArgumentError, "#{reputation_name} does not have scope #{scope}"
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,50 @@
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 Normalization
19
+ def normalized_reputation_value_for(reputation_name, *args)
20
+ scope = args.first
21
+ if !self.class.has_reputation_for?(reputation_name)
22
+ raise ArgumentError, "#{reputation_name} is not valid"
23
+ else
24
+ reputation_name = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope)
25
+ process = ReputationSystem::Network.get_reputation_def(self.class.name, reputation_name)[:aggregated_by]
26
+ reputation = RSReputation.find_or_create_reputation(reputation_name, self, process)
27
+ reputation.normalized_value
28
+ end
29
+ end
30
+
31
+ def activate_all_reputations
32
+ RSReputation.find(:all, :conditions => {:target_id => self.id, :target_type => self.class.name, :active => false}).each do |r|
33
+ r.active = true
34
+ r.save!
35
+ end
36
+ end
37
+
38
+ def deactivate_all_reputations
39
+ RSReputation.find(:all, :conditions => {:target_id => self.id, :target_type => self.class.name, :active => true}).each do |r|
40
+ r.active = false
41
+ r.save!
42
+ end
43
+ end
44
+
45
+ def reputations_activated?(reputation_name)
46
+ r = RSReputation.find(:first, :conditions => {:reputation_name => reputation_name.to_s, :target_id => self.id, :target_type => self.class.name})
47
+ r ? r.active : false
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,102 @@
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 Query
19
+ def self.included(klass)
20
+ klass.extend ClassMethods
21
+ end
22
+
23
+ module ClassMethods
24
+ DELTA = 0.000001
25
+
26
+ def find_with_reputation(*args)
27
+ reputation_name, srn, find_scope, options = parse_query_args(*args)
28
+ options[:select] ||= sanitize_sql_array(["%s.*", self.table_name])
29
+ options[:select] = sanitize_sql_array(["%s, COALESCE(rs_reputations.value, 0) AS %s", options[:select], reputation_name])
30
+ find_options = get_find_options(srn, options)
31
+ find_options[:conditions][0].gsub!(reputation_name.to_s, "COALESCE(rs_reputations.value, 0)")
32
+ find(find_scope, find_options)
33
+ end
34
+
35
+ def count_with_reputation(*args)
36
+ reputation_name, srn, find_scope, options = parse_query_args(*args)
37
+ find_options = get_find_options(srn, options)
38
+ find_options[:conditions][0].gsub!(reputation_name.to_s, "COALESCE(rs_reputations.value, 0)")
39
+ count(find_scope, find_options)
40
+ end
41
+
42
+ def find_with_normalized_reputation(*args)
43
+ reputation_name, srn, find_scope, options = parse_query_args(*args)
44
+ max = RSReputation.max(srn, self.name)
45
+ min = RSReputation.min(srn, self.name)
46
+ range = max - min
47
+ options[:select] ||= sanitize_sql_array(["%s.*", self.table_name])
48
+ if range < DELTA
49
+ options[:select] = sanitize_sql_array(["%s, (0) AS normalized_%s", options[:select], reputation_name])
50
+ else
51
+ options[:select] = sanitize_sql_array(["%s, ((rs_reputations.value - %s) / %s) AS normalized_%s", options[:select], min, range, reputation_name])
52
+ end
53
+ find_options = get_find_options(srn, options)
54
+ find(find_scope, options)
55
+ end
56
+
57
+ def find_with_reputation_sql(*args)
58
+ reputation_name, srn, find_scope, options = parse_query_args(*args)
59
+ options[:select] ||= sanitize_sql_array(["%s.*", self.table_name])
60
+ options[:select] = sanitize_sql_array(["%s, COALESCE(rs_reputations.value, 0) AS %s", options[:select], reputation_name])
61
+ find_options = get_find_options(srn, options)
62
+ if respond_to?(:construct_finder_sql, true)
63
+ construct_finder_sql(find_options)
64
+ else
65
+ construct_finder_arel(find_options).to_sql
66
+ end
67
+ end
68
+
69
+ protected
70
+ def get_find_options(srn, options)
71
+ options[:joins] ||= []
72
+ options[:joins] = [options[:joins]] unless options[:joins].is_a? Array
73
+ temp_joins = sanitize_sql_array(["LEFT JOIN rs_reputations ON %s.id = rs_reputations.target_id AND rs_reputations.target_type = ? AND rs_reputations.reputation_name = ? AND rs_reputations.active = ?", self.name, srn.to_s, true])
74
+ temp_joins = sanitize_sql_array([temp_joins, self.table_name])
75
+ options[:joins] << temp_joins
76
+ options[:conditions] ||= [""]
77
+ options[:conditions] = [options[:conditions]] unless options[:conditions].is_a? Array
78
+ options
79
+ end
80
+
81
+ def parse_query_args(*args)
82
+ case args.length
83
+ when 2
84
+ find_scope = args[1]
85
+ options = {}
86
+ when 3
87
+ find_scope = args[1]
88
+ options = args[2]
89
+ when 4
90
+ scope = args[1]
91
+ find_scope = args[2]
92
+ options = args[3]
93
+ else
94
+ raise ArgumentError, "Expecting 2, 3 or 4 arguments but got #{args.length}"
95
+ end
96
+ reputation_name = args[0]
97
+ srn = ReputationSystem::Network.get_scoped_reputation_name(name, reputation_name, scope)
98
+ [reputation_name, srn, find_scope, options]
99
+ end
100
+ end
101
+ end
102
+ end