feat: unscoped rating

main
Washington Botelho 2018-03-05 20:26:50 -03:00
parent 09010f5ee2
commit fd954f677f
No known key found for this signature in database
GPG Key ID: 5DE4F42A8F073617
9 changed files with 179 additions and 18 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
FactoryBot.define do
factory :global do
end
end

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class CreateGlobalsTable < ActiveRecord::Migration[5.0]
def change
create_table :globals do |t|
end
end
end

View File

@ -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

View File

@ -2,4 +2,5 @@
class Category < ::ActiveRecord::Base
belongs_to :article
belongs_to :global
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class Global < ::ActiveRecord::Base
rating scoping: :categories, unscoped_rating: true
has_many :categories
end