rating 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +21 -0
- data/README.md +180 -0
- data/lib/generators/rating/install_generator.rb +15 -0
- data/lib/generators/rating/templates/db/migrate/create_rating_tables.rb +31 -0
- data/lib/rating.rb +10 -0
- data/lib/rating/models/rating/extension.rb +46 -0
- data/lib/rating/models/rating/rate.rb +41 -0
- data/lib/rating/models/rating/rating.rb +96 -0
- data/lib/rating/version.rb +5 -0
- data/spec/factories/article.rb +7 -0
- data/spec/factories/rating/rate.rb +10 -0
- data/spec/factories/rating/rating.rb +12 -0
- data/spec/factories/user.rb +7 -0
- data/spec/models/extension/after_create_spec.rb +19 -0
- data/spec/models/extension/order_by_rating_spec.rb +121 -0
- data/spec/models/extension/rate_for_spec.rb +14 -0
- data/spec/models/extension/rate_spec.rb +14 -0
- data/spec/models/extension/rated_question_spec.rb +20 -0
- data/spec/models/extension/rated_spec.rb +38 -0
- data/spec/models/extension/rates_spec.rb +38 -0
- data/spec/models/extension/rating_spec.rb +38 -0
- data/spec/models/rate/create_spec.rb +64 -0
- data/spec/models/rate/rate_for_spec.rb +20 -0
- data/spec/models/rate_spec.rb +26 -0
- data/spec/models/rating/averager_data_spec.rb +29 -0
- data/spec/models/rating/data_spec.rb +37 -0
- data/spec/models/rating/update_rating_spec.rb +28 -0
- data/spec/models/rating/values_data_spec.rb +33 -0
- data/spec/models/rating_spec.rb +17 -0
- data/spec/rails_helper.rb +11 -0
- data/spec/support/common.rb +22 -0
- data/spec/support/database_cleaner.rb +19 -0
- data/spec/support/db/migrate/create_articles_table.rb +9 -0
- data/spec/support/db/migrate/create_users_table.rb +9 -0
- data/spec/support/factory_bot.rb +9 -0
- data/spec/support/html_matchers.rb +7 -0
- data/spec/support/migrate.rb +7 -0
- data/spec/support/models/article.rb +5 -0
- data/spec/support/models/user.rb +5 -0
- data/spec/support/shoulda.rb +10 -0
- metadata +247 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
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.
|
data/README.md
ADDED
@@ -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
|
data/lib/rating.rb
ADDED
@@ -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
|