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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +111 -5
  4. data/lib/generators/rating/install_generator.rb +7 -3
  5. data/lib/generators/rating/templates/db/migrate/create_rating_tables.rb +10 -6
  6. data/lib/rating/models/rating/extension.rb +32 -13
  7. data/lib/rating/models/rating/rate.rb +9 -8
  8. data/lib/rating/models/rating/rating.rb +34 -17
  9. data/lib/rating/version.rb +1 -1
  10. data/spec/factories/article.rb +1 -1
  11. data/spec/factories/author.rb +7 -0
  12. data/spec/factories/category.rb +7 -0
  13. data/spec/factories/user.rb +1 -1
  14. data/spec/models/extension/after_create_spec.rb +16 -7
  15. data/spec/models/extension/order_by_rating_spec.rb +75 -0
  16. data/spec/models/extension/rate_for_spec.rb +15 -3
  17. data/spec/models/extension/rate_spec.rb +15 -3
  18. data/spec/models/extension/rated_question_spec.rb +24 -6
  19. data/spec/models/extension/rated_records_spec.rb +26 -0
  20. data/spec/models/extension/rated_spec.rb +33 -13
  21. data/spec/models/extension/rates_records_spec.rb +26 -0
  22. data/spec/models/extension/rates_spec.rb +33 -17
  23. data/spec/models/extension/rating_records_spec.rb +28 -0
  24. data/spec/models/extension/rating_spec.rb +31 -15
  25. data/spec/models/rate/create_spec.rb +104 -37
  26. data/spec/models/rate/rate_for_spec.rb +26 -6
  27. data/spec/models/rate_spec.rb +2 -1
  28. data/spec/models/rating/averager_data_spec.rb +24 -5
  29. data/spec/models/rating/data_spec.rb +38 -11
  30. data/spec/models/rating/update_rating_spec.rb +24 -6
  31. data/spec/models/rating/values_data_spec.rb +31 -8
  32. data/spec/models/rating_spec.rb +1 -0
  33. data/spec/support/db/migrate/create_authors_table.rb +9 -0
  34. data/spec/support/db/migrate/create_category_spec.rb +9 -0
  35. data/spec/support/migrate.rb +3 -1
  36. data/spec/support/models/author.rb +5 -0
  37. data/spec/support/models/category.rb +4 -0
  38. metadata +22 -20
  39. data/spec/support/html_matchers.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a11655b90d44d485ffd1f791ddb1acf2438cdb4a
4
- data.tar.gz: d18accbb4c21b0a63a3a771db0e0f32db007b289
3
+ metadata.gz: b7eb5b802982259c2ec3101c38bbfe18302226f2
4
+ data.tar.gz: cce443ffafbf9887de9d9261fa5e0223ad190b47
5
5
  SHA512:
6
- metadata.gz: 23142de88a5bde63875a119d0833c9321d6a351b01d9052a8c6f7a9c2c5aae4f86d6f0acea127f6c039779aca67b5bc7b63037b8ae520628d69e5946cf52393b
7
- data.tar.gz: aa732a3d4c09ff0472a142c61a074590df24b8701d0898bdd39341f0622ffb80388d3198cdbc029614255d1cb4d558b4c86862fe38dcdf6f080773784ece8c25
6
+ metadata.gz: a29fc4b2a26a3f8cd40d3d97ad2d1691a3bd5cbd70a5bd436c9f57b5dbbb11470185a602394f6629989dda254f68aa0ff01ce4ca22c59291a1f83f81ad36cb11
7
+ data.tar.gz: f79ec758ff607f735e9452b45705c52eacc6df4898cc4c8826a90fa6fe7cdf86fa0cde79c67321540aa262328afede7167f471e694b7df6744e944bbc52a41ae
@@ -1,3 +1,10 @@
1
+ ## v0.2.0
2
+
3
+ ### News
4
+
5
+ - Scope support to be possible rate item from a resource;
6
+ - Author model type where zero rating record is not generated on record creation.
7
+
1
8
  ## v0.1.0
2
9
 
3
10
  - First release.
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 (mean) = (Rating)
36
+ `R`: average for the resource
37
37
 
38
- `v`: number of votes for the movie = (votes)
38
+ `v`: number of votes for the resource
39
39
 
40
40
  `m`: average of the number of votes
41
41
 
42
- `C`: the mean vote across the whole report
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 be voted.
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 'configure Rating'
7
+ desc 'creates Rating migration'
8
8
 
9
9
  def create_migration
10
- version = Time.current.strftime('%Y%m%d%H%M%S')
10
+ template 'db/migrate/create_rating_tables.rb', "db/migrate/#{timestamp}_create_rating_tables.rb"
11
+ end
12
+
13
+ private
11
14
 
12
- template 'db/migrate/create_rating_tables.rb', "db/migrate/#{version}_create_rating_tables.rb"
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 , index: true, null: false, polymorphic: true
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: :index_rating_rates_on_author_and_resource,
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], unique: true
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
- has_one :rating,
37
+ has_many :rating_records,
26
38
  as: :resource,
27
39
  class_name: '::Rating::Rating',
28
40
  dependent: :destroy
29
41
 
30
- has_many :rates,
42
+ has_many :rates_records,
31
43
  as: :resource,
32
44
  class_name: '::Rating::Rate',
33
45
  dependent: :destroy
34
46
 
35
- has_many :rated,
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
- includes(:rating).order("#{Rating.table_name}.#{column} #{direction}")
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 , polymorphic: true
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, resource_type: resource.class.base_class.name]
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
- execute_sql [sql, resource.class.base_class.name, resource.id]
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: false)
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