wbotelhos/rating

View on GitHub
lib/rating/models/rating/rating.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Rating
  class Rating < ActiveRecord::Base
    self.table_name_prefix = 'rating_'
    self.table_name        = ::Rating::Config.rating_table

    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

    validates :resource_id, uniqueness: {
      case_sensitive: false,
      scope:          %i[resource_type scopeable_id scopeable_type],
    }

    class << self
      def averager_data(resource, 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?

        sql = %(
          SELECT
            CAST(#{total_count} / #{distinct_count} AS DECIMAL(25, 16)) count_avg,
            COALESCE(AVG(value), 0) rating_avg
          FROM #{rate_table_name}
          WHERE
            resource_type = :resource_type
            #{scope_type_query(resource, scopeable)}
            #{scope_where_query(resource)}
        ).squish

        execute_sql [sql, values]
      end

      def data(resource, scopeable)
        averager = averager_data(resource, scopeable)
        values   = values_data(resource, scopeable)

        {
          average:  values.rating_avg.round(2),
          estimate: estimate(averager, values).round(2),
          sum:      values.rating_sum,
          total:    values.rating_count,
        }
      end

      def values_data(resource, scopeable)
        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 = ?
            #{scope_type_and_id_query(resource, scopeable)}
            #{scope_where_query(resource)}
        ).squish

        values =  [sql, resource.class.base_class.name, resource.id]
        values += [scopeable.class.base_class.name, scopeable.id] unless scopeable.nil? || unscoped_rating?(resource)

        execute_sql values
      end

      def update_rating(resource, 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]
        record.sum      = result[:sum]
        record.total    = result[:total]
        record.estimate = result[:estimate]

        record.save
      end

      private

      def estimate(averager, values)
        resource_type_rating_avg = averager.rating_avg
        count_avg                = averager.count_avg
        resource_rating_avg      = values.rating_avg
        resource_rating_count    = values.rating_count.to_f

        ((resource_rating_count / (resource_rating_count + count_avg)) * resource_rating_avg) +
          ((count_avg           / (resource_rating_count + count_avg)) * resource_type_rating_avg)
      end

      def execute_sql(sql)
        Rate.find_by_sql(sql).first
      end

      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
            #{scope_type_query(resource, scopeable)}
            #{scope_where_query(resource)}
        ))
      end

      def rate_table_name
        @rate_table_name ||= Rate.table_name
      end

      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

      def scope_where_query(resource)
        return '' if where_condition(resource).blank?

        "AND #{where_condition(resource)}"
      end

      def where_condition(resource)
        resource.rating_options[:where]
      end
    end
  end
end