add scope support

main
Washington Botelho 2017-11-02 16:55:46 -02:00
parent dd1c8e2dd2
commit cd51130143
No known key found for this signature in database
GPG Key ID: BECE10A8106CC7A0
31 changed files with 771 additions and 186 deletions

101
README.md
View File

@ -3,7 +3,7 @@
[![Build Status](https://travis-ci.org/wbotelhos/rating.svg)](https://travis-ci.org/wbotelhos/rating) [![Build Status](https://travis-ci.org/wbotelhos/rating.svg)](https://travis-ci.org/wbotelhos/rating)
[![Gem Version](https://badge.fury.io/rb/rating.svg)](https://badge.fury.io/rb/rating) [![Gem Version](https://badge.fury.io/rb/rating.svg)](https://badge.fury.io/rb/rating)
A true Bayesian rating system with cache enabled. A true Bayesian rating system with scope and cache enabled.
## JS Rating? ## JS Rating?
@ -71,7 +71,7 @@ class User < ApplicationRecord
end end
``` ```
Now this model can vote or be voted. Now this model can vote or receive votes.
### rate ### rate
@ -175,6 +175,103 @@ Article.order_by_rating :average, :asc
It will return a collection of resource ordered by `Rating` table data. It will return a collection of resource ordered by `Rating` table data.
### Scope
All methods support scope query, since you may want to vote on items of a resource instead the resource itself.
Let's say an article belongs to one or more categories and you want to vote on some categories of this article.
```ruby
category_1 = Category.first
category_2 = Category.second
author = User.last
resource = Article.last
```
In this situation you should scope the vote of article with some category:
**rate**
```ruby
author.rate resource, 3, scopeable: category_1
author.rate resource, 5, scopeable: category_2
```
Now `article` has a rating for `category_1` and another one for `category_2`.
**rating**
Recovering the rating values for article, we have:
```ruby
author.rating
# nil
```
But using the scope to make the right query:
```ruby
author.rating scope: category_1
# { average: 3, estimate: 3, sum: 3, total: 1 }
author.rating scope: category_2
# { average: 5, estimate: 5, sum: 5, total: 1 }
```
**rated**
On the same way you can find your rates with a scoped query:
```ruby
user.rated scope: category_1
# { value: 3, scopeable: category_1 }
```
**rates**
The resource still have the power to consult its rates:
```ruby
article.rates scope: category_1
# { value: 3, scopeable: category_1 }
article.rates scope: category_2
# { value: 3, scopeable: category_2 }
```
**order_by_rating**
To order the rating you do the same thing:
```ruby
Article.order_by_rating scope: category_1
```
### Records
Maybe you want to recover all records with or without scope, so you can add the suffix `_records` on relations:
```ruby
category_1 = Category.first
category_2 = Category.second
author = User.last
resource = Article.last
author.rate resource, 1
author.rate resource, 3, scopeable: category_1
author.rate resource, 5, scopeable: category_2
author.rating_records
# { average: 1, estimate: 1, scopeable: nil , sum: 1, total: 1 },
# { average: 3, estimate: 3, scopeable: category_1, sum: 3, total: 1 },
# { average: 5, estimate: 5, scopeable: category_2, sum: 5, total: 1 }
user.rated_records
# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }
article.rates_records
# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }
```
## Love it! ## Love it!
Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Gratipay](https://gratipay.com/rating). Thanks! (: Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Gratipay](https://gratipay.com/rating). Thanks! (:

View File

@ -5,14 +5,15 @@ class CreateRatingTables < ActiveRecord::Migration[5.0]
create_table :rating_rates do |t| create_table :rating_rates do |t|
t.decimal :value, default: 0, precision: 17, scale: 14 t.decimal :value, default: 0, precision: 17, scale: 14
t.references :author , index: true, null: false, polymorphic: true t.references :author , index: true, null: false, polymorphic: true
t.references :resource, index: true, null: false, polymorphic: true t.references :resource , index: true, null: false, polymorphic: true
t.references :scopeable, index: true, null: true , polymorphic: true
t.timestamps null: false t.timestamps null: false
end end
add_index :rating_rates, %i[author_id author_type resource_id resource_type], add_index :rating_rates, %i[author_id author_type resource_id resource_type scopeable_id scopeable_type],
name: :index_rating_rates_on_author_and_resource, name: :index_rating_rates_on_author_and_resource_and_scopeable,
unique: true unique: true
create_table :rating_ratings do |t| create_table :rating_ratings do |t|
@ -21,11 +22,14 @@ class CreateRatingTables < ActiveRecord::Migration[5.0]
t.integer :sum , default: 0, mull: false t.integer :sum , default: 0, mull: false
t.integer :total , default: 0, mull: false t.integer :total , default: 0, mull: false
t.references :resource, index: true, null: false, polymorphic: true t.references :resource , index: true, null: false, polymorphic: true
t.references :scopeable, index: true, null: true , polymorphic: true
t.timestamps null: false t.timestamps null: false
end end
add_index :rating_ratings, %i[resource_id resource_type], unique: true add_index :rating_ratings, %i[resource_id resource_type scopeable_id scopeable_type],
name: :index_rating_rating_on_resource_and_scopeable,
unique: true
end end
end end

View File

@ -5,16 +5,28 @@ module Rating
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
def rate(resource, value, author: self) def rate(resource, value, author: self, scope: nil)
Rate.create author: author, resource: resource, value: value Rate.create author: author, resource: resource, scopeable: scope, value: value
end end
def rate_for(resource) def rate_for(resource, scope: nil)
Rate.rate_for author: self, resource: resource Rate.rate_for author: self, resource: resource, scopeable: scope
end end
def rated?(resource) def rated?(resource, scope: nil)
!rate_for(resource).nil? !rate_for(resource, scope: scope).nil?
end
def rates(scope: nil)
rates_records.where scopeable: scope
end
def rated(scope: nil)
rated_records.where scopeable: scope
end
def rating(scope: nil)
rating_records.find_by scopeable: scope
end end
end end
@ -22,23 +34,30 @@ module Rating
def rating def rating
after_create { Rating.find_or_create_by resource: self } after_create { Rating.find_or_create_by resource: self }
has_one :rating, has_many :rating_records,
as: :resource, as: :resource,
class_name: '::Rating::Rating', class_name: '::Rating::Rating',
dependent: :destroy dependent: :destroy
has_many :rates, has_many :rates_records,
as: :resource, as: :resource,
class_name: '::Rating::Rate', class_name: '::Rating::Rate',
dependent: :destroy dependent: :destroy
has_many :rated, has_many :rated_records,
as: :author, as: :author,
class_name: '::Rating::Rate', class_name: '::Rating::Rate',
dependent: :destroy dependent: :destroy
scope :order_by_rating, ->(column = :estimate, direction = :desc) { scope :order_by_rating, ->(column = :estimate, direction = :desc, scope: nil) {
includes(:rating).order("#{Rating.table_name}.#{column} #{direction}") scope_values = {
scopeable_id: scope&.id,
scopeable_type: scope&.class&.base_class&.name
}
includes(:rating_records)
.where(Rating.table_name => scope_values)
.order("#{Rating.table_name}.#{column} #{direction}")
} }
end end
end end

View File

@ -6,19 +6,20 @@ module Rating
after_save :update_rating after_save :update_rating
belongs_to :author , polymorphic: true belongs_to :author , polymorphic: true
belongs_to :resource, polymorphic: true belongs_to :resource , polymorphic: true
belongs_to :scopeable, polymorphic: true
validates :author, :resource, :value, presence: true validates :author, :resource, :value, presence: true
validates :value, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 } validates :value, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 }
validates :author_id, uniqueness: { validates :author_id, uniqueness: {
case_sensitive: false, case_sensitive: false,
scope: %i[author_type resource_id resource_type] scope: %i[author_type resource_id resource_type scopeable_id scopeable_type]
} }
def self.create(author:, resource:, value:) def self.create(author:, resource:, scopeable: nil, value:)
record = find_or_initialize_by(author: author, resource: resource) record = find_or_initialize_by(author: author, resource: resource, scopeable: scopeable)
return record if record.persisted? && value == record.value return record if record.persisted? && value == record.value
@ -28,14 +29,14 @@ module Rating
record record
end end
def self.rate_for(author:, resource:) def self.rate_for(author:, resource:, scopeable: nil)
find_by author: author, resource: resource find_by author: author, resource: resource, scopeable: scopeable
end end
private private
def update_rating def update_rating
::Rating::Rating.update_rating resource ::Rating::Rating.update_rating resource, scopeable
end end
end end
end end

View File

@ -4,30 +4,34 @@ module Rating
class Rating < ActiveRecord::Base class Rating < ActiveRecord::Base
self.table_name = 'rating_ratings' self.table_name = 'rating_ratings'
belongs_to :resource, polymorphic: true belongs_to :resource , polymorphic: true
belongs_to :scopeable, polymorphic: true
validates :average, :estimate, :resource, :sum, :total, presence: true validates :average, :estimate, :resource, :sum, :total, presence: true
validates :average, :estimate, :sum, :total, numericality: true validates :average, :estimate, :sum, :total, numericality: true
class << self class << self
def averager_data(resource) def averager_data(resource, scopeable)
total_count = how_many_resource_received_votes_sql? total_count = how_many_resource_received_votes_sql?(distinct: false, scopeable: scopeable)
distinct_count = how_many_resource_received_votes_sql?(distinct: true) distinct_count = how_many_resource_received_votes_sql?(distinct: true , scopeable: scopeable)
values = { resource_type: resource.class.base_class.name }
values[:scopeable_type] = scopeable.class.base_class.name unless scopeable.nil?
sql = %( sql = %(
SELECT SELECT
(#{total_count} / CAST(#{distinct_count} AS float)) count_avg, (#{total_count} / CAST(#{distinct_count} AS float)) count_avg,
COALESCE(AVG(value), 0) rating_avg COALESCE(AVG(value), 0) rating_avg
FROM #{rate_table_name} FROM #{rate_table_name}
WHERE resource_type = :resource_type WHERE resource_type = :resource_type and #{scope_type_query(scopeable)}
).squish ).squish
execute_sql [sql, resource_type: resource.class.base_class.name] execute_sql [sql, values]
end end
def data(resource) def data(resource, scopeable)
averager = averager_data(resource) averager = averager_data(resource, scopeable)
values = values_data(resource) values = values_data(resource, scopeable)
{ {
average: values.rating_avg, average: values.rating_avg,
@ -37,22 +41,31 @@ module Rating
} }
end end
def values_data(resource) def values_data(resource, scopeable)
scope_query = if scopeable.nil?
'scopeable_type is NULL and scopeable_id is NULL'
else
'scopeable_type = ? and scopeable_id = ?'
end
sql = %( sql = %(
SELECT SELECT
COALESCE(AVG(value), 0) rating_avg, COALESCE(AVG(value), 0) rating_avg,
COALESCE(SUM(value), 0) rating_sum, COALESCE(SUM(value), 0) rating_sum,
COUNT(1) rating_count COUNT(1) rating_count
FROM #{rate_table_name} FROM #{rate_table_name}
WHERE resource_type = ? and resource_id = ? WHERE resource_type = ? and resource_id = ? and #{scope_query}
).squish ).squish
execute_sql [sql, resource.class.base_class.name, resource.id] values = [sql, resource.class.base_class.name, resource.id]
values += [scopeable.class.base_class.name, scopeable.id] unless scopeable.nil?
execute_sql values
end end
def update_rating(resource) def update_rating(resource, scopeable)
record = find_or_initialize_by(resource: resource) record = find_or_initialize_by(resource: resource, scopeable: scopeable)
result = data(resource) result = data(resource, scopeable)
record.average = result[:average] record.average = result[:average]
record.sum = result[:sum] record.sum = result[:sum]
@ -78,19 +91,23 @@ module Rating
Rate.find_by_sql(sql).first Rate.find_by_sql(sql).first
end end
def how_many_resource_received_votes_sql?(distinct: false) def how_many_resource_received_votes_sql?(distinct:, scopeable:)
count = distinct ? 'COUNT(DISTINCT resource_id)' : 'COUNT(1)' count = distinct ? 'COUNT(DISTINCT resource_id)' : 'COUNT(1)'
%(( %((
SELECT #{count} SELECT #{count}
FROM #{rate_table_name} FROM #{rate_table_name}
WHERE resource_type = :resource_type WHERE resource_type = :resource_type and #{scope_type_query(scopeable)}
)) ))
end end
def rate_table_name def rate_table_name
@rate_table_name ||= Rate.table_name @rate_table_name ||= Rate.table_name
end end
def scope_type_query(scopeable)
scopeable.nil? ? 'scopeable_type is NULL' : 'scopeable_type = :scopeable_type'
end
end end
end end
end end

View File

@ -8,14 +8,14 @@ require 'rating/version'
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.author = 'Washington Botelho' spec.author = 'Washington Botelho'
spec.description = 'A true Bayesian rating system with cache enabled.' spec.description = 'A true Bayesian rating system with scope and cache enabled.'
spec.email = 'wbotelhos@gmail.com' spec.email = 'wbotelhos@gmail.com'
spec.files = Dir['lib/**/*'] + %w[CHANGELOG.md LICENSE README.md] spec.files = Dir['lib/**/*'] + %w[CHANGELOG.md LICENSE README.md]
spec.homepage = 'https://github.com/wbotelhos/rating' spec.homepage = 'https://github.com/wbotelhos/rating'
spec.license = 'MIT' spec.license = 'MIT'
spec.name = 'rating' spec.name = 'rating'
spec.platform = Gem::Platform::RUBY spec.platform = Gem::Platform::RUBY
spec.summary = 'A true Bayesian rating system with cache enabled.' spec.summary = 'A true Bayesian rating system with scope and cache enabled.'
spec.test_files = Dir['spec/**/*'] spec.test_files = Dir['spec/**/*']
spec.version = Rating::VERSION spec.version = Rating::VERSION

View File

@ -2,6 +2,6 @@
FactoryBot.define do FactoryBot.define do
factory :article do factory :article do
sequence(:name) { |i| "Name #{i}" } sequence(:name) { |i| "Article #{i}" }
end end
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :category do
sequence(:name) { |i| "Category #{i}" }
end
end

View File

@ -2,6 +2,6 @@
FactoryBot.define do FactoryBot.define do
factory :user do factory :user do
sequence(:name) { |i| "Name #{i}" } sequence(:name) { |i| "User #{i}" }
end end
end end

View File

@ -9,11 +9,12 @@ RSpec.describe Rating::Extension, ':after_create' do
it 'creates a record with zero values just to be easy to make the count' do it 'creates a record with zero values just to be easy to make the count' do
rating = Rating::Rating.find_by(resource: user) rating = Rating::Rating.find_by(resource: user)
expect(rating.average).to eq 0 expect(rating.average).to eq 0
expect(rating.estimate).to eq 0 expect(rating.estimate).to eq 0
expect(rating.resource).to eq user expect(rating.resource).to eq user
expect(rating.sum).to eq 0 expect(rating.scopeable).to eq nil
expect(rating.total).to eq 0 expect(rating.sum).to eq 0
expect(rating.total).to eq 0
end end
end end
end end

View File

@ -3,6 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Extension, ':order_by_rating' do RSpec.describe Rating::Extension, ':order_by_rating' do
let!(:category) { create :category }
let!(:user_1) { create :user } let!(:user_1) { create :user }
let!(:user_2) { create :user } let!(:user_2) { create :user }
@ -15,6 +17,9 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
create :rating_rate, author: user_1, resource: article_2, value: 11 create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10 create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1 create :rating_rate, author: user_2, resource: article_1, value: 1
create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1
create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2
end end
context 'with default filters' do context 'with default filters' do
@ -36,6 +41,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
article_1 article_1
] ]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:average, :asc, scope: category)).to eq [
article_1
]
end
end
end end
context 'as desc' do context 'as desc' do
@ -46,6 +59,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
article_3 article_3
] ]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:average, :desc, scope: category)).to eq [
article_1
]
end
end
end end
end end
@ -58,6 +79,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
article_1 article_1
] ]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:estimate, :asc, scope: category)).to eq [
article_1
]
end
end
end end
context 'as desc' do context 'as desc' do
@ -68,6 +97,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
article_3 article_3
] ]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:estimate, :desc, scope: category)).to eq [
article_1
]
end
end
end end
end end
@ -80,6 +117,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
article_1 article_1
] ]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:sum, :asc, scope: category)).to eq [
article_1
]
end
end
end end
context 'as desc' do context 'as desc' do
@ -90,6 +135,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
article_3 article_3
] ]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:sum, :desc, scope: category)).to eq [
article_1
]
end
end
end end
context 'filtering by :total' do context 'filtering by :total' do
@ -100,6 +153,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
expect(result[0..1]).to match_array [article_2, article_3] expect(result[0..1]).to match_array [article_2, article_3]
expect(result.last).to eq article_1 expect(result.last).to eq article_1
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:total, :asc, scope: category)).to eq [
article_1
]
end
end
end end
context 'as desc' do context 'as desc' do
@ -109,6 +170,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
expect(result.first).to eq article_1 expect(result.first).to eq article_1
expect(result[1..2]).to match_array [article_2, article_3] expect(result[1..2]).to match_array [article_2, article_3]
end end
context 'with scope' do
it 'works' do
expect(Article.order_by_rating(:total, :desc, scope: category)).to eq [
article_1
]
end
end
end end
end end
end end
@ -117,5 +186,11 @@ RSpec.describe Rating::Extension, ':order_by_rating' do
it 'works' do it 'works' do
expect(User.order_by_rating(:total, :desc)).to match_array [user_1, user_2] expect(User.order_by_rating(:total, :desc)).to match_array [user_1, user_2]
end end
context 'with scope' do
it 'returns empty since creation has no scope' do
expect(User.order_by_rating(:total, :desc, scope: category)).to eq []
end
end
end end
end end

