rating 0.1.0 → 0.2.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.
- 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
|
[](https://travis-ci.org/wbotelhos/rating)
|
4
4
|
[](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
|