activerecord-reputation-system 1.3.4 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +28 -20
- data/lib/reputation_system.rb +3 -1
- data/lib/reputation_system/base.rb +3 -1
- data/lib/reputation_system/finder_methods.rb +84 -0
- data/lib/reputation_system/query_builder.rb +73 -0
- data/lib/reputation_system/query_methods.rb +61 -0
- data/lib/reputation_system/version.rb +1 -1
- data/spec/reputation_system/{query_spec.rb → finder_methods_spec.rb} +0 -0
- data/spec/reputation_system/query_methods_spec.rb +238 -0
- metadata +33 -16
- data/lib/reputation_system/query.rb +0 -102
- data/spec/reputation_system/network_spec.rb +0 -27
data/README.md
CHANGED
@@ -1,13 +1,7 @@
|
|
1
|
-
##
|
1
|
+
## ActiveRecord Reputation System [![Build Status](https://secure.travis-ci.org/twitter/activerecord-reputation-system.png)](http://travis-ci.org/twitter/activerecord-reputation-system) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/twitter/activerecord-reputation-system)
|
2
2
|
|
3
3
|
The Active Record Reputation System helps you discover more about your application and make better decisions. The Reputation System gem makes it easy to integrate reputation systems into Rails applications, decouple the system from the main application and provide guidelines for good design of reputation systems.
|
4
4
|
|
5
|
-
## Concept
|
6
|
-
|
7
|
-
In this gem, the reputation system is described as a network of reputations where updates are triggered by evaluations and reputation values are computed and propagated by the network. In this network, reputations with values directly computed from evaluations are called primary reputations and reputations with values indirectly computed from evaluations are called non-primary reputations. The following is an abstract view of a possible Reputation System:
|
8
|
-
|
9
|
-
![Alt text](./activerecord-reputation-system/raw/master/abs_rs.png "Abstract view of Reputation System")
|
10
|
-
|
11
5
|
## Installation
|
12
6
|
|
13
7
|
Add to Gemfile:
|
@@ -99,19 +93,19 @@ has_reputation :name,
|
|
99
93
|
:source_of => [{:reputation => name, :of => attribute}, ...],
|
100
94
|
:init_value => initial_value
|
101
95
|
```
|
102
|
-
*
|
103
|
-
*
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
*
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
*
|
112
|
-
|
113
|
-
|
114
|
-
*
|
96
|
+
* `:name` is the name of the reputation.
|
97
|
+
* `:source` is a source of the reputation. If it is primary reputation, it takes a class name as input. If it's a non-primary reputation, it takes one or more source definitions, which consist of:
|
98
|
+
* `:reputation` - name of reputation to be used as a source.
|
99
|
+
* `:of` - attribute name (It also accepts a proc as an argument) of the ActiveRecord model which has the source reputation. (default: :self)
|
100
|
+
* `:weight` (optional) - weight value to be used for aggregation (default: 1).
|
101
|
+
* `:aggregated_by` is a mathematical process to be used to aggregate reputation or evaluation values. The following processes are available (each value is weighted by a predefined weight):
|
102
|
+
* average - averages all values received.
|
103
|
+
* sum - sums up all the values received.
|
104
|
+
* product - multiplies all the values received.
|
105
|
+
* `:source_of` (optional) - just like active record association, you don't need to define this if a name can be derived from class name; otherwise if the reputation is used as a part of a source belonging to other reputations, you must define. It takes one or more source definitions, which consists of:
|
106
|
+
* `:reputation` - name of the reputation to be used as a source.
|
107
|
+
* `:of` - attribute name (It also accepts a proc as an argument) of the ActiveRecord model which has the source reputation. (default: :self)
|
108
|
+
* `:init_value` (optional) - initial reputation value assigned to new reputation. It is 0 for average and sum process and 1 for product by default.
|
115
109
|
|
116
110
|
## Evaluation
|
117
111
|
```ruby
|
@@ -161,6 +155,20 @@ reputations_activated?(reputation_name)
|
|
161
155
|
```
|
162
156
|
|
163
157
|
## Querying with Reputation
|
158
|
+
|
159
|
+
``` ruby
|
160
|
+
# Includes the specified reputation value for the given name.
|
161
|
+
ActiveRecord::Base.with_reputation(reputation_name, scope)
|
162
|
+
# For example:
|
163
|
+
User.with_reputation(:karma).where("karma > ?", 3).order("karma")
|
164
|
+
|
165
|
+
# Includes the specified normalized reputation value for the given name.
|
166
|
+
ActiveRecord::Base.with_normalized_reputation(reputation, scope)
|
167
|
+
# For example:
|
168
|
+
User.with_normalized_reputation(:karma).where("karma > ?" > 0.5).order("karma")
|
169
|
+
```
|
170
|
+
Note: Above query methods does not support calcualtion methods such as count, sum and etc yet.
|
171
|
+
|
164
172
|
```ruby
|
165
173
|
# Includes the specified reputation value for the given name via a normal Active Record find query.
|
166
174
|
ActiveRecord::Base.find_with_reputation(reputation_name, find_scope, options)
|
data/lib/reputation_system.rb
CHANGED
@@ -15,7 +15,9 @@
|
|
15
15
|
##
|
16
16
|
|
17
17
|
require 'reputation_system/base'
|
18
|
-
require 'reputation_system/
|
18
|
+
require 'reputation_system/query_methods'
|
19
|
+
require 'reputation_system/finder_methods'
|
20
|
+
require 'reputation_system/query_builder'
|
19
21
|
require 'reputation_system/evaluation'
|
20
22
|
require 'reputation_system/network'
|
21
23
|
require 'reputation_system/reputation'
|
@@ -50,7 +50,9 @@ module ReputationSystem
|
|
50
50
|
# If it is first time to be called
|
51
51
|
unless ancestors.include?(ReputationSystem::Reputation)
|
52
52
|
has_many :reputations, :as => :target, :class_name => "RSReputation", :dependent => :destroy
|
53
|
-
include ReputationSystem::
|
53
|
+
include ReputationSystem::QueryBuilder
|
54
|
+
include ReputationSystem::QueryMethods
|
55
|
+
include ReputationSystem::FinderMethods
|
54
56
|
include ReputationSystem::Reputation
|
55
57
|
include ReputationSystem::Scope
|
56
58
|
end
|
@@ -0,0 +1,84 @@
|
|
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 FinderMethods
|
19
|
+
def self.included(klass)
|
20
|
+
klass.extend ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def find_with_reputation(*args)
|
26
|
+
reputation_name, srn, find_scope, options = parse_query_args(*args)
|
27
|
+
options[:select] = build_select_statement(table_name, reputation_name, options[:select])
|
28
|
+
options[:joins] = build_join_statement(table_name, name, srn, options[:joins])
|
29
|
+
options[:conditions] = build_condition_statement(options[:conditions])
|
30
|
+
find(find_scope, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def count_with_reputation(*args)
|
34
|
+
reputation_name, srn, find_scope, options = parse_query_args(*args)
|
35
|
+
options[:joins] = build_join_statement(table_name, name, srn, options[:joins])
|
36
|
+
options[:conditions] = build_condition_statement(options[:conditions])
|
37
|
+
options[:conditions][0].gsub!(reputation_name.to_s, "COALESCE(rs_reputations.value, 0)")
|
38
|
+
count(find_scope, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_with_normalized_reputation(*args)
|
42
|
+
reputation_name, srn, find_scope, options = parse_query_args(*args)
|
43
|
+
options[:select] = build_select_statement(table_name, reputation_name, options[:select], srn, true)
|
44
|
+
options[:joins] = build_join_statement(table_name, name, srn, options[:joins])
|
45
|
+
options[:conditions] = build_condition_statement(options[:conditions])
|
46
|
+
find(find_scope, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_with_reputation_sql(*args)
|
50
|
+
reputation_name, srn, find_scope, options = parse_query_args(*args)
|
51
|
+
options[:select] = build_select_statement(table_name, reputation_name, options[:select])
|
52
|
+
options[:joins] = build_join_statement(table_name, name, srn, options[:joins])
|
53
|
+
options[:conditions] = build_condition_statement(options[:conditions])
|
54
|
+
if respond_to?(:construct_finder_sql, true)
|
55
|
+
construct_finder_sql(options)
|
56
|
+
else
|
57
|
+
construct_finder_arel(options).to_sql
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def parse_query_args(*args)
|
64
|
+
case args.length
|
65
|
+
when 2
|
66
|
+
find_scope = args[1]
|
67
|
+
options = {}
|
68
|
+
when 3
|
69
|
+
find_scope = args[1]
|
70
|
+
options = args[2]
|
71
|
+
when 4
|
72
|
+
scope = args[1]
|
73
|
+
find_scope = args[2]
|
74
|
+
options = args[3]
|
75
|
+
else
|
76
|
+
raise ArgumentError, "Expecting 2, 3 or 4 arguments but got #{args.length}"
|
77
|
+
end
|
78
|
+
reputation_name = args[0]
|
79
|
+
srn = ReputationSystem::Network.get_scoped_reputation_name(name, reputation_name, scope)
|
80
|
+
[reputation_name, srn, find_scope, options]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,73 @@
|
|
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 QueryBuilder
|
19
|
+
def self.included(klass)
|
20
|
+
klass.extend ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
DELTA = 0.000001
|
25
|
+
REPUTATION_JOIN_STATEMENT = "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 = ?"
|
26
|
+
|
27
|
+
def build_select_statement(table_name, reputation_name, select=nil, srn=nil, normalize=false)
|
28
|
+
select = sanitize_sql_array(["%s.*", table_name]) unless select
|
29
|
+
if normalize
|
30
|
+
max = RSReputation.max(srn, self.name)
|
31
|
+
min = RSReputation.min(srn, self.name)
|
32
|
+
range = max - min
|
33
|
+
if range < DELTA
|
34
|
+
sanitize_sql_array(["%s, (0) AS normalized_%s", select, reputation_name])
|
35
|
+
else
|
36
|
+
sanitize_sql_array(["%s, ((rs_reputations.value - %s) / %s) AS normalized_%s", select, min, range, reputation_name])
|
37
|
+
end
|
38
|
+
else
|
39
|
+
sanitize_sql_array(["%s, COALESCE(rs_reputations.value, 0) AS %s", select, reputation_name])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_select_statement_with_reputation_only(table_name, reputation_name, srn=nil, normalize=false)
|
44
|
+
if normalize
|
45
|
+
max = RSReputation.max(srn, self.name)
|
46
|
+
min = RSReputation.min(srn, self.name)
|
47
|
+
range = max - min
|
48
|
+
if range < DELTA
|
49
|
+
sanitize_sql_array(["(0) AS normalized_%s", reputation_name])
|
50
|
+
else
|
51
|
+
sanitize_sql_array(["((rs_reputations.value - %s) / %s) AS normalized_%s", min, range, reputation_name])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
sanitize_sql_array(["COALESCE(rs_reputations.value, 0) AS %s", reputation_name])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_condition_statement(conditions=nil)
|
59
|
+
conditions ||= [""]
|
60
|
+
conditions = [conditions] unless conditions.is_a? Array
|
61
|
+
conditions
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_join_statement(table_name, class_name, srn, joins=nil)
|
65
|
+
joins ||= []
|
66
|
+
joins = [joins] unless joins.is_a? Array
|
67
|
+
rep_join = sanitize_sql_array([REPUTATION_JOIN_STATEMENT, class_name.to_s, srn.to_s, true])
|
68
|
+
rep_join = sanitize_sql_array([rep_join, table_name])
|
69
|
+
joins << rep_join
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,61 @@
|
|
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 QueryMethods
|
19
|
+
def self.included(klass)
|
20
|
+
klass.extend ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def with_reputation(*args)
|
25
|
+
reputation_name, srn = parse_arel_query_args(args)
|
26
|
+
select = build_select_statement(table_name, reputation_name)
|
27
|
+
joins = build_join_statement(table_name, name, srn)
|
28
|
+
self.select(select).joins(joins)
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_reputation_only(*args)
|
32
|
+
reputation_name, srn = parse_arel_query_args(args)
|
33
|
+
select = build_select_statement_with_reputation_only(table_name, reputation_name)
|
34
|
+
joins = build_join_statement(table_name, name, srn)
|
35
|
+
self.select(select).joins(joins)
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_normalized_reputation(*args)
|
39
|
+
reputation_name, srn = parse_arel_query_args(args)
|
40
|
+
select = build_select_statement(table_name, reputation_name, nil, srn, true)
|
41
|
+
joins = build_join_statement(table_name, name, srn)
|
42
|
+
self.select(select).joins(joins)
|
43
|
+
end
|
44
|
+
|
45
|
+
def with_normalized_reputation_only(*args)
|
46
|
+
reputation_name, srn = parse_arel_query_args(args)
|
47
|
+
select = build_select_statement_with_reputation_only(table_name, reputation_name, srn, true)
|
48
|
+
joins = build_join_statement(table_name, name, srn)
|
49
|
+
self.select(select).joins(joins)
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def parse_arel_query_args(args)
|
55
|
+
reputation_name = args[0]
|
56
|
+
srn = ReputationSystem::Network.get_scoped_reputation_name(name, reputation_name, args[1])
|
57
|
+
[reputation_name, srn]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
File without changes
|
@@ -0,0 +1,238 @@
|
|
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 ActiveRecord::Base do
|
20
|
+
|
21
|
+
before(:each) do
|
22
|
+
@user = User.create!(:name => 'jack')
|
23
|
+
@question = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
24
|
+
@answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id)
|
25
|
+
@phrase = Phrase.create!(:text => "One")
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#with_reputation" do
|
29
|
+
context "Without Scopes" do
|
30
|
+
before :each do
|
31
|
+
@question.add_evaluation(:total_votes, 3, @user)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should return result with given reputation" do
|
35
|
+
res = Question.with_reputation(:total_votes)
|
36
|
+
res.should == [@question]
|
37
|
+
res[0].total_votes.should_not be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should retain conditions option" do
|
41
|
+
@question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
42
|
+
@question2.add_evaluation(:total_votes, 5, @user)
|
43
|
+
res = Question.with_reputation(:total_votes).where("total_votes > 4")
|
44
|
+
res.should == [@question2]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should retain joins option" do
|
48
|
+
res = Question.with_reputation(:total_votes).
|
49
|
+
select("questions.*, users.name AS user_name").
|
50
|
+
joins("JOIN users ON questions.author_id = users.id")
|
51
|
+
res.should == [@question]
|
52
|
+
res[0].user_name.should == @user.name
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not retain select option" do
|
56
|
+
res = Question.with_reputation(:total_votes).select("questions.id")
|
57
|
+
res.should == [@question]
|
58
|
+
res[0].id.should_not be_nil
|
59
|
+
lambda {res[0].text}.should_not raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "With Scopes" do
|
64
|
+
before :each do
|
65
|
+
@trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase)
|
66
|
+
@trans_ja.add_evaluation(:votes, 3, @user)
|
67
|
+
@trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase)
|
68
|
+
@trans_fr.add_evaluation(:votes, 6, @user)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should return result with given reputation" do
|
72
|
+
res = Phrase.with_reputation(:maturity, :ja)
|
73
|
+
res.should == [@phrase]
|
74
|
+
res[0].maturity.should == 3
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#with_reputation_only" do
|
80
|
+
context "Without Scopes" do
|
81
|
+
before :each do
|
82
|
+
@question.add_evaluation(:total_votes, 3, @user)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return result with given reputation" do
|
86
|
+
res = Question.with_reputation_only(:total_votes)
|
87
|
+
res.length.should == 1
|
88
|
+
res[0].total_votes.should_not be_nil
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should retain conditions option" do
|
92
|
+
@question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
93
|
+
@question2.add_evaluation(:total_votes, 5, @user)
|
94
|
+
res = Question.with_reputation_only(:total_votes).where("total_votes > 4")
|
95
|
+
res.length.should == 1
|
96
|
+
res[0].total_votes.should > 4
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should retain joins option" do
|
100
|
+
res = Question.with_reputation_only(:total_votes).
|
101
|
+
select("questions.*, users.name AS user_name").
|
102
|
+
joins("JOIN users ON questions.author_id = users.id")
|
103
|
+
res[0].user_name.should == @user.name
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should retain select option" do
|
107
|
+
res = Question.with_reputation_only(:total_votes).select("questions.id")
|
108
|
+
res.should == [@question]
|
109
|
+
res[0].id.should_not be_nil
|
110
|
+
lambda {res[0].text}.should raise_error
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "With Scopes" do
|
115
|
+
before :each do
|
116
|
+
@trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase)
|
117
|
+
@trans_ja.add_evaluation(:votes, 3, @user)
|
118
|
+
@trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase)
|
119
|
+
@trans_fr.add_evaluation(:votes, 6, @user)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should return result with given reputation" do
|
123
|
+
res = Phrase.with_reputation_only(:maturity, :ja)
|
124
|
+
res.length.should == 1
|
125
|
+
res[0].maturity.should == 3
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "#with_normalized_reputation" do
|
131
|
+
context "Without Scopes" do
|
132
|
+
before :each do
|
133
|
+
@question.add_evaluation(:total_votes, 3, @user)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should return result with given normalized reputation" do
|
137
|
+
@question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
138
|
+
@question2.add_evaluation(:total_votes, 6, @user)
|
139
|
+
res = Question.with_normalized_reputation(:total_votes)
|
140
|
+
res.should == [@question, @question2]
|
141
|
+
res[0].normalized_total_votes.should be_within(DELTA).of(0)
|
142
|
+
res[1].normalized_total_votes.should be_within(DELTA).of(1)
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should not retain select option" do
|
146
|
+
res = Question.with_normalized_reputation(:total_votes).select("questions.id")
|
147
|
+
res.should == [@question]
|
148
|
+
res[0].id.should_not be_nil
|
149
|
+
lambda {res[0].text}.should_not raise_error
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should retain conditions option" do
|
153
|
+
@question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
154
|
+
@question2.add_evaluation(:total_votes, 6, @user)
|
155
|
+
res = Question.with_normalized_reputation(:total_votes).where("normalized_total_votes > 0.6")
|
156
|
+
res.should == [@question2]
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should retain joins option" do
|
160
|
+
res = Question.with_normalized_reputation(:total_votes).
|
161
|
+
select("questions.*, users.name AS user_name").
|
162
|
+
joins("JOIN users ON questions.author_id = users.id")
|
163
|
+
res.should == [@question]
|
164
|
+
res[0].user_name.should == @user.name
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context "With Scopes" do
|
169
|
+
before :each do
|
170
|
+
@trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase)
|
171
|
+
@trans_ja.add_evaluation(:votes, 3, @user)
|
172
|
+
@trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase)
|
173
|
+
@trans_fr.add_evaluation(:votes, 6, @user)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should return result with given reputation" do
|
177
|
+
res = Phrase.with_normalized_reputation(:maturity, :ja)
|
178
|
+
res.should == [@phrase]
|
179
|
+
res[0].normalized_maturity.should be_within(DELTA).of(0)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe "#with_normalized_reputation_only" do
|
185
|
+
context "Without Scopes" do
|
186
|
+
before :each do
|
187
|
+
@question.add_evaluation(:total_votes, 3, @user)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should return result with given normalized reputation" do
|
191
|
+
@question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
192
|
+
@question2.add_evaluation(:total_votes, 6, @user)
|
193
|
+
res = Question.with_normalized_reputation_only(:total_votes)
|
194
|
+
res.length.should == 2
|
195
|
+
res[0].normalized_total_votes.should be_within(DELTA).of(0)
|
196
|
+
res[1].normalized_total_votes.should be_within(DELTA).of(1)
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should not retain select option" do
|
200
|
+
res = Question.with_normalized_reputation_only(:total_votes).select("questions.id")
|
201
|
+
res.length.should == 1
|
202
|
+
res[0].id.should_not be_nil
|
203
|
+
lambda {res[0].text}.should raise_error
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should retain conditions option" do
|
207
|
+
@question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
208
|
+
@question2.add_evaluation(:total_votes, 6, @user)
|
209
|
+
res = Question.with_normalized_reputation_only(:total_votes).where("normalized_total_votes > 0.6")
|
210
|
+
res.length.should == 1
|
211
|
+
res[0].normalized_total_votes.should > 0.6
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should retain joins option" do
|
215
|
+
res = Question.with_normalized_reputation_only(:total_votes).
|
216
|
+
select("questions.*, users.name AS user_name").
|
217
|
+
joins("JOIN users ON questions.author_id = users.id")
|
218
|
+
res.length.should == 1
|
219
|
+
res[0].user_name.should == @user.name
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
context "With Scopes" do
|
224
|
+
before :each do
|
225
|
+
@trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase)
|
226
|
+
@trans_ja.add_evaluation(:votes, 3, @user)
|
227
|
+
@trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase)
|
228
|
+
@trans_fr.add_evaluation(:votes, 6, @user)
|
229
|
+
end
|
230
|
+
|
231
|
+
it "should return result with given reputation" do
|
232
|
+
res = Phrase.with_normalized_reputation_only(:maturity, :ja)
|
233
|
+
res.length.should == 1
|
234
|
+
res[0].normalized_maturity.should be_within(DELTA).of(0)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-reputation-system
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 7
|
5
|
+
prerelease:
|
5
6
|
segments:
|
6
7
|
- 1
|
7
|
-
- 3
|
8
8
|
- 4
|
9
|
-
|
9
|
+
- 0
|
10
|
+
version: 1.4.0
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Katsuya Noguchi
|
@@ -14,87 +15,98 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2012-
|
18
|
-
default_executable:
|
18
|
+
date: 2012-09-10 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
|
-
name: activerecord
|
22
21
|
prerelease: false
|
23
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
+
hash: 3
|
27
28
|
segments:
|
28
29
|
- 0
|
29
30
|
version: "0"
|
30
31
|
type: :development
|
32
|
+
name: activerecord
|
31
33
|
version_requirements: *id001
|
32
34
|
- !ruby/object:Gem::Dependency
|
33
|
-
name: rake
|
34
35
|
prerelease: false
|
35
36
|
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
36
38
|
requirements:
|
37
39
|
- - ">="
|
38
40
|
- !ruby/object:Gem::Version
|
41
|
+
hash: 49
|
39
42
|
segments:
|
40
43
|
- 0
|
41
44
|
- 8
|
42
45
|
- 7
|
43
46
|
version: 0.8.7
|
44
47
|
type: :development
|
48
|
+
name: rake
|
45
49
|
version_requirements: *id002
|
46
50
|
- !ruby/object:Gem::Dependency
|
47
|
-
name: rspec
|
48
51
|
prerelease: false
|
49
52
|
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
50
54
|
requirements:
|
51
55
|
- - ~>
|
52
56
|
- !ruby/object:Gem::Version
|
57
|
+
hash: 19
|
53
58
|
segments:
|
54
59
|
- 2
|
55
60
|
- 8
|
56
61
|
version: "2.8"
|
57
62
|
type: :development
|
63
|
+
name: rspec
|
58
64
|
version_requirements: *id003
|
59
65
|
- !ruby/object:Gem::Dependency
|
60
|
-
name: rdoc
|
61
66
|
prerelease: false
|
62
67
|
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
63
69
|
requirements:
|
64
70
|
- - ">="
|
65
71
|
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
66
73
|
segments:
|
67
74
|
- 0
|
68
75
|
version: "0"
|
69
76
|
type: :development
|
77
|
+
name: rdoc
|
70
78
|
version_requirements: *id004
|
71
79
|
- !ruby/object:Gem::Dependency
|
72
|
-
name: database_cleaner
|
73
80
|
prerelease: false
|
74
81
|
requirement: &id005 !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
75
83
|
requirements:
|
76
84
|
- - ~>
|
77
85
|
- !ruby/object:Gem::Version
|
86
|
+
hash: 1
|
78
87
|
segments:
|
79
88
|
- 0
|
80
89
|
- 7
|
81
90
|
- 1
|
82
91
|
version: 0.7.1
|
83
92
|
type: :development
|
93
|
+
name: database_cleaner
|
84
94
|
version_requirements: *id005
|
85
95
|
- !ruby/object:Gem::Dependency
|
86
|
-
name: sqlite3
|
87
96
|
prerelease: false
|
88
97
|
requirement: &id006 !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
89
99
|
requirements:
|
90
100
|
- - ~>
|
91
101
|
- !ruby/object:Gem::Version
|
102
|
+
hash: 17
|
92
103
|
segments:
|
93
104
|
- 1
|
94
105
|
- 3
|
95
106
|
- 5
|
96
107
|
version: 1.3.5
|
97
108
|
type: :development
|
109
|
+
name: sqlite3
|
98
110
|
version_requirements: *id006
|
99
111
|
description: ActiveRecord Reputation System gem allows rails apps to compute and publish reputation scores for active record models.
|
100
112
|
email:
|
@@ -122,8 +134,10 @@ files:
|
|
122
134
|
- lib/models/rs_reputation_message.rb
|
123
135
|
- lib/reputation_system/base.rb
|
124
136
|
- lib/reputation_system/evaluation.rb
|
137
|
+
- lib/reputation_system/finder_methods.rb
|
125
138
|
- lib/reputation_system/network.rb
|
126
|
-
- lib/reputation_system/
|
139
|
+
- lib/reputation_system/query_builder.rb
|
140
|
+
- lib/reputation_system/query_methods.rb
|
127
141
|
- lib/reputation_system/reputation.rb
|
128
142
|
- lib/reputation_system/scope.rb
|
129
143
|
- lib/reputation_system/version.rb
|
@@ -133,12 +147,11 @@ files:
|
|
133
147
|
- spec/models/rs_reputation_spec.rb
|
134
148
|
- spec/reputation_system/base_spec.rb
|
135
149
|
- spec/reputation_system/evaluation_spec.rb
|
136
|
-
- spec/reputation_system/
|
137
|
-
- spec/reputation_system/
|
150
|
+
- spec/reputation_system/finder_methods_spec.rb
|
151
|
+
- spec/reputation_system/query_methods_spec.rb
|
138
152
|
- spec/reputation_system/reputation_spec.rb
|
139
153
|
- spec/reputation_system/scope_spec.rb
|
140
154
|
- spec/spec_helper.rb
|
141
|
-
has_rdoc: true
|
142
155
|
homepage: https://github.com/twitter/activerecord-reputation-system
|
143
156
|
licenses: []
|
144
157
|
|
@@ -148,23 +161,27 @@ rdoc_options: []
|
|
148
161
|
require_paths:
|
149
162
|
- lib
|
150
163
|
required_ruby_version: !ruby/object:Gem::Requirement
|
164
|
+
none: false
|
151
165
|
requirements:
|
152
166
|
- - ">="
|
153
167
|
- !ruby/object:Gem::Version
|
168
|
+
hash: 3
|
154
169
|
segments:
|
155
170
|
- 0
|
156
171
|
version: "0"
|
157
172
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
|
+
none: false
|
158
174
|
requirements:
|
159
175
|
- - ">="
|
160
176
|
- !ruby/object:Gem::Version
|
177
|
+
hash: 3
|
161
178
|
segments:
|
162
179
|
- 0
|
163
180
|
version: "0"
|
164
181
|
requirements: []
|
165
182
|
|
166
183
|
rubyforge_project:
|
167
|
-
rubygems_version: 1.
|
184
|
+
rubygems_version: 1.8.24
|
168
185
|
signing_key:
|
169
186
|
specification_version: 3
|
170
187
|
summary: ActiveRecord Reputation System gem allows rails apps to compute and publish reputation scores for active record models
|
@@ -1,102 +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
|
-
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
|
@@ -1,27 +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 ActiveRecord::Base do
|
20
|
-
|
21
|
-
before(:each) do
|
22
|
-
@user = User.create!(:name => 'jack')
|
23
|
-
@question = Question.create!(:text => 'Does this work?', :author_id => @user.id)
|
24
|
-
@answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id)
|
25
|
-
@phrase = Phrase.create!(:text => "One")
|
26
|
-
end
|
27
|
-
end
|