View File

@ -6,9 +6,21 @@ RSpec.describe Rating::Extension, ':rate_for' do
let!(:user) { create :user } let!(:user) { create :user }
let!(:article) { create :article } let!(:article) { create :article }
it 'delegates to rate object' do context 'with no scopeable' do
expect(Rating::Rate).to receive(:rate_for).with author: user, resource: article it 'delegates to rate object' do
expect(Rating::Rate).to receive(:rate_for).with author: user, resource: article, scopeable: nil
user.rate_for article user.rate_for article
end
end
context 'with scopeable' do
let!(:category) { build :category }
it 'delegates to rate object' do
expect(Rating::Rate).to receive(:rate_for).with author: user, resource: article, scopeable: category
user.rate_for article, scope: category
end
end end
end end

View File

@ -6,9 +6,21 @@ RSpec.describe Rating::Extension, ':rate' do
let!(:user) { create :user } let!(:user) { create :user }
let!(:article) { create :article } let!(:article) { create :article }
it 'delegates to rate object' do context 'with no scopeable' do
expect(Rating::Rate).to receive(:create).with author: user, resource: article, value: 3 it 'delegates to rate object' do
expect(Rating::Rate).to receive(:create).with author: user, resource: article, scopeable: nil, value: 3
user.rate article, 3 user.rate article, 3
end
end
context 'with scopeable' do
let!(:category) { build :category }
it 'delegates to rate object' do
expect(Rating::Rate).to receive(:create).with author: user, resource: article, scopeable: category, value: 3
user.rate article, 3, scope: category
end
end end
end end

