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