rating 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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