View File

@ -6,15 +6,33 @@ RSpec.describe Rating::Extension, ':rated?' do
let!(:user) { create :user } let!(:user) { create :user }
let!(:article) { create :article } let!(:article) { create :article }
context 'when has no rate for the given resource' do context 'with no scopeable' do
before { allow(user).to receive(:rate_for).with(article) { nil } } context 'when has no rate for the given resource' do
before { allow(user).to receive(:rate_for).with(article, scope: nil) { nil } }
specify { expect(user.rated?(article)).to eq false } specify { expect(user.rated?(article)).to eq false }
end
context 'when has rate for the given resource' do
before { allow(user).to receive(:rate_for).with(article, scope: nil) { double } }
specify { expect(user.rated?(article)).to eq true }
end
end end
context 'when has rate for the given resource' do context 'with scopeable' do
before { allow(user).to receive(:rate_for).with(article) { double } } let!(:category) { build :category }
specify { expect(user.rated?(article)).to eq true } context 'when has no rate for the given resource' do
before { allow(user).to receive(:rate_for).with(article, scope: category) { nil } }
specify { expect(user.rated?(article, scope: category)).to eq false }
end
context 'when has rate for the given resource' do
before { allow(user).to receive(:rate_for).with(article, scope: category) { double } }
specify { expect(user.rated?(article, scope: category)).to eq true }
end
end end
end end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, '.rated_records' do
let!(:category) { create :category }
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
let!(:rate_1) { create :rating_rate, author: user_1, resource: article_1, value: 100 }
let!(:rate_2) { create :rating_rate, author: user_1, resource: article_2, value: 11 }
let!(:rate_3) { create :rating_rate, author: user_1, resource: article_3, value: 10 }
let!(:rate_4) { create :rating_rate, author: user_2, resource: article_1, value: 1 }
let!(:rate_5) { create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1 }
let!(:rate_6) { create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2 }
it 'returns all rates that this author gave' do
expect(user_1.rated_records).to match_array [rate_1, rate_2, rate_3, rate_5]
end
end

