rating 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +180 -0
  5. data/lib/generators/rating/install_generator.rb +15 -0
  6. data/lib/generators/rating/templates/db/migrate/create_rating_tables.rb +31 -0
  7. data/lib/rating.rb +10 -0
  8. data/lib/rating/models/rating/extension.rb +46 -0
  9. data/lib/rating/models/rating/rate.rb +41 -0
  10. data/lib/rating/models/rating/rating.rb +96 -0
  11. data/lib/rating/version.rb +5 -0
  12. data/spec/factories/article.rb +7 -0
  13. data/spec/factories/rating/rate.rb +10 -0
  14. data/spec/factories/rating/rating.rb +12 -0
  15. data/spec/factories/user.rb +7 -0
  16. data/spec/models/extension/after_create_spec.rb +19 -0
  17. data/spec/models/extension/order_by_rating_spec.rb +121 -0
  18. data/spec/models/extension/rate_for_spec.rb +14 -0
  19. data/spec/models/extension/rate_spec.rb +14 -0
  20. data/spec/models/extension/rated_question_spec.rb +20 -0
  21. data/spec/models/extension/rated_spec.rb +38 -0
  22. data/spec/models/extension/rates_spec.rb +38 -0
  23. data/spec/models/extension/rating_spec.rb +38 -0
  24. data/spec/models/rate/create_spec.rb +64 -0
  25. data/spec/models/rate/rate_for_spec.rb +20 -0
  26. data/spec/models/rate_spec.rb +26 -0
  27. data/spec/models/rating/averager_data_spec.rb +29 -0
  28. data/spec/models/rating/data_spec.rb +37 -0
  29. data/spec/models/rating/update_rating_spec.rb +28 -0
  30. data/spec/models/rating/values_data_spec.rb +33 -0
  31. data/spec/models/rating_spec.rb +17 -0
  32. data/spec/rails_helper.rb +11 -0
  33. data/spec/support/common.rb +22 -0
  34. data/spec/support/database_cleaner.rb +19 -0
  35. data/spec/support/db/migrate/create_articles_table.rb +9 -0
  36. data/spec/support/db/migrate/create_users_table.rb +9 -0
  37. data/spec/support/factory_bot.rb +9 -0
  38. data/spec/support/html_matchers.rb +7 -0
  39. data/spec/support/migrate.rb +7 -0
  40. data/spec/support/models/article.rb +5 -0
  41. data/spec/support/models/user.rb +5 -0
  42. data/spec/support/shoulda.rb +10 -0
  43. metadata +247 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a11655b90d44d485ffd1f791ddb1acf2438cdb4a
