rating 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +111 -5
- data/lib/generators/rating/install_generator.rb +7 -3
- data/lib/generators/rating/templates/db/migrate/create_rating_tables.rb +10 -6
- data/lib/rating/models/rating/extension.rb +32 -13
- data/lib/rating/models/rating/rate.rb +9 -8
- data/lib/rating/models/rating/rating.rb +34 -17
- data/lib/rating/version.rb +1 -1
- data/spec/factories/article.rb +1 -1
- data/spec/factories/author.rb +7 -0
- data/spec/factories/category.rb +7 -0
- data/spec/factories/user.rb +1 -1
- data/spec/models/extension/after_create_spec.rb +16 -7
- data/spec/models/extension/order_by_rating_spec.rb +75 -0
- data/spec/models/extension/rate_for_spec.rb +15 -3
- data/spec/models/extension/rate_spec.rb +15 -3
- data/spec/models/extension/rated_question_spec.rb +24 -6
- data/spec/models/extension/rated_records_spec.rb +26 -0
- data/spec/models/extension/rated_spec.rb +33 -13
- data/spec/models/extension/rates_records_spec.rb +26 -0
- data/spec/models/extension/rates_spec.rb +33 -17
- data/spec/models/extension/rating_records_spec.rb +28 -0
- data/spec/models/extension/rating_spec.rb +31 -15
- data/spec/models/rate/create_spec.rb +104 -37
- data/spec/models/rate/rate_for_spec.rb +26 -6
- data/spec/models/rate_spec.rb +2 -1
- data/spec/models/rating/averager_data_spec.rb +24 -5
- data/spec/models/rating/data_spec.rb +38 -11
- data/spec/models/rating/update_rating_spec.rb +24 -6
- data/spec/models/rating/values_data_spec.rb +31 -8
- data/spec/models/rating_spec.rb +1 -0
- data/spec/support/db/migrate/create_authors_table.rb +9 -0
- data/spec/support/db/migrate/create_category_spec.rb +9 -0
- data/spec/support/migrate.rb +3 -1
- data/spec/support/models/author.rb +5 -0
- data/spec/support/models/category.rb +4 -0
- metadata +22 -20
- data/spec/support/html_matchers.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7eb5b802982259c2ec3101c38bbfe18302226f2
|
4
|
+
data.tar.gz: cce443ffafbf9887de9d9261fa5e0223ad190b47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a29fc4b2a26a3f8cd40d3d97ad2d1691a3bd5cbd70a5bd436c9f57b5dbbb11470185a602394f6629989dda254f68aa0ff01ce4ca22c59291a1f83f81ad36cb11
|
7
|
+
data.tar.gz: f79ec758ff607f735e9452b45705c52eacc6df4898cc4c8826a90fa6fe7cdf86fa0cde79c67321540aa262328afede7167f471e694b7df6744e944bbc52a41ae
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[![Build Status](https://travis-ci.org/wbotelhos/rating.svg)](https://travis-ci.org/wbotelhos/rating)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/rating.svg)](https://badge.fury.io/rb/rating)
|
5
5
|
|
6
|
-
A true Bayesian rating system with cache enabled.
|
6
|
+
A true Bayesian rating system with scope and cache enabled.
|
7
7
|
|
8
8
|
## JS Rating?
|
9
9
|
|
@@ -33,13 +33,13 @@ Rating uses the know as "True Bayesian Estimate" inspired on [IMDb rating](http:
|
|
33
33
|
|
34
34
|
`WR`: weighted rating
|
35
35
|
|
36
|
-
`R`: average for the resource
|
36
|
+
`R`: average for the resource
|
37
37
|
|
38
|
-
`v`: number of votes for the
|
38
|
+
`v`: number of votes for the resource
|
39
39
|
|
40
40
|
`m`: average of the number of votes
|
41
41
|
|
42
|
-
`C`: the
|
42
|
+
`C`: the average rating based on all resources
|
43
43
|
|
44
44
|
## Install
|
45
45
|
|
@@ -71,7 +71,7 @@ class User < ApplicationRecord
|
|
71
71
|
end
|
72
72
|
```
|
73
73
|
|
74
|
-
Now this model can vote or
|
74
|
+
Now this model can vote or receive votes.
|
75
75
|
|
76
76
|
### rate
|
77
77
|
|
@@ -175,6 +175,112 @@ Article.order_by_rating :average, :asc
|
|
175
175
|
|
176
176
|
It will return a collection of resource ordered by `Rating` table data.
|
177
177
|
|
178
|
+
### Scope
|
179
|
+
|
180
|
+
All methods support scope query, since you may want to vote on items of a resource instead the resource itself.
|
181
|
+
Let's say an article belongs to one or more categories and you want to vote on some categories of this article.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
category_1 = Category.first
|
185
|
+
category_2 = Category.second
|
186
|
+
author = User.last
|
187
|
+
resource = Article.last
|
188
|
+
```
|
189
|
+
|
190
|
+
In this situation you should scope the vote of article with some category:
|
191
|
+
|
192
|
+
**rate**
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
author.rate resource, 3, scopeable: category_1
|
196
|
+
author.rate resource, 5, scopeable: category_2
|
197
|
+
```
|
198
|
+
|
199
|
+
Now `article` has a rating for `category_1` and another one for `category_2`.
|
200
|
+
|
201
|
+
**rating**
|
202
|
+
|
203
|
+
Recovering the rating values for article, we have:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
author.rating
|
207
|
+
# nil
|
208
|
+
```
|
209
|
+
|
210
|
+
But using the scope to make the right query:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
author.rating scope: category_1
|
214
|
+
# { average: 3, estimate: 3, sum: 3, total: 1 }
|
215
|
+
|
216
|
+
author.rating scope: category_2
|
217
|
+
# { average: 5, estimate: 5, sum: 5, total: 1 }
|
218
|
+
```
|
219
|
+
|
220
|
+
**rated**
|
221
|
+
|
222
|
+
On the same way you can find your rates with a scoped query:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
user.rated scope: category_1
|
226
|
+
# { value: 3, scopeable: category_1 }
|
227
|
+
```
|
228
|
+
|
229
|
+
**rates**
|
230
|
+
|
231
|
+
The resource still have the power to consult its rates:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
article.rates scope: category_1
|
235
|
+
# { value: 3, scopeable: category_1 }
|
236
|
+
|
237
|
+
article.rates scope: category_2
|
238
|
+
# { value: 3, scopeable: category_2 }
|
239
|
+
```
|
240
|
+
|
241
|
+
**order_by_rating**
|
242
|
+
|
243
|
+
To order the rating you do the same thing:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
Article.order_by_rating scope: category_1
|
247
|
+
```
|
248
|
+
|
249
|
+
### Records
|
250
|
+
|
251
|
+
Maybe you want to recover all records with or without scope, so you can add the suffix `_records` on relations:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
category_1 = Category.first
|
255
|
+
category_2 = Category.second
|
256
|
+
author = User.last
|
257
|
+
resource = Article.last
|
258
|
+
|
259
|
+
author.rate resource, 1
|
260
|
+
author.rate resource, 3, scopeable: category_1
|
261
|
+
author.rate resource, 5, scopeable: category_2
|
262
|
+
|
263
|
+
author.rating_records
|
264
|
+
# { average: 1, estimate: 1, scopeable: nil , sum: 1, total: 1 },
|
265
|
+
# { average: 3, estimate: 3, scopeable: category_1, sum: 3, total: 1 },
|
266
|
+
# { average: 5, estimate: 5, scopeable: category_2, sum: 5, total: 1 }
|
267
|
+
|
268
|
+
user.rated_records
|
269
|
+
# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }
|
270
|
+
|
271
|
+
article.rates_records
|
272
|
+
# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }
|
273
|
+
```
|
274
|
+
|
275
|
+
### As
|
276
|
+
|
277
|
+
If you have a model that will only be able to rate but not to receive a rate, configure it as `author`.
|
278
|
+
An author model still can be rated, but won't genarate a Rating record with all values as zero to be easier to display.
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
rating as: :author
|
282
|
+
```
|
283
|
+
|
178
284
|
## Love it!
|
179
285
|
|
180
286
|
Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Gratipay](https://gratipay.com/rating). Thanks! (:
|
@@ -4,12 +4,16 @@ module Rating
|
|
4
4
|
class InstallGenerator < Rails::Generators::Base
|
5
5
|
source_root File.expand_path('../templates', __FILE__)
|
6
6
|
|
7
|
-
desc '
|
7
|
+
desc 'creates Rating migration'
|
8
8
|
|
9
9
|
def create_migration
|
10
|
-
|
10
|
+
template 'db/migrate/create_rating_tables.rb', "db/migrate/#{timestamp}_create_rating_tables.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
11
14
|
|
12
|
-
|
15
|
+
def timestamp
|
16
|
+
Time.current.strftime '%Y%m%d%H%M%S'
|
13
17
|
end
|
14
18
|
end
|
15
19
|
end
|
@@ -5,14 +5,15 @@ class CreateRatingTables < ActiveRecord::Migration[5.0]
|
|
5
5
|
create_table :rating_rates do |t|
|
6
6
|
t.decimal :value, default: 0, precision: 17, scale: 14
|
7
7
|
|
8
|
-
t.references :author
|
9
|
-
t.references :resource, index: true, null: false, polymorphic: true
|
8
|
+
t.references :author , index: true, null: false, polymorphic: true
|
9
|
+
t.references :resource , index: true, null: false, polymorphic: true
|
10
|
+
t.references :scopeable, index: true, null: true , polymorphic: true
|
10
11
|
|
11
12
|
t.timestamps null: false
|
12
13
|
end
|
13
14
|
|
14
|
-
add_index :rating_rates, %i[author_id author_type resource_id resource_type],
|
15
|
-
name: :
|
15
|
+
add_index :rating_rates, %i[author_id author_type resource_id resource_type scopeable_id scopeable_type],
|
16
|
+
name: :index_rating_rates_on_author_and_resource_and_scopeable,
|
16
17
|
unique: true
|
17
18
|
|
18
19
|
create_table :rating_ratings do |t|
|
@@ -21,11 +22,14 @@ class CreateRatingTables < ActiveRecord::Migration[5.0]
|
|
21
22
|
t.integer :sum , default: 0, mull: false
|
22
23
|
t.integer :total , default: 0, mull: false
|
23
24
|
|
24
|
-
t.references :resource, index: true, null: false, polymorphic: true
|
25
|
+
t.references :resource , index: true, null: false, polymorphic: true
|
26
|
+
t.references :scopeable, index: true, null: true , polymorphic: true
|
25
27
|
|
26
28
|
t.timestamps null: false
|
27
29
|
end
|
28
30
|
|
29
|
-
add_index :rating_ratings, %i[resource_id resource_type],
|
31
|
+
add_index :rating_ratings, %i[resource_id resource_type scopeable_id scopeable_type],
|
32
|
+
name: :index_rating_rating_on_resource_and_scopeable,
|
33
|
+
unique: true
|
30
34
|
end
|
31
35
|
end
|
@@ -5,40 +5,59 @@ module Rating
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
def rate(resource, value, author: self)
|
9
|
-
Rate.create author: author, resource: resource, value: value
|
8
|
+
def rate(resource, value, author: self, scope: nil)
|
9
|
+
Rate.create author: author, resource: resource, scopeable: scope, value: value
|
10
10
|
end
|
11
11
|
|
12
|
-
def rate_for(resource)
|
13
|
-
Rate.rate_for author: self, resource: resource
|
12
|
+
def rate_for(resource, scope: nil)
|
13
|
+
Rate.rate_for author: self, resource: resource, scopeable: scope
|
14
14
|
end
|
15
15
|
|
16
|
-
def rated?(resource)
|
17
|
-
!rate_for(resource).nil?
|
16
|
+
def rated?(resource, scope: nil)
|
17
|
+
!rate_for(resource, scope: scope).nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def rates(scope: nil)
|
21
|
+
rates_records.where scopeable: scope
|
22
|
+
end
|
23
|
+
|
24
|
+
def rated(scope: nil)
|
25
|
+
rated_records.where scopeable: scope
|
26
|
+
end
|
27
|
+
|
28
|
+
def rating(scope: nil)
|
29
|
+
rating_records.find_by scopeable: scope
|
18
30
|
end
|
19
31
|
end
|
20
32
|
|
21
33
|
module ClassMethods
|
22
|
-
def rating
|
23
|
-
after_create { Rating.find_or_create_by resource: self }
|
34
|
+
def rating(as: nil)
|
35
|
+
after_create -> { Rating.find_or_create_by resource: self }, unless: -> { as == :author }
|
24
36
|
|
25
|
-
|
37
|
+
has_many :rating_records,
|
26
38
|
as: :resource,
|
27
39
|
class_name: '::Rating::Rating',
|
28
40
|
dependent: :destroy
|
29
41
|
|
30
|
-
has_many :
|
42
|
+
has_many :rates_records,
|
31
43
|
as: :resource,
|
32
44
|
class_name: '::Rating::Rate',
|
33
45
|
dependent: :destroy
|
34
46
|
|
35
|
-
has_many :
|
47
|
+
has_many :rated_records,
|
36
48
|
as: :author,
|
37
49
|
class_name: '::Rating::Rate',
|
38
50
|
dependent: :destroy
|
39
51
|
|
40
|
-
scope :order_by_rating, ->(column = :estimate, direction = :desc) {
|
41
|
-
|
52
|
+
scope :order_by_rating, ->(column = :estimate, direction = :desc, scope: nil) {
|
53
|
+
scope_values = {
|
54
|
+
scopeable_id: scope&.id,
|
55
|
+
scopeable_type: scope&.class&.base_class&.name
|
56
|
+
}
|
57
|
+
|
58
|
+
includes(:rating_records)
|
59
|
+
.where(Rating.table_name => scope_values)
|
60
|
+
.order("#{Rating.table_name}.#{column} #{direction}")
|
42
61
|
}
|
43
62
|
end
|
44
63
|
end
|
@@ -6,19 +6,20 @@ module Rating
|
|
6
6
|
|
7
7
|
after_save :update_rating
|
8
8
|
|
9
|
-
belongs_to :author
|
10
|
-
belongs_to :resource, polymorphic: true
|
9
|
+
belongs_to :author , polymorphic: true
|
10
|
+
belongs_to :resource , polymorphic: true
|
11
|
+
belongs_to :scopeable, polymorphic: true
|
11
12
|
|
12
13
|
validates :author, :resource, :value, presence: true
|
13
14
|
validates :value, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 }
|
14
15
|
|
15
16
|
validates :author_id, uniqueness: {
|
16
17
|
case_sensitive: false,
|
17
|
-
scope: %i[author_type resource_id resource_type]
|
18
|
+
scope: %i[author_type resource_id resource_type scopeable_id scopeable_type]
|
18
19
|
}
|
19
20
|
|
20
|
-
def self.create(author:, resource:, value:)
|
21
|
-
record = find_or_initialize_by(author: author, resource: resource)
|
21
|
+
def self.create(author:, resource:, scopeable: nil, value:)
|
22
|
+
record = find_or_initialize_by(author: author, resource: resource, scopeable: scopeable)
|
22
23
|
|
23
24
|
return record if record.persisted? && value == record.value
|
24
25
|
|
@@ -28,14 +29,14 @@ module Rating
|
|
28
29
|
record
|
29
30
|
end
|
30
31
|
|
31
|
-
def self.rate_for(author:, resource:)
|
32
|
-
find_by author: author, resource: resource
|
32
|
+
def self.rate_for(author:, resource:, scopeable: nil)
|
33
|
+
find_by author: author, resource: resource, scopeable: scopeable
|
33
34
|
end
|
34
35
|
|
35
36
|
private
|
36
37
|
|
37
38
|
def update_rating
|
38
|
-
::Rating::Rating.update_rating resource
|
39
|
+
::Rating::Rating.update_rating resource, scopeable
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
@@ -4,30 +4,34 @@ module Rating
|
|
4
4
|
class Rating < ActiveRecord::Base
|
5
5
|
self.table_name = 'rating_ratings'
|
6
6
|
|
7
|
-
belongs_to :resource, polymorphic: true
|
7
|
+
belongs_to :resource , polymorphic: true
|
8
|
+
belongs_to :scopeable, polymorphic: true
|
8
9
|
|
9
10
|
validates :average, :estimate, :resource, :sum, :total, presence: true
|
10
11
|
validates :average, :estimate, :sum, :total, numericality: true
|
11
12
|
|
12
13
|
class << self
|
13
|
-
def averager_data(resource)
|
14
|
-
total_count = how_many_resource_received_votes_sql?
|
15
|
-
distinct_count = how_many_resource_received_votes_sql?(distinct: true)
|
14
|
+
def averager_data(resource, scopeable)
|
15
|
+
total_count = how_many_resource_received_votes_sql?(distinct: false, scopeable: scopeable)
|
16
|
+
distinct_count = how_many_resource_received_votes_sql?(distinct: true , scopeable: scopeable)
|
17
|
+
values = { resource_type: resource.class.base_class.name }
|
18
|
+
|
19
|
+
values[:scopeable_type] = scopeable.class.base_class.name unless scopeable.nil?
|
16
20
|
|
17
21
|
sql = %(
|
18
22
|
SELECT
|
19
23
|
(#{total_count} / CAST(#{distinct_count} AS float)) count_avg,
|
20
24
|
COALESCE(AVG(value), 0) rating_avg
|
21
25
|
FROM #{rate_table_name}
|
22
|
-
WHERE resource_type = :resource_type
|
26
|
+
WHERE resource_type = :resource_type and #{scope_type_query(scopeable)}
|
23
27
|
).squish
|
24
28
|
|
25
|
-
execute_sql [sql,
|
29
|
+
execute_sql [sql, values]
|
26
30
|
end
|
27
31
|
|
28
|
-
def data(resource)
|
29
|
-
averager = averager_data(resource)
|
30
|
-
values = values_data(resource)
|
32
|
+
def data(resource, scopeable)
|
33
|
+
averager = averager_data(resource, scopeable)
|
34
|
+
values = values_data(resource, scopeable)
|
31
35
|
|
32
36
|
{
|
33
37
|
average: values.rating_avg,
|
@@ -37,22 +41,31 @@ module Rating
|
|
37
41
|
}
|
38
42
|
end
|
39
43
|
|
40
|
-
def values_data(resource)
|
44
|
+
def values_data(resource, scopeable)
|
45
|
+
scope_query = if scopeable.nil?
|
46
|
+
'scopeable_type is NULL and scopeable_id is NULL'
|
47
|
+
else
|
48
|
+
'scopeable_type = ? and scopeable_id = ?'
|
49
|
+
end
|
50
|
+
|
41
51
|
sql = %(
|
42
52
|
SELECT
|
43
53
|
COALESCE(AVG(value), 0) rating_avg,
|
44
54
|
COALESCE(SUM(value), 0) rating_sum,
|
45
55
|
COUNT(1) rating_count
|
46
56
|
FROM #{rate_table_name}
|
47
|
-
WHERE resource_type = ? and resource_id = ?
|
57
|
+
WHERE resource_type = ? and resource_id = ? and #{scope_query}
|
48
58
|
).squish
|
49
59
|
|
50
|
-
|
60
|
+
values = [sql, resource.class.base_class.name, resource.id]
|
61
|
+
values += [scopeable.class.base_class.name, scopeable.id] unless scopeable.nil?
|
62
|
+
|
63
|
+
execute_sql values
|
51
64
|
end
|
52
65
|
|
53
|
-
def update_rating(resource)
|
54
|
-
record = find_or_initialize_by(resource: resource)
|
55
|
-
result = data(resource)
|
66
|
+
def update_rating(resource, scopeable)
|
67
|
+
record = find_or_initialize_by(resource: resource, scopeable: scopeable)
|
68
|
+
result = data(resource, scopeable)
|
56
69
|
|
57
70
|
record.average = result[:average]
|
58
71
|
record.sum = result[:sum]
|
@@ -78,19 +91,23 @@ module Rating
|
|
78
91
|
Rate.find_by_sql(sql).first
|
79
92
|
end
|
80
93
|
|
81
|
-
def how_many_resource_received_votes_sql?(distinct:
|
94
|
+
def how_many_resource_received_votes_sql?(distinct:, scopeable:)
|
82
95
|
count = distinct ? 'COUNT(DISTINCT resource_id)' : 'COUNT(1)'
|
83
96
|
|
84
97
|
%((
|
85
98
|
SELECT #{count}
|
86
99
|
FROM #{rate_table_name}
|
87
|
-
WHERE resource_type = :resource_type
|
100
|
+
WHERE resource_type = :resource_type and #{scope_type_query(scopeable)}
|
88
101
|
))
|
89
102
|
end
|
90
103
|
|
91
104
|
def rate_table_name
|
92
105
|
@rate_table_name ||= Rate.table_name
|
93
106
|
end
|
107
|
+
|
108
|
+
def scope_type_query(scopeable)
|
109
|
+
scopeable.nil? ? 'scopeable_type is NULL' : 'scopeable_type = :scopeable_type'
|
110
|
+
end
|
94
111
|
end
|
95
112
|
end
|
96
113
|
end
|