View File

@ -3,36 +3,56 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Extension, ':rated' do RSpec.describe Rating::Extension, ':rated' do
let!(:user) { create :user } let!(:category) { create :category }
let!(:article) { create :article }
before { user.rate article, 3 } let!(:user_1) { create :user }
let!(:user_2) { create :user }
it 'returns rates made by the caller' do let!(:article_1) { create :article }
expect(user.rated).to eq [Rating::Rate.find_by(resource: article)] let!(:article_2) { create :article }
let!(:article_3) { create :article }
let!(:rate_1) { create :rating_rate, author: user_1, resource: article_1, value: 100 }
let!(:rate_2) { create :rating_rate, author: user_1, resource: article_2, value: 11 }
let!(:rate_3) { create :rating_rate, author: user_1, resource: article_3, value: 10 }
let!(:rate_4) { create :rating_rate, author: user_2, resource: article_1, value: 1 }
let!(:rate_5) { create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1 }
let!(:rate_6) { create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2 }
context 'with no scope' do
it 'returns rates made by this author' do
expect(user_1.rated).to match_array [rate_1, rate_2, rate_3]
end
end
context 'with no scope' do
it 'returns scoped rates made by this author' do
expect(user_1.rated(scope: category)).to eq [rate_5]
end
end end
context 'when destroy author' do context 'when destroy author' do
before do before do
expect(Rating::Rate.where(author: user).count).to eq 1 expect(Rating::Rate.where(author: user_1).count).to eq 4
user.destroy! user_1.destroy!
end end
it 'destroys rates of this author' do it 'destroys rates of that author' do
expect(Rating::Rate.where(author: user).count).to eq 0 expect(Rating::Rate.where(author: user_1).count).to eq 0
end end
end end
context 'when destroy resource rated by author' do context 'when destroy resource rated by author' do
before do before do
expect(Rating::Rate.where(resource: article).count).to eq 1 expect(Rating::Rate.where(resource: article_1).count).to eq 4
article.destroy! article_1.destroy!
end end
it 'destroys rates for that resource' do it 'destroys rates of that resource' do
expect(Rating::Rate.where(resource: article).count).to eq 0 expect(Rating::Rate.where(resource: article_1).count).to eq 0
end end
end end
end end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, '.rates_records' do
let!(:category) { create :category }
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
let!(:rate_1) { create :rating_rate, author: user_1, resource: article_1, value: 100 }
let!(:rate_2) { create :rating_rate, author: user_1, resource: article_2, value: 11 }
let!(:rate_3) { create :rating_rate, author: user_1, resource: article_3, value: 10 }
let!(:rate_4) { create :rating_rate, author: user_2, resource: article_1, value: 1 }
let!(:rate_5) { create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1 }
let!(:rate_6) { create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2 }
it 'returns all rates that this resource received' do
expect(article_1.rates_records).to match_array [rate_1, rate_4, rate_5, rate_6]
end
end