4
+ data.tar.gz: d18accbb4c21b0a63a3a771db0e0f32db007b289
5
+ SHA512:
6
+ metadata.gz: 23142de88a5bde63875a119d0833c9321d6a351b01d9052a8c6f7a9c2c5aae4f86d6f0acea127f6c039779aca67b5bc7b63037b8ae520628d69e5946cf52393b
7
+ data.tar.gz: aa732a3d4c09ff0472a142c61a074590df24b8701d0898bdd39341f0622ffb80388d3198cdbc029614255d1cb4d558b4c86862fe38dcdf6f080773784ece8c25
@@ -0,0 +1,3 @@
1
+ ## v0.1.0
2
+
3
+ - First release.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Washington Botelho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,180 @@
1
+ # Rating
2
+
3
+ [![Build Status](https://travis-ci.org/wbotelhos/rating.svg)](https://travis-ci.org/wbotelhos/rating)
4
+ [![Gem Version](https://badge.fury.io/rb/rating.svg)](https://badge.fury.io/rb/rating)
5
+
6
+ A true Bayesian rating system with cache enabled.
7
+
8
+ ## JS Rating?
9
+
10
+ This is **Raty**: https://github.com/wbotelhos/raty :star2:
11
+
12
+ ## Description
13
+
14
+ Rating uses the know as "True Bayesian Estimate" inspired on [IMDb rating](http://www.imdb.com/help/show_leaf?votestopfaq) with the following formula:
15
+
16
+ ```
17
+ (WR) = (v ÷ (v + m)) × R + (m ÷ (v + m)) × C
18
+ ```
19
+
20
+ **IMDb Implementation:**
21
+
22
+ `WR`: weighted rating
23
+
24
+ `R`: average for the movie (mean) = (Rating)
25
+
26
+ `v`: number of votes for the movie = (votes)
27
+
28
+ `m`: minimum votes required to be listed in the Top 250
29
+
30
+ `C`: the mean vote across the whole report
31
+
32
+ **Rating Implementation:**
33
+
34
+ `WR`: weighted rating
35
+
36
+ `R`: average for the resource (mean) = (Rating)
37
+
38
+ `v`: number of votes for the movie = (votes)
39
+
40
+ `m`: average of the number of votes
41
+
42
+ `C`: the mean vote across the whole report
43
+
44
+ ## Install
45
+
46
+ Add the following code on your `Gemfile` and run `bundle install`:
47
+
48
+ ```ruby
49
+ gem 'rating'
50
+ ```
51
+
52
+ Run the following task to create a Rating migration:
53
+
54
+ ```bash
55
+ rails g rating:install
56
+ ```
57
+
58
+ Then execute the migrations to create the to create tables `rating_rates` and `rating_ratings`:
59
+
60
+ ```bash
61
+ rake db:migrate
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ Just add the callback `rating` to your model:
67
+
68
+ ```ruby
69
+ class User < ApplicationRecord
70
+ rating
71
+ end
72
+ ```
73
+
74
+ Now this model can vote or be voted.
75
+
76
+ ### rate
77
+
78
+ You can vote on some resource:
79
+
80
+ ```ruby
81
+ author = User.last
82
+ resource = Article.last
83
+
84
+ author.rate(resource, 3)
85
+ ```
86
+
87
+ ### rating
88
+
89
+ A voted resource exposes a cached data about it state:
90
+
91
+ ```ruby
92
+ resource = Article.last
93
+
94
+ resource.rating
95
+ ```
96
+
97
+ It will return a `Rating` object that keeps:
98
+
99
+ `average`: the normal mean of votes;
100
+
101
+ `estimate`: the true Bayesian estimate mean value (you should use this over average);
102
+
103
+ `sum`: the sum of votes for this resource;
104
+
105
+ `total`: the total of votes for this resource.
106
+
107
+ ### rate_for
108
+
109
+ You can retrieve the rate of some author gave to some resource:
110
+
111
+ ```ruby
112
+ author = User.last
113
+ resource = Article.last
114
+
115
+ author.rate_for resource
116
+ ```
117
+
118
+ It will return a `Rate` object that keeps:
119
+
120
+ `author`: the author of vote;
121
+
122
+ `resource`: the resource that received the vote;
123
+
124
+ `value`: the value of the vote.
125
+
126
+ ### rated?
127
+
128
+ Maybe you want just to know if some author already rated some resource and receive `true` or `false`:
129
+
130
+ ```ruby
131
+ author = User.last
132
+ resource = Article.last
133
+
134
+ author.rated? resource
135
+ ```
136
+
137
+ ### rates
138
+
139
+ You can retrieve all rates made by some author:
140
+
141
+ ```ruby
142
+ author = User.last
143
+
144
+ author.rates
145
+ ```
146
+
147
+ It will return a collection of `Rate` object.
148
+
149
+ ### rated
150
+
151
+ In the same way you can retrieve all rates that some author received:
152
+
153
+ ```ruby
154
+ author = User.last
155
+
156
+ author.rated
157
+ ```
158
+
159
+ It will return a collection of `Rate` object.
160
+
161
+ ### order_by_rating
162
+
163
+ You can list resource ordered by rating data:
164
+
165
+ ```ruby
166
+ Article.order_by_rating
167
+ ```
168
+
169
+ It will return a collection of resource ordered by `estimate desc` as default.
170
+ The order column and direction can be changed:
171
+
172
+ ```ruby
173
+ Article.order_by_rating :average, :asc
174
+ ```
175
+
176
+ It will return a collection of resource ordered by `Rating` table data.
177
+
178
+ ## Love it!
179
+
180
+ Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Gratipay](https://gratipay.com/rating). Thanks! (:
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rating
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ desc 'configure Rating'
8
+
9
+ def create_migration
10
+ version = Time.current.strftime('%Y%m%d%H%M%S')
11
+
12
+ template 'db/migrate/create_rating_tables.rb', "db/migrate/#{version}_create_rating_tables.rb"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRatingTables < ActiveRecord::Migration[5.0]
4
+ def change
5
+ create_table :rating_rates do |t|
6
+ t.decimal :value, default: 0, precision: 17, scale: 14
7
+
8
+ t.references :author , index: true, null: false, polymorphic: true
9
+ t.references :resource, index: true, null: false, polymorphic: true
10
+
11
+ t.timestamps null: false
12
+ end
13
+
14
+ add_index :rating_rates, %i[author_id author_type resource_id resource_type],
15
+ name: :index_rating_rates_on_author_and_resource,
16
+ unique: true
17
+
18
+ create_table :rating_ratings do |t|
19
+ t.decimal :average , default: 0, mull: false, precision: 17, scale: 14
20
+ t.decimal :estimate, default: 0, mull: false, precision: 17, scale: 14
21
+ t.integer :sum , default: 0, mull: false
22
+ t.integer :total , default: 0, mull: false
23
+
24
+ t.references :resource, index: true, null: false, polymorphic: true
25
+
26
+ t.timestamps null: false
27
+ end
28
+
29
+ add_index :rating_ratings, %i[resource_id resource_type], unique: true
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rating
4
+ end
5
+
6
+ require 'rating/models/rating/extension'
7
+ require 'rating/models/rating/rate'
8
+ require 'rating/models/rating/rating'
9
+
10
+ ActiveRecord::Base.include Rating::Extension
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rating
4
+ module Extension
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def rate(resource, value, author: self)
9
+ Rate.create author: author, resource: resource, value: value
10
+ end
11
+
12
+ def rate_for(resource)
13
+ Rate.rate_for author: self, resource: resource
14
+ end
15
+
16
+ def rated?(resource)
17
+ !rate_for(resource).nil?
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ def rating
23
+ after_create { Rating.find_or_create_by resource: self }
24
+
25
+ has_one :rating,
26
+ as: :resource,
27
+ class_name: '::Rating::Rating',
28
+ dependent: :destroy
29
+
30
+ has_many :rates,
31
+ as: :resource,
32
+ class_name: '::Rating::Rate',
33
+ dependent: :destroy
34
+
35
+ has_many :rated,
36
+ as: :author,
37
+ class_name: '::Rating::Rate',
38
+ dependent: :destroy
39
+
40
+ scope :order_by_rating, ->(column = :estimate, direction = :desc) {
41
+ includes(:rating).order("#{Rating.table_name}.#{column} #{direction}")
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rating
4
+ class Rate < ActiveRecord::Base
5
+ self.table_name = 'rating_rates'
6
+
7
+ after_save :update_rating
8
+
9
+ belongs_to :author , polymorphic: true
10
+ belongs_to :resource, polymorphic: true
11
+
12
+ validates :author, :resource, :value, presence: true
13
+ validates :value, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 }
14
+
15
+ validates :author_id, uniqueness: {
16
+ case_sensitive: false,
17
+ scope: %i[author_type resource_id resource_type]
18
+ }
19
+
20
+ def self.create(author:, resource:, value:)
21
+ record = find_or_initialize_by(author: author, resource: resource)
22
+
23
+ return record if record.persisted? && value == record.value
24
+
25
+ record.value = value
26
+ record.save
27
+
28
+ record
29
+ end
30
+
31
+ def self.rate_for(author:, resource:)
32
+ find_by author: author, resource: resource
33
+ end
34
+
35
+ private
36
+
37
+ def update_rating
38
+ ::Rating::Rating.update_rating resource
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rating
4
+ class Rating < ActiveRecord::Base
5
+ self.table_name = 'rating_ratings'
6
+
7
+ belongs_to :resource, polymorphic: true
8
+
9
+ validates :average, :estimate, :resource, :sum, :total, presence: true
10
+ validates :average, :estimate, :sum, :total, numericality: true
11
+
12
+ 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)
16
+
17
+ sql = %(
18
+ SELECT
19
+ (#{total_count} / CAST(#{distinct_count} AS float)) count_avg,
20
+ COALESCE(AVG(value), 0) rating_avg
21
+ FROM #{rate_table_name}
22
+ WHERE resource_type = :resource_type
23
+ ).squish
24
+
25
+ execute_sql [sql, resource_type: resource.class.base_class.name]
26
+ end
27
+
28
+ def data(resource)
29
+ averager = averager_data(resource)
30
+ values = values_data(resource)
31
+
32
+ {
33
+ average: values.rating_avg,
34
+ estimate: estimate(averager, values),
35
+ sum: values.rating_sum,
36
+ total: values.rating_count
37
+ }
38
+ end
39
+
40
+ def values_data(resource)
41
+ sql = %(
42
+ SELECT
43
+ COALESCE(AVG(value), 0) rating_avg,
44
+ COALESCE(SUM(value), 0) rating_sum,
45
+ COUNT(1) rating_count
46
+ FROM #{rate_table_name}
47
+ WHERE resource_type = ? and resource_id = ?
48
+ ).squish
49
+
50
+ execute_sql [sql, resource.class.base_class.name, resource.id]
51
+ end
52
+
53
+ def update_rating(resource)
54
+ record = find_or_initialize_by(resource: resource)
55
+ result = data(resource)
56
+
57
+ record.average = result[:average]
58
+ record.sum = result[:sum]
59
+ record.total = result[:total]
60
+ record.estimate = result[:estimate]
61
+
62
+ record.save
63
+ end
64
+
65
+ private
66
+
67
+ def estimate(averager, values)
68
+ resource_type_rating_avg = averager.rating_avg
69
+ count_avg = averager.count_avg
70
+ resource_rating_avg = values.rating_avg
71
+ resource_rating_count = values.rating_count.to_f
72
+
73
+ (resource_rating_count / (resource_rating_count + count_avg)) * resource_rating_avg +
74
+ (count_avg / (resource_rating_count + count_avg)) * resource_type_rating_avg
75
+ end
76
+
77
+ def execute_sql(sql)
78
+ Rate.find_by_sql(sql).first
79
+ end
80
+
81
+ def how_many_resource_received_votes_sql?(distinct: false)
82
+ count = distinct ? 'COUNT(DISTINCT resource_id)' : 'COUNT(1)'
83
+
84
+ %((
85
+ SELECT #{count}
86
+ FROM #{rate_table_name}
87
+ WHERE resource_type = :resource_type
88
+ ))
89
+ end
90
+
91
+ def rate_table_name
92
+ @rate_table_name ||= Rate.table_name
93
+ end
94
+ end
95
+ end
96
+ end