diff --git a/README.md b/README.md index ab5a89a..90059ef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![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) -A true Bayesian rating system with cache enabled. +A true Bayesian rating system with scope and cache enabled. ## JS Rating? @@ -71,7 +71,7 @@ class User < ApplicationRecord end ``` -Now this model can vote or be voted. +Now this model can vote or receive votes. ### rate @@ -175,6 +175,103 @@ Article.order_by_rating :average, :asc 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! Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Gratipay](https://gratipay.com/rating). Thanks! (: diff --git a/lib/generators/rating/templates/db/migrate/create_rating_tables.rb b/lib/generators/rating/templates/db/migrate/create_rating_tables.rb index 48b3dec..c38e582 100644 --- a/lib/generators/rating/templates/db/migrate/create_rating_tables.rb +++ b/lib/generators/rating/templates/db/migrate/create_rating_tables.rb @@ -5,14 +5,15 @@ class CreateRatingTables < ActiveRecord::Migration[5.0] create_table :rating_rates do |t| t.decimal :value, default: 0, precision: 17, scale: 14 - t.references :author , index: true, null: false, polymorphic: true - t.references :resource, 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 :scopeable, index: true, null: true , polymorphic: true t.timestamps null: false end - add_index :rating_rates, %i[author_id author_type resource_id resource_type], - name: :index_rating_rates_on_author_and_resource, + 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_and_scopeable, unique: true 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 :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 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 diff --git a/lib/rating/models/rating/extension.rb b/lib/rating/models/rating/extension.rb index 07a2e85..6788293 100644 --- a/lib/rating/models/rating/extension.rb +++ b/lib/rating/models/rating/extension.rb @@ -5,16 +5,28 @@ module Rating extend ActiveSupport::Concern included do - def rate(resource, value, author: self) - Rate.create author: author, resource: resource, value: value + def rate(resource, value, author: self, scope: nil) + Rate.create author: author, resource: resource, scopeable: scope, value: value end - def rate_for(resource) - Rate.rate_for author: self, resource: resource + def rate_for(resource, scope: nil) + Rate.rate_for author: self, resource: resource, scopeable: scope end - def rated?(resource) - !rate_for(resource).nil? + def rated?(resource, scope: 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 @@ -22,23 +34,30 @@ module Rating def rating after_create { Rating.find_or_create_by resource: self } - has_one :rating, + has_many :rating_records, as: :resource, class_name: '::Rating::Rating', dependent: :destroy - has_many :rates, + has_many :rates_records, as: :resource, class_name: '::Rating::Rate', dependent: :destroy - has_many :rated, + has_many :rated_records, as: :author, class_name: '::Rating::Rate', dependent: :destroy - scope :order_by_rating, ->(column = :estimate, direction = :desc) { - includes(:rating).order("#{Rating.table_name}.#{column} #{direction}") + scope :order_by_rating, ->(column = :estimate, direction = :desc, scope: nil) { + 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 diff --git a/lib/rating/models/rating/rate.rb b/lib/rating/models/rating/rate.rb index 7ca79ab..ab6a439 100644 --- a/lib/rating/models/rating/rate.rb +++ b/lib/rating/models/rating/rate.rb @@ -6,19 +6,20 @@ module Rating after_save :update_rating - belongs_to :author , polymorphic: true - belongs_to :resource, polymorphic: true + belongs_to :author , polymorphic: true + belongs_to :resource , polymorphic: true + belongs_to :scopeable, polymorphic: true validates :author, :resource, :value, presence: true validates :value, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 } validates :author_id, uniqueness: { 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:) - record = find_or_initialize_by(author: author, resource: resource) + def self.create(author:, resource:, scopeable: nil, value:) + record = find_or_initialize_by(author: author, resource: resource, scopeable: scopeable) return record if record.persisted? && value == record.value @@ -28,14 +29,14 @@ module Rating record end - def self.rate_for(author:, resource:) - find_by author: author, resource: resource + def self.rate_for(author:, resource:, scopeable: nil) + find_by author: author, resource: resource, scopeable: scopeable end private def update_rating - ::Rating::Rating.update_rating resource + ::Rating::Rating.update_rating resource, scopeable end end end diff --git a/lib/rating/models/rating/rating.rb b/lib/rating/models/rating/rating.rb index b07a1dc..439ec9d 100644 --- a/lib/rating/models/rating/rating.rb +++ b/lib/rating/models/rating/rating.rb @@ -4,30 +4,34 @@ module Rating class Rating < ActiveRecord::Base 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, :sum, :total, numericality: true class << self - def averager_data(resource) - total_count = how_many_resource_received_votes_sql? - distinct_count = how_many_resource_received_votes_sql?(distinct: true) + def averager_data(resource, scopeable) + total_count = how_many_resource_received_votes_sql?(distinct: false, scopeable: scopeable) + 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 = %( SELECT (#{total_count} / CAST(#{distinct_count} AS float)) count_avg, COALESCE(AVG(value), 0) rating_avg FROM #{rate_table_name} - WHERE resource_type = :resource_type + WHERE resource_type = :resource_type and #{scope_type_query(scopeable)} ).squish - execute_sql [sql, resource_type: resource.class.base_class.name] + execute_sql [sql, values] end - def data(resource) - averager = averager_data(resource) - values = values_data(resource) + def data(resource, scopeable) + averager = averager_data(resource, scopeable) + values = values_data(resource, scopeable) { average: values.rating_avg, @@ -37,22 +41,31 @@ module Rating } 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 = %( SELECT COALESCE(AVG(value), 0) rating_avg, COALESCE(SUM(value), 0) rating_sum, COUNT(1) rating_count FROM #{rate_table_name} - WHERE resource_type = ? and resource_id = ? + WHERE resource_type = ? and resource_id = ? and #{scope_query} ).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 - def update_rating(resource) - record = find_or_initialize_by(resource: resource) - result = data(resource) + def update_rating(resource, scopeable) + record = find_or_initialize_by(resource: resource, scopeable: scopeable) + result = data(resource, scopeable) record.average = result[:average] record.sum = result[:sum] @@ -78,19 +91,23 @@ module Rating Rate.find_by_sql(sql).first 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)' %(( SELECT #{count} FROM #{rate_table_name} - WHERE resource_type = :resource_type + WHERE resource_type = :resource_type and #{scope_type_query(scopeable)} )) end def rate_table_name @rate_table_name ||= Rate.table_name end + + def scope_type_query(scopeable) + scopeable.nil? ? 'scopeable_type is NULL' : 'scopeable_type = :scopeable_type' + end end end end diff --git a/rating.gemspec b/rating.gemspec index e2e33ef..1d254f5 100644 --- a/rating.gemspec +++ b/rating.gemspec @@ -8,14 +8,14 @@ require 'rating/version' Gem::Specification.new do |spec| 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.files = Dir['lib/**/*'] + %w[CHANGELOG.md LICENSE README.md] spec.homepage = 'https://github.com/wbotelhos/rating' spec.license = 'MIT' spec.name = 'rating' 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.version = Rating::VERSION diff --git a/spec/factories/article.rb b/spec/factories/article.rb index 7c18c71..f44dfcc 100644 --- a/spec/factories/article.rb +++ b/spec/factories/article.rb @@ -2,6 +2,6 @@ FactoryBot.define do factory :article do - sequence(:name) { |i| "Name #{i}" } + sequence(:name) { |i| "Article #{i}" } end end diff --git a/spec/factories/category.rb b/spec/factories/category.rb new file mode 100644 index 0000000..ab1e615 --- /dev/null +++ b/spec/factories/category.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :category do + sequence(:name) { |i| "Category #{i}" } + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb index d6e337f..347d708 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -2,6 +2,6 @@ FactoryBot.define do factory :user do - sequence(:name) { |i| "Name #{i}" } + sequence(:name) { |i| "User #{i}" } end end diff --git a/spec/models/extension/after_create_spec.rb b/spec/models/extension/after_create_spec.rb index 6c68ce0..0ba8ff0 100644 --- a/spec/models/extension/after_create_spec.rb +++ b/spec/models/extension/after_create_spec.rb @@ -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 rating = Rating::Rating.find_by(resource: user) - expect(rating.average).to eq 0 - expect(rating.estimate).to eq 0 - expect(rating.resource).to eq user - expect(rating.sum).to eq 0 - expect(rating.total).to eq 0 + expect(rating.average).to eq 0 + expect(rating.estimate).to eq 0 + expect(rating.resource).to eq user + expect(rating.scopeable).to eq nil + expect(rating.sum).to eq 0 + expect(rating.total).to eq 0 end end end diff --git a/spec/models/extension/order_by_rating_spec.rb b/spec/models/extension/order_by_rating_spec.rb index 7f6b450..a265b82 100644 --- a/spec/models/extension/order_by_rating_spec.rb +++ b/spec/models/extension/order_by_rating_spec.rb @@ -3,6 +3,8 @@ require 'rails_helper' RSpec.describe Rating::Extension, ':order_by_rating' do + let!(:category) { create :category } + let!(:user_1) { 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_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 context 'with default filters' do @@ -36,6 +41,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do article_1 ] end + + context 'with scope' do + it 'works' do + expect(Article.order_by_rating(:average, :asc, scope: category)).to eq [ + article_1 + ] + end + end end context 'as desc' do @@ -46,6 +59,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do article_3 ] 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 @@ -58,6 +79,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do article_1 ] end + + context 'with scope' do + it 'works' do + expect(Article.order_by_rating(:estimate, :asc, scope: category)).to eq [ + article_1 + ] + end + end end context 'as desc' do @@ -68,6 +97,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do article_3 ] 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 @@ -80,6 +117,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do article_1 ] end + + context 'with scope' do + it 'works' do + expect(Article.order_by_rating(:sum, :asc, scope: category)).to eq [ + article_1 + ] + end + end end context 'as desc' do @@ -90,6 +135,14 @@ RSpec.describe Rating::Extension, ':order_by_rating' do article_3 ] end + + context 'with scope' do + it 'works' do + expect(Article.order_by_rating(:sum, :desc, scope: category)).to eq [ + article_1 + ] + end + end end 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.last).to eq article_1 end + + context 'with scope' do + it 'works' do + expect(Article.order_by_rating(:total, :asc, scope: category)).to eq [ + article_1 + ] + end + end end 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[1..2]).to match_array [article_2, article_3] 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 @@ -117,5 +186,11 @@ RSpec.describe Rating::Extension, ':order_by_rating' do it 'works' do expect(User.order_by_rating(:total, :desc)).to match_array [user_1, user_2] 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 diff --git a/spec/models/extension/rate_for_spec.rb b/spec/models/extension/rate_for_spec.rb index daa4bd0..c90bdd2 100644 --- a/spec/models/extension/rate_for_spec.rb +++ b/spec/models/extension/rate_for_spec.rb @@ -6,9 +6,21 @@ RSpec.describe Rating::Extension, ':rate_for' do let!(:user) { create :user } let!(:article) { create :article } - it 'delegates to rate object' do - expect(Rating::Rate).to receive(:rate_for).with author: user, resource: article + context 'with no scopeable' do + 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 diff --git a/spec/models/extension/rate_spec.rb b/spec/models/extension/rate_spec.rb index ea4b9a8..4de2241 100644 --- a/spec/models/extension/rate_spec.rb +++ b/spec/models/extension/rate_spec.rb @@ -6,9 +6,21 @@ RSpec.describe Rating::Extension, ':rate' do let!(:user) { create :user } let!(:article) { create :article } - it 'delegates to rate object' do - expect(Rating::Rate).to receive(:create).with author: user, resource: article, value: 3 + context 'with no scopeable' do + 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 diff --git a/spec/models/extension/rated_question_spec.rb b/spec/models/extension/rated_question_spec.rb index 29f84f2..02b3f61 100644 --- a/spec/models/extension/rated_question_spec.rb +++ b/spec/models/extension/rated_question_spec.rb @@ -6,15 +6,33 @@ RSpec.describe Rating::Extension, ':rated?' do let!(:user) { create :user } let!(:article) { create :article } - context 'when has no rate for the given resource' do - before { allow(user).to receive(:rate_for).with(article) { nil } } + context 'with no scopeable' do + 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 - context 'when has rate for the given resource' do - before { allow(user).to receive(:rate_for).with(article) { double } } + context 'with scopeable' do + 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 diff --git a/spec/models/extension/rated_records_spec.rb b/spec/models/extension/rated_records_spec.rb new file mode 100644 index 0000000..d18e718 --- /dev/null +++ b/spec/models/extension/rated_records_spec.rb @@ -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 diff --git a/spec/models/extension/rated_spec.rb b/spec/models/extension/rated_spec.rb index b20e91b..a041097 100644 --- a/spec/models/extension/rated_spec.rb +++ b/spec/models/extension/rated_spec.rb @@ -3,36 +3,56 @@ require 'rails_helper' RSpec.describe Rating::Extension, ':rated' do - let!(:user) { create :user } - let!(:article) { create :article } + let!(:category) { create :category } - before { user.rate article, 3 } + let!(:user_1) { create :user } + let!(:user_2) { create :user } - it 'returns rates made by the caller' do - expect(user.rated).to eq [Rating::Rate.find_by(resource: article)] + 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 } + + 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 context 'when destroy author' 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 - it 'destroys rates of this author' do - expect(Rating::Rate.where(author: user).count).to eq 0 + it 'destroys rates of that author' do + expect(Rating::Rate.where(author: user_1).count).to eq 0 end end context 'when destroy resource rated by author' 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 - it 'destroys rates for that resource' do - expect(Rating::Rate.where(resource: article).count).to eq 0 + it 'destroys rates of that resource' do + expect(Rating::Rate.where(resource: article_1).count).to eq 0 end end end diff --git a/spec/models/extension/rates_records_spec.rb b/spec/models/extension/rates_records_spec.rb new file mode 100644 index 0000000..c27ea6f --- /dev/null +++ b/spec/models/extension/rates_records_spec.rb @@ -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 diff --git a/spec/models/extension/rates_spec.rb b/spec/models/extension/rates_spec.rb index 1f3167a..4aadfcd 100644 --- a/spec/models/extension/rates_spec.rb +++ b/spec/models/extension/rates_spec.rb @@ -3,36 +3,52 @@ require 'rails_helper' RSpec.describe Rating::Extension, ':rates' do - let!(:user) { create :user } - let!(:article) { create :article } + let!(:category) { create :category } - before { user.rate article, 3 } + let!(:user_1) { create :user } + let!(:user_2) { create :user } - it 'returns rates record' do - expect(article.rates).to eq [Rating::Rate.last] + 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 } + + 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 context 'when destroy author' do - before do - expect(Rating::Rate.where(resource: article).count).to eq 1 + it 'destroys rates of that author' do + expect(Rating::Rate.where(author: user_1).count).to eq 4 - user.destroy! - end + user_1.destroy! - it 'destroys rates of that resource' do - expect(Rating::Rate.where(resource: article).count).to eq 0 + expect(Rating::Rate.where(author: user_1).count).to eq 0 end end 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 - 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 diff --git a/spec/models/extension/rating_records_spec.rb b/spec/models/extension/rating_records_spec.rb new file mode 100644 index 0000000..f110e26 --- /dev/null +++ b/spec/models/extension/rating_records_spec.rb @@ -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 diff --git a/spec/models/extension/rating_spec.rb b/spec/models/extension/rating_spec.rb index 574a257..bfc6ffe 100644 --- a/spec/models/extension/rating_spec.rb +++ b/spec/models/extension/rating_spec.rb @@ -2,37 +2,53 @@ require 'rails_helper' -RSpec.describe Rating::Extension, ':rating' do - let!(:user_1) { create :user } - let!(:article) { create :article } +RSpec.describe Rating::Extension, '.rating' do + let!(:category) { create :category } - before { user_1.rate article, 1 } + let!(:user_1) { create :user } + let!(:user_2) { create :user } - it 'returns rating record' do - expect(article.rating).to eq Rating::Rating.last + 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 } + + 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 context 'when destroy author' do - let!(:user_2) { create :user } - - before { user_2.rate article, 2 } - 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! - expect(Rating::Rating.where(resource: article).count).to eq 1 + expect(Rating::Rating.where(resource: article_1).count).to eq 2 end end context 'when destroy resource' 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 diff --git a/spec/models/rate/create_spec.rb b/spec/models/rate/create_spec.rb index ae1f7fc..49e399a 100644 --- a/spec/models/rate/create_spec.rb +++ b/spec/models/rate/create_spec.rb @@ -6,59 +6,126 @@ RSpec.describe Rating::Rate, ':create' do let!(:user) { create :user } 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 - it 'creates a rate entry' do - rate = described_class.last + context 'when rate does not exist yet' do + it 'creates a rate entry' do + rate = described_class.last - expect(rate.author).to eq user - expect(rate.resource).to eq article - expect(rate.value).to eq 3 + expect(rate.author).to eq user + expect(rate.resource).to eq article + 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 - it 'creates a rating entry' do - rating = Rating::Rating.last + context 'when rate already exists' do + let!(:user_2) { create :user } - 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 + before { create :rating_rate, author: user_2, resource: article, value: 4 } + + it 'creates one more rate entry' do + rates = described_class.where(author: [user, user_2]).order('created_at asc') + + 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 - context 'when rate already exists' do - let!(:user_2) { create :user } + context 'with scopeable' do + 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 - rates = described_class.where(author: [user, user_2]).order('created_at asc') + context 'when rate does not exist yet' do + 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(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 + expect(rating.average).to eq 3 + expect(rating.estimate).to eq 3 + expect(rating.resource).to eq article + expect(rating.scopeable).to eq category + expect(rating.sum).to eq 3 + expect(rating.total).to eq 1 + end end - it 'updates the unique rating entry' do - rating = Rating::Rating.find_by(resource: article) + context 'when rate already exists' do + let!(:user_2) { create :user } - 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 + before { create :rating_rate, author: user_2, resource: article, scopeable: category, value: 4 } + + it 'creates one more rate entry' do + rates = described_class.where(author: [user, user_2]).order('created_at asc') + + 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 diff --git a/spec/models/rate/rate_for_spec.rb b/spec/models/rate/rate_for_spec.rb index 1527421..2bcf779 100644 --- a/spec/models/rate/rate_for_spec.rb +++ b/spec/models/rate/rate_for_spec.rb @@ -6,15 +6,35 @@ RSpec.describe Rating::Rate, ':rate_for' do let!(:user) { create :user } let!(:article) { create :article } - context 'when rate does not exist' do - specify { expect(described_class.rate_for(author: user, resource: article)).to eq nil } + context 'with no scopeable' do + 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 - context 'when rate does not exist' do - before { described_class.create author: user, resource: article, value: 3 } + context 'with scopeable' do + let!(:category) { create :category } - it 'returns the record' do - expect(described_class.rate_for(author: user, resource: article)).to eq described_class.last + context 'when rate does not exist' do + 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 diff --git a/spec/models/rate_spec.rb b/spec/models/rate_spec.rb index c343615..e26d06e 100644 --- a/spec/models/rate_spec.rb +++ b/spec/models/rate_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Rating::Rate do it { is_expected.to belong_to :author } 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 :resource } @@ -20,7 +21,7 @@ RSpec.describe Rating::Rate do specify do 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 end end diff --git a/spec/models/rating/averager_data_spec.rb b/spec/models/rating/averager_data_spec.rb index 7257beb..0bc77c2 100644 --- a/spec/models/rating/averager_data_spec.rb +++ b/spec/models/rating/averager_data_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' 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_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_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 the values average of given resource type' do - expect(subject.as_json['rating_avg']).to eq 30.5 + context 'with no scopeable' do + 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 - it 'returns the average of number of records for the given resource type' do - expect(subject.as_json['count_avg']).to eq 1.3333333333333333 + context 'with scopeable' do + 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 diff --git a/spec/models/rating/data_spec.rb b/spec/models/rating/data_spec.rb index cbbedad..edacb80 100644 --- a/spec/models/rating/data_spec.rb +++ b/spec/models/rating/data_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Rating::Rating, ':data' do - subject { described_class.data article_1 } + let!(:category) { create :category } let!(:user_1) { 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_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 the average of value for a resource' do - expect(subject[:average]).to eq 50.5 + context 'with no scopeable' do + 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 - it 'returns the sum of values for a resource' do - expect(subject[:sum]).to eq 101 - end + context 'with scopeable' do + subject { described_class.data article_1, category } - it 'returns the count of votes for a resource' do - expect(subject[:total]).to eq 2 - end + it 'returns the average of value for a resource' do + expect(subject[:average]).to eq 1.5 + end - it 'returns the estimate for a resource' do - expect(subject[:estimate]).to eq 42.50000000000001 + it 'returns the sum of values for a resource' do + 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 diff --git a/spec/models/rating/update_rating_spec.rb b/spec/models/rating/update_rating_spec.rb index 416ec35..8e31981 100644 --- a/spec/models/rating/update_rating_spec.rb +++ b/spec/models/rating/update_rating_spec.rb @@ -3,6 +3,8 @@ require 'rails_helper' RSpec.describe Rating::Rating, ':update_rating' do + let!(:category) { create :category } + let!(:user_1) { 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_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 'updates the rating data of the given resource' do - record = described_class.find_by(resource: article_1) + context 'with no scopeable' do + 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.estimate).to eq 42.50000000000001 - expect(record.sum).to eq 101 - expect(record.total).to eq 2 + expect(record.average).to eq 50.50000000000001 + expect(record.estimate).to eq 42.50000000000001 + expect(record.sum).to eq 101 + 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 diff --git a/spec/models/rating/values_data_spec.rb b/spec/models/rating/values_data_spec.rb index ff42692..efe3a57 100644 --- a/spec/models/rating/values_data_spec.rb +++ b/spec/models/rating/values_data_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' 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_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_3, value: 10 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 - it 'returns the average of value for a resource' do - expect(subject.as_json['rating_avg']).to eq 52.0 + context 'with no scopeable' do + 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 - it 'returns the sum of values for a resource' do - expect(subject.as_json['rating_sum']).to eq 104 - end + context 'with scopeable' do + subject { described_class.values_data article_1, category } - it 'returns the count of votes for a resource' do - expect(subject.as_json['rating_count']).to eq 2 + it 'returns the average of value for a resource' do + 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 diff --git a/spec/models/rating_spec.rb b/spec/models/rating_spec.rb index 5fd3bf9..3976cbf 100644 --- a/spec/models/rating_spec.rb +++ b/spec/models/rating_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Rating::Rating do it { expect(object).to be_valid } 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 :estimate } diff --git a/spec/support/db/migrate/create_category_spec.rb b/spec/support/db/migrate/create_category_spec.rb new file mode 100644 index 0000000..f76519a --- /dev/null +++ b/spec/support/db/migrate/create_category_spec.rb @@ -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 diff --git a/spec/support/migrate.rb b/spec/support/migrate.rb index f768526..610fbe3 100644 --- a/spec/support/migrate.rb +++ b/spec/support/migrate.rb @@ -2,6 +2,7 @@ require File.expand_path('../../lib/generators/rating/templates/db/migrate/create_rating_tables.rb', __dir__) +CreateArticlesTable.new.change +CreateCategoriesTable.new.change CreateRatingTables.new.change CreateUsersTable.new.change -CreateArticlesTable.new.change diff --git a/spec/support/models/category.rb b/spec/support/models/category.rb new file mode 100644 index 0000000..cff22d3 --- /dev/null +++ b/spec/support/models/category.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Category < ::ActiveRecord::Base +end