View File

@ -3,36 +3,52 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Extension, ':rates' do RSpec.describe Rating::Extension, ':rates' do
let!(:user) { create :user } let!(:category) { create :category }
let!(:article) { create :article }
before { user.rate article, 3 } let!(:user_1) { create :user }
let!(:user_2) { create :user }
it 'returns rates record' do let!(:article_1) { create :article }
expect(article.rates).to eq [Rating::Rate.last] let!(:article_2) { create :article }
let!(:article_3) { create :article }
let!(:rate_1) { create :rating_rate, author: user_1, resource: article_1, value: 100 }
let!(:rate_2) { create :rating_rate, author: user_1, resource: article_2, value: 11 }
let!(:rate_3) { create :rating_rate, author: user_1, resource: article_3, value: 10 }
let!(:rate_4) { create :rating_rate, author: user_2, resource: article_1, value: 1 }
let!(:rate_5) { create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1 }
let!(:rate_6) { create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2 }
context 'with no scope' do
it 'returns rates that this resource received' do
expect(article_1.rates).to match_array [rate_1, rate_4]
end
end
context 'with scope' do
it 'returns scoped rates that this resource received' do
expect(article_1.rates(scope: category)).to match_array [rate_5, rate_6]
end
end end
context 'when destroy author' do context 'when destroy author' do
before do it 'destroys rates of that author' do
expect(Rating::Rate.where(resource: article).count).to eq 1 expect(Rating::Rate.where(author: user_1).count).to eq 4
user.destroy! user_1.destroy!
end
it 'destroys rates of that resource' do expect(Rating::Rate.where(author: user_1).count).to eq 0
expect(Rating::Rate.where(resource: article).count).to eq 0
end end
end end
context 'when destroy resource' do context 'when destroy resource' do
before do
expect(Rating::Rate.where(resource: article).count).to eq 1
article.destroy!
end
it 'destroys rates of that resource' do it 'destroys rates of that resource' do
expect(Rating::Rate.where(resource: article).count).to eq 0 expect(Rating::Rate.where(resource: article_1).count).to eq 4
article_1.destroy!
expect(Rating::Rate.where(resource: article_1).count).to eq 0
end end
end end
end end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, '.rating' do
let!(:category) { create :category }
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
before do
create :rating_rate, author: user_1, resource: article_1, value: 100
create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1
create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1
create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2
end
it 'returns all rating of this resource' do
expect(article_1.rating_records).to match_array Rating::Rating.where(resource: article_1)
end
end

View File

