From fd954f677fe1af6746cc229fa969c05e15d053c5 Mon Sep 17 00:00:00 2001 From: Washington Botelho Date: Mon, 5 Mar 2018 20:26:50 -0300 Subject: [PATCH] feat: unscoped rating --- lib/rating/models/rating/extension.rb | 8 +- lib/rating/models/rating/rating.rb | 42 +++--- spec/factories/global.rb | 6 + spec/models/extension/unscoped_rating_spec.rb | 121 ++++++++++++++++++ .../db/migrate/create_categories_table.rb | 1 + .../db/migrate/create_globals_table.rb | 8 ++ spec/support/migrate.rb | 3 + spec/support/models/category.rb | 1 + spec/support/models/global.rb | 7 + 9 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 spec/factories/global.rb create mode 100644 spec/models/extension/unscoped_rating_spec.rb create mode 100644 spec/support/db/migrate/create_globals_table.rb create mode 100644 spec/support/models/global.rb diff --git a/lib/rating/models/rating/extension.rb b/lib/rating/models/rating/extension.rb index 0c7aee1..95bad21 100644 --- a/lib/rating/models/rating/extension.rb +++ b/lib/rating/models/rating/extension.rb @@ -55,8 +55,8 @@ module Rating end module ClassMethods - def rating(as: nil, scoping: nil) - after_create -> { rating_warm_up scoping: scoping }, unless: -> { as == :author } + def rating(options = {}) + after_create -> { rating_warm_up scoping: options[:scoping] }, unless: -> { options[:as] == :author } has_many :rating_records, as: :resource, @@ -78,6 +78,10 @@ module Rating .where(Rating.table_name => { scopeable_id: scope&.id, scopeable_type: scope&.class&.base_class&.name }) .order("#{Rating.table_name}.#{column} #{direction}") } + + define_method :rating_options do + options + end end end end diff --git a/lib/rating/models/rating/rating.rb b/lib/rating/models/rating/rating.rb index 35c3c55..f727221 100644 --- a/lib/rating/models/rating/rating.rb +++ b/lib/rating/models/rating/rating.rb @@ -18,8 +18,8 @@ module Rating class << self 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) + total_count = how_many_resource_received_votes_sql(distinct: false, resource: resource, scopeable: scopeable) + distinct_count = how_many_resource_received_votes_sql(distinct: true, resource: resource, scopeable: scopeable) values = { resource_type: resource.class.base_class.name } values[:scopeable_type] = scopeable.class.base_class.name unless scopeable.nil? @@ -29,7 +29,7 @@ module Rating (CAST(#{total_count} AS DECIMAL(17, 14)) / #{distinct_count}) count_avg, COALESCE(AVG(value), 0) rating_avg FROM #{rate_table_name} - WHERE resource_type = :resource_type AND #{scope_type_query(scopeable)} + WHERE resource_type = :resource_type #{scope_type_query(resource, scopeable)} ).squish execute_sql [sql, values] @@ -48,29 +48,26 @@ module Rating end 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 = ? AND #{scope_query} + WHERE resource_type = ? AND resource_id = ? #{scope_type_and_id_query(resource, scopeable)} ).squish values = [sql, resource.class.base_class.name, resource.id] - values += [scopeable.class.base_class.name, scopeable.id] unless scopeable.nil? + values += [scopeable.class.base_class.name, scopeable.id] unless scopeable.nil? || unscoped_rating?(resource) execute_sql values end def update_rating(resource, scopeable) - record = find_or_initialize_by(resource: resource, scopeable: scopeable) + attributes = { resource: resource } + attributes[:scopeable] = unscoped_rating?(resource) ? nil : scopeable + + record = find_or_initialize_by(attributes) result = data(resource, scopeable) record.average = result[:average] @@ -97,13 +94,17 @@ module Rating Rate.find_by_sql(sql).first end - def how_many_resource_received_votes_sql?(distinct:, scopeable:) + def unscoped_rating?(resource) + resource.rating_options[:unscoped_rating] + end + + def how_many_resource_received_votes_sql(distinct:, resource:, scopeable:) count = distinct ? 'COUNT(DISTINCT resource_id)' : 'COUNT(1)' %(( SELECT GREATEST(#{count}, 1) FROM #{rate_table_name} - WHERE resource_type = :resource_type AND #{scope_type_query(scopeable)} + WHERE resource_type = :resource_type #{scope_type_query(resource, scopeable)} )) end @@ -111,8 +112,17 @@ module Rating @rate_table_name ||= Rate.table_name end - def scope_type_query(scopeable) - scopeable.nil? ? 'scopeable_type is NULL' : 'scopeable_type = :scopeable_type' + def scope_type_query(resource, scopeable) + return '' if unscoped_rating?(resource) + + scopeable.nil? ? 'AND scopeable_type is NULL' : 'AND scopeable_type = :scopeable_type' + end + + def scope_type_and_id_query(resource, scopeable) + return '' if unscoped_rating?(resource) + return 'AND scopeable_type is NULL AND scopeable_id is NULL' if scopeable.nil? + + 'AND scopeable_type = ? AND scopeable_id = ?' end end end diff --git a/spec/factories/global.rb b/spec/factories/global.rb new file mode 100644 index 0000000..dbac56f --- /dev/null +++ b/spec/factories/global.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :global do + end +end diff --git a/spec/models/extension/unscoped_rating_spec.rb b/spec/models/extension/unscoped_rating_spec.rb new file mode 100644 index 0000000..4eceeb7 --- /dev/null +++ b/spec/models/extension/unscoped_rating_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Rating::Extension, 'unscoped_rating' do + let!(:author_1) { create :author } + let!(:author_2) { create :author } + let!(:scope) { create :category } + + context 'when is false' do + let!(:resource) { create :article } + + it 'groups in different line record' do + author_1.rate resource, 1, scope: scope + author_2.rate resource, 2, scope: scope + author_1.rate resource, 5 + + ratings = Rating::Rating.all.order('id') + + expect(ratings.size).to eq 2 + + rating = ratings[0] + + expect(rating.average.to_s).to eq '1.5' + expect(rating.estimate.to_s).to eq '1.5' + expect(rating.resource).to eq resource + expect(rating.scopeable).to eq scope + expect(rating.sum).to eq 3 + expect(rating.total).to eq 2 + + rating = ratings[1] + + expect(rating.average.to_s).to eq '5.0' + expect(rating.estimate.to_s).to eq '5.0' + expect(rating.resource).to eq resource + expect(rating.scopeable).to eq nil + expect(rating.sum).to eq 5 + expect(rating.total).to eq 1 + end + end + + context 'when is true' do + let!(:resource) { create :global } + + it 'groups in the same line record' do + author_1.rate resource, 1, scope: scope + author_2.rate resource, 2, scope: scope + author_1.rate resource, 5 + + ratings = Rating::Rating.all.order('id') + + expect(ratings.size).to eq 1 + + rating = ratings[0] + + expect(rating.average.to_s).to eq '2.6666666666666667' + expect(rating.estimate.to_s).to eq '2.6666666666666667' + expect(rating.resource).to eq resource + expect(rating.scopeable).to eq nil + expect(rating.sum).to eq 8 + expect(rating.total).to eq 3 + end + end + + context 'when is true' do + let!(:resource) { create :global } + + it 'groups in the same line record' do + author_1.rate resource, 1, scope: scope + author_2.rate resource, 2, scope: scope + author_1.rate resource, 5 + + ratings = Rating::Rating.all.order('id') + + expect(ratings.size).to eq 1 + + rating = ratings[0] + + expect(rating.average.to_s).to eq '2.6666666666666667' + expect(rating.estimate.to_s).to eq '2.6666666666666667' + expect(rating.resource).to eq resource + expect(rating.scopeable).to eq nil + expect(rating.sum).to eq 8 + expect(rating.total).to eq 3 + end + end + + context 'when is true and have a non scopeable record first on dabase' do + let!(:resource) { create :global } + + before { ::Rating::Rating.create resource: resource, scopeable: scope } + + it 'groups in the line with no scope' do + author_1.rate resource, 1, scope: scope + author_2.rate resource, 2, scope: scope + author_1.rate resource, 5 + + ratings = Rating::Rating.all.order('id') + + expect(ratings.size).to eq 2 + + rating = ratings[0] + + expect(rating.average.to_s).to eq '0.0' + expect(rating.estimate.to_s).to eq '0.0' + expect(rating.resource).to eq resource + expect(rating.scopeable).to eq scope + expect(rating.sum).to eq 0 + expect(rating.total).to eq 0 + + rating = ratings[1] + + expect(rating.average.to_s).to eq '2.6666666666666667' + expect(rating.estimate.to_s).to eq '2.6666666666666667' + expect(rating.resource).to eq resource + expect(rating.scopeable).to eq nil + expect(rating.sum).to eq 8 + expect(rating.total).to eq 3 + end + end +end diff --git a/spec/support/db/migrate/create_categories_table.rb b/spec/support/db/migrate/create_categories_table.rb index 5b6760c..add2e9f 100644 --- a/spec/support/db/migrate/create_categories_table.rb +++ b/spec/support/db/migrate/create_categories_table.rb @@ -6,6 +6,7 @@ class CreateCategoriesTable < ActiveRecord::Migration[5.0] t.string :name, null: false t.references :article, foreign_key: true, index: true + t.references :global, foreign_key: true, index: true end end end diff --git a/spec/support/db/migrate/create_globals_table.rb b/spec/support/db/migrate/create_globals_table.rb new file mode 100644 index 0000000..ed022e4 --- /dev/null +++ b/spec/support/db/migrate/create_globals_table.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class CreateGlobalsTable < ActiveRecord::Migration[5.0] + def change + create_table :globals do |t| + end + end +end diff --git a/spec/support/migrate.rb b/spec/support/migrate.rb index 18377a6..be92c8d 100644 --- a/spec/support/migrate.rb +++ b/spec/support/migrate.rb @@ -7,7 +7,10 @@ Dir[File.expand_path('db/migrate/*.rb', __dir__)].each { |file| require file } CreateArticlesTable.new.change CreateAuthorsTable.new.change + +CreateGlobalsTable.new.change CreateCategoriesTable.new.change + CreateCommentsTable.new.change CreateRateTable.new.change CreateRatingTable.new.change diff --git a/spec/support/models/category.rb b/spec/support/models/category.rb index 8230753..3189281 100644 --- a/spec/support/models/category.rb +++ b/spec/support/models/category.rb @@ -2,4 +2,5 @@ class Category < ::ActiveRecord::Base belongs_to :article + belongs_to :global end diff --git a/spec/support/models/global.rb b/spec/support/models/global.rb new file mode 100644 index 0000000..0f48fce --- /dev/null +++ b/spec/support/models/global.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Global < ::ActiveRecord::Base + rating scoping: :categories, unscoped_rating: true + + has_many :categories +end