@ -2,37 +2,53 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Extension, ':rating' do RSpec.describe Rating::Extension, '.rating' do
let!(:user_1) { create :user } let!(:category) { create :category }
let!(:article) { create :article }
before { user_1.rate article, 1 } let!(:user_1) { create :user }
let!(:user_2) { create :user }
it 'returns rating record' do let!(:article_1) { create :article }
expect(article.rating).to eq Rating::Rating.last let!(:article_2) { create :article }
let!(:article_3) { create :article }
let!(:rate_1) { create :rating_rate, author: user_1, resource: article_1, value: 100 }
let!(:rate_2) { create :rating_rate, author: user_1, resource: article_2, value: 11 }
let!(:rate_3) { create :rating_rate, author: user_1, resource: article_3, value: 10 }
let!(:rate_4) { create :rating_rate, author: user_2, resource: article_1, value: 1 }
let!(:rate_5) { create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1 }
let!(:rate_6) { create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2 }
context 'with no scope' do
it 'returns rating record' do
expect(article_1.rating).to eq Rating::Rating.find_by(resource: article_1, scopeable: nil)
end
end
context 'with scope' do
it 'returns scoped rating record' do
expect(article_1.rating(scope: category)).to eq Rating::Rating.find_by(resource: article_1, scopeable: category)
end
end end
context 'when destroy author' do context 'when destroy author' do
let!(:user_2) { create :user }
before { user_2.rate article, 2 }
it 'does not destroy resource rating' do it 'does not destroy resource rating' do
expect(Rating::Rating.where(resource: article).count).to eq 1 expect(Rating::Rating.where(resource: article_1).count).to eq 2
user_1.destroy! user_1.destroy!
expect(Rating::Rating.where(resource: article).count).to eq 1 expect(Rating::Rating.where(resource: article_1).count).to eq 2
end end
end end
context 'when destroy resource' do context 'when destroy resource' do
it 'destroys resource rating too' do it 'destroys resource rating too' do
expect(Rating::Rating.where(resource: article).count).to eq 1 expect(Rating::Rating.where(resource: article_1).count).to eq 2
article.destroy! article_1.destroy!
expect(Rating::Rating.where(resource: article).count).to eq 0 expect(Rating::Rating.where(resource: article_1).count).to eq 0
end end
end end
end end

View File

@ -6,59 +6,126 @@ RSpec.describe Rating::Rate, ':create' do
let!(:user) { create :user } let!(:user) { create :user }
let!(:article) { create :article } let!(:article) { create :article }
before { create :rating_rate, author: user, resource: article, value: 3 } context 'with no scopeable' do
before { create :rating_rate, author: user, resource: article, value: 3 }
context 'when rate does not exist yet' do context 'when rate does not exist yet' do
it 'creates a rate entry' do it 'creates a rate entry' do
rate = described_class.last rate = described_class.last
expect(rate.author).to eq user expect(rate.author).to eq user
expect(rate.resource).to eq article expect(rate.resource).to eq article
expect(rate.value).to eq 3 expect(rate.value).to eq 3
end
it 'creates a rating entry' do
rating = Rating::Rating.last
expect(rating.average).to eq 3
expect(rating.estimate).to eq 3
expect(rating.resource).to eq article
expect(rating.sum).to eq 3
expect(rating.total).to eq 1
end
end end
it 'creates a rating entry' do context 'when rate already exists' do
rating = Rating::Rating.last let!(:user_2) { create :user }
expect(rating.average).to eq 3 before { create :rating_rate, author: user_2, resource: article, value: 4 }
expect(rating.estimate).to eq 3
expect(rating.resource).to eq article it 'creates one more rate entry' do
expect(rating.sum).to eq 3 rates = described_class.where(author: [user, user_2]).order('created_at asc')
expect(rating.total).to eq 1
expect(rates.size).to eq 2
rate = rates[0]
expect(rate.author).to eq user
expect(rate.resource).to eq article
expect(rate.value).to eq 3
rate = rates[1]
expect(rate.author).to eq user_2
expect(rate.resource).to eq article
expect(rate.value).to eq 4
end
it 'updates the unique rating entry' do
rating = Rating::Rating.find_by(resource: article)
expect(rating.average).to eq 3.5
expect(rating.estimate).to eq 3.5
expect(rating.resource).to eq article
expect(rating.sum).to eq 7
expect(rating.total).to eq 2
end
end end
end end
context 'when rate already exists' do context 'with scopeable' do
let!(:user_2) { create :user } let!(:category) { create :category }
before { create :rating_rate, author: user_2, resource: article, value: 4 } before { create :rating_rate, author: user, resource: article, scopeable: category, value: 3 }
it 'creates one more rate entry' do context 'when rate does not exist yet' do
rates = described_class.where(author: [user, user_2]).order('created_at asc') it 'creates a rate entry' do
rate = described_class.last
expect(rates.size).to eq 2 expect(rate.author).to eq user
expect(rate.resource).to eq article
expect(rate.scopeable).to eq category
expect(rate.value).to eq 3
end
rate = rates[0] it 'creates a rating entry' do
rating = Rating::Rating.last
expect(rate.author).to eq user expect(rating.average).to eq 3
expect(rate.resource).to eq article expect(rating.estimate).to eq 3
expect(rate.value).to eq 3 expect(rating.resource).to eq article
expect(rating.scopeable).to eq category
rate = rates[1] expect(rating.sum).to eq 3
expect(rating.total).to eq 1
expect(rate.author).to eq user_2 end
expect(rate.resource).to eq article
expect(rate.value).to eq 4
end end
it 'updates the unique rating entry' do context 'when rate already exists' do
rating = Rating::Rating.find_by(resource: article) let!(:user_2) { create :user }
expect(rating.average).to eq 3.5 before { create :rating_rate, author: user_2, resource: article, scopeable: category, value: 4 }
expect(rating.estimate).to eq 3.5
expect(rating.resource).to eq article it 'creates one more rate entry' do
expect(rating.sum).to eq 7 rates = described_class.where(author: [user, user_2]).order('created_at asc')
expect(rating.total).to eq 2
expect(rates.size).to eq 2
rate = rates[0]
expect(rate.author).to eq user
expect(rate.resource).to eq article
expect(rate.scopeable).to eq category
expect(rate.value).to eq 3
rate = rates[1]
expect(rate.author).to eq user_2
expect(rate.resource).to eq article
expect(rate.scopeable).to eq category
expect(rate.value).to eq 4
end
it 'updates the unique rating entry' do
rating = Rating::Rating.find_by(resource: article, scopeable: category)
expect(rating.average).to eq 3.5
expect(rating.estimate).to eq 3.5
expect(rating.resource).to eq article
expect(rating.scopeable).to eq category
expect(rating.sum).to eq 7
expect(rating.total).to eq 2
end
end end
end end
end end

View File

@ -6,15 +6,35 @@ RSpec.describe Rating::Rate, ':rate_for' do
let!(:user) { create :user } let!(:user) { create :user }
let!(:article) { create :article } let!(:article) { create :article }
context 'when rate does not exist' do context 'with no scopeable' do
specify { expect(described_class.rate_for(author: user, resource: article)).to eq nil } context 'when rate does not exist' do
specify { expect(described_class.rate_for(author: user, resource: article)).to eq nil }
end
context 'when rate does not exist' do
before { described_class.create author: user, resource: article, value: 3 }
it 'returns the record' do
expect(described_class.rate_for(author: user, resource: article)).to eq described_class.last
end
end
end end
context 'when rate does not exist' do context 'with scopeable' do
before { described_class.create author: user, resource: article, value: 3 } let!(:category) { create :category }
it 'returns the record' do context 'when rate does not exist' do
expect(described_class.rate_for(author: user, resource: article)).to eq described_class.last specify { expect(described_class.rate_for(author: user, resource: article, scopeable: category)).to eq nil }
end
context 'when rate does not exist' do
before { described_class.create author: user, resource: article, scopeable: category, value: 3 }
it 'returns the record' do
query = described_class.rate_for(author: user, resource: article, scopeable: category)
expect(query).to eq described_class.last
end
end end
end end
end end

View File

@ -9,6 +9,7 @@ RSpec.describe Rating::Rate do
it { is_expected.to belong_to :author } it { is_expected.to belong_to :author }
it { is_expected.to belong_to :resource } it { is_expected.to belong_to :resource }
it { is_expected.to belong_to :scopeable }
it { is_expected.to validate_presence_of :author } it { is_expected.to validate_presence_of :author }
it { is_expected.to validate_presence_of :resource } it { is_expected.to validate_presence_of :resource }
@ -20,7 +21,7 @@ RSpec.describe Rating::Rate do
specify do specify do
expect(object).to validate_uniqueness_of(:author_id) expect(object).to validate_uniqueness_of(:author_id)
.scoped_to(%i[author_type resource_id resource_type]) .scoped_to(%i[author_type resource_id resource_type scopeable_id scopeable_type])
.case_insensitive .case_insensitive
end end
end end

View File

@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Rating, ':averager_data' do RSpec.describe Rating::Rating, ':averager_data' do
subject { described_class.averager_data article_1 } let!(:category) { create :category }
let!(:user_1) { create :user } let!(:user_1) { create :user }
let!(:user_2) { create :user } let!(:user_2) { create :user }
@ -17,13 +17,32 @@ RSpec.describe Rating::Rating, ':averager_data' do
create :rating_rate, author: user_1, resource: article_2, value: 11 create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10 create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1 create :rating_rate, author: user_2, resource: article_1, value: 1
create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1
create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2
end end
it 'returns the values average of given resource type' do context 'with no scopeable' do
expect(subject.as_json['rating_avg']).to eq 30.5 subject { described_class.averager_data article_1, nil }
it 'returns the values average of given resource type' do
expect(subject.as_json['rating_avg']).to eq 30.5
end
it 'returns the average of number of records for the given resource type' do
expect(subject.as_json['count_avg']).to eq 1.3333333333333333
end
end end
it 'returns the average of number of records for the given resource type' do context 'with scopeable' do
expect(subject.as_json['count_avg']).to eq 1.3333333333333333 subject { described_class.averager_data article_1, category }
it 'returns the values average of given resource type' do
expect(subject.as_json['rating_avg']).to eq 1.5
end
it 'returns the average of number of records for the given resource type' do
expect(subject.as_json['count_avg']).to eq 2
end
end end
end end

View File

@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Rating, ':data' do RSpec.describe Rating::Rating, ':data' do
subject { described_class.data article_1 } let!(:category) { create :category }
let!(:user_1) { create :user } let!(:user_1) { create :user }
let!(:user_2) { create :user } let!(:user_2) { create :user }
@ -17,21 +17,48 @@ RSpec.describe Rating::Rating, ':data' do
create :rating_rate, author: user_1, resource: article_2, value: 11 create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10 create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1 create :rating_rate, author: user_2, resource: article_1, value: 1
create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1
create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2
end end
it 'returns the average of value for a resource' do context 'with no scopeable' do
expect(subject[:average]).to eq 50.5 subject { described_class.data article_1, nil }
it 'returns the average of value for a resource' do
expect(subject[:average]).to eq 50.5
end
it 'returns the sum of values for a resource' do
expect(subject[:sum]).to eq 101
end
it 'returns the count of votes for a resource' do
expect(subject[:total]).to eq 2
end
it 'returns the estimate for a resource' do
expect(subject[:estimate]).to eq 42.50000000000001
end
end end
it 'returns the sum of values for a resource' do context 'with scopeable' do
expect(subject[:sum]).to eq 101 subject { described_class.data article_1, category }
end
it 'returns the count of votes for a resource' do it 'returns the average of value for a resource' do
expect(subject[:total]).to eq 2 expect(subject[:average]).to eq 1.5
end end
it 'returns the estimate for a resource' do it 'returns the sum of values for a resource' do
expect(subject[:estimate]).to eq 42.50000000000001 expect(subject[:sum]).to eq 3
end
it 'returns the count of votes for a resource' do
expect(subject[:total]).to eq 2
end
it 'returns the estimate for a resource' do
expect(subject[:estimate]).to eq 1.5
end
end end
end end

View File

@ -3,6 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Rating, ':update_rating' do RSpec.describe Rating::Rating, ':update_rating' do
let!(:category) { create :category }
let!(:user_1) { create :user } let!(:user_1) { create :user }
let!(:user_2) { create :user } let!(:user_2) { create :user }
@ -15,14 +17,30 @@ RSpec.describe Rating::Rating, ':update_rating' do
create :rating_rate, author: user_1, resource: article_2, value: 11 create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10 create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1 create :rating_rate, author: user_2, resource: article_1, value: 1
create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1
create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2
end end
it 'updates the rating data of the given resource' do context 'with no scopeable' do
record = described_class.find_by(resource: article_1) it 'updates the rating data of the given resource' do
record = described_class.find_by(resource: article_1)
expect(record.average).to eq 50.50000000000001 expect(record.average).to eq 50.50000000000001
expect(record.estimate).to eq 42.50000000000001 expect(record.estimate).to eq 42.50000000000001
expect(record.sum).to eq 101 expect(record.sum).to eq 101
expect(record.total).to eq 2 expect(record.total).to eq 2
end
end
context 'with scopeable' do
it 'updates the rating data of the given resource respecting the scope' do
record = described_class.find_by(resource: article_1, scopeable: category)
expect(record.average).to eq 1.5
expect(record.estimate).to eq 1.5
expect(record.sum).to eq 3
expect(record.total).to eq 2
end
end end
end end

View File

@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Rating::Rating, ':values_data' do RSpec.describe Rating::Rating, ':values_data' do
subject { described_class.values_data article_1 } let!(:category) { create :category }
let!(:user_1) { create :user } let!(:user_1) { create :user }
let!(:user_2) { create :user } let!(:user_2) { create :user }
@ -17,17 +17,40 @@ RSpec.describe Rating::Rating, ':values_data' do
create :rating_rate, author: user_1, resource: article_2, value: 11 create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10 create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 4 create :rating_rate, author: user_2, resource: article_1, value: 4
create :rating_rate, author: user_1, resource: article_1, scopeable: category, value: 1
create :rating_rate, author: user_2, resource: article_1, scopeable: category, value: 2
end end
it 'returns the average of value for a resource' do context 'with no scopeable' do
expect(subject.as_json['rating_avg']).to eq 52.0 subject { described_class.values_data article_1, nil }
it 'returns the average of value for a resource' do
expect(subject.as_json['rating_avg']).to eq 52.0
end
it 'returns the sum of values for a resource' do
expect(subject.as_json['rating_sum']).to eq 104
end
it 'returns the count of votes for a resource' do
expect(subject.as_json['rating_count']).to eq 2
end
end end
it 'returns the sum of values for a resource' do context 'with scopeable' do
expect(subject.as_json['rating_sum']).to eq 104 subject { described_class.values_data article_1, category }
end
it 'returns the count of votes for a resource' do it 'returns the average of value for a resource' do
expect(subject.as_json['rating_count']).to eq 2 expect(subject.as_json['rating_avg']).to eq 1.5
end
it 'returns the sum of values for a resource' do
expect(subject.as_json['rating_sum']).to eq 3
end
it 'returns the count of votes for a resource' do
expect(subject.as_json['rating_count']).to eq 2
end
end end
end end

View File

@ -8,6 +8,7 @@ RSpec.describe Rating::Rating do
it { expect(object).to be_valid } it { expect(object).to be_valid }
it { is_expected.to belong_to :resource } it { is_expected.to belong_to :resource }
it { is_expected.to belong_to :scopeable }
it { is_expected.to validate_presence_of :average } it { is_expected.to validate_presence_of :average }
it { is_expected.to validate_presence_of :estimate } it { is_expected.to validate_presence_of :estimate }

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class CreateCategoriesTable < ActiveRecord::Migration[5.0]
def change
create_table :categories do |t|
t.string :name, null: false
end
end
end

View File

@ -2,6 +2,7 @@
require File.expand_path('../../lib/generators/rating/templates/db/migrate/create_rating_tables.rb', __dir__) require File.expand_path('../../lib/generators/rating/templates/db/migrate/create_rating_tables.rb', __dir__)
CreateArticlesTable.new.change
CreateCategoriesTable.new.change
CreateRatingTables.new.change CreateRatingTables.new.change
CreateUsersTable.new.change CreateUsersTable.new.change
CreateArticlesTable.new.change

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
class Category < ::ActiveRecord::Base
end