diff --git a/lib/acts_as_rated.rb b/lib/acts_as_rated.rb
index ed11253..172604e 100644
--- a/lib/acts_as_rated.rb
+++ b/lib/acts_as_rated.rb
@@ -3,8 +3,8 @@ module Acts #:nodoc:
# == acts_as_rated
# Adds rating capabilities to any ActiveRecord object.
- # It has the ability to work with objects that have or don't special fields to keep a tally of the
- # ratings and number of votes for each object.
+ # It has the ability to work with objects that have or don't special fields to keep a tally of the
+ # ratings and number of votes for each object.
# In addition it will by default use the User model as the rater object and keep the ratings per-user.
# It can be configured to use another class, or not use a rater at all, just keeping a global rating
#
@@ -31,7 +31,7 @@ module Acts #:nodoc:
# hobbit.rated_total # => 8
# hobbit.rated_count # => 2
#
- # hobbit.unrate bill
+ # hobbit.unrate bill
# hobbit.rating_average # => 5
# hobbit.rated_total # => 5
# hobbit.rated_count # => 1
@@ -42,54 +42,54 @@ module Acts #:nodoc:
# usr = Book.find_rated_by jill # => [catch22, hobbit]
#
module Rated
-
+
class RateError < RuntimeError; end
-
+
def self.included(base) #:nodoc:
- base.extend(ClassMethods)
+ base.extend(ClassMethods)
end
-
+
module ClassMethods
# Make the model ratable. Can work both with and without a rater entity (defaults to User).
# The Rating model, holding the details of the ratings, will be created dynamically if it doesn't exist.
- #
+ #
# * Adds a has_many :ratings association to the model for easy retrieval of the detailed ratings.
# * Adds a has_many :raters association to the onject, unless :no_rater is given as a configuration parameter.
# * Adds a has_many :ratings associations to the rater class.
# * Adds a has_one :rating_statistic association to the model, if :with_stats_table => true is given as a configuration param.
#
# === Options
- # * :rating_class -
+ # * :rating_class -
# class of the model used for the ratings. Defaults to Rating. This class will be dynamically created if not already defined.
# If the class is predefined, it must have in it the following definitions:
- # belongs_to :rated, :polymorphic => true and if using a rater (which is true in most cases, see below) also
+ # belongs_to :rated, :polymorphic => true and if using a rater (which is true in most cases, see below) also
# belongs_to :rater, :class_name => 'User', :foreign_key => :rater_id replace user with the rater class if needed.
- # * :rater_class -
- # class of the model that creates the rating.
- # Defaults to User This class will NOT be created, so it must be defined in the app.
+ # * :rater_class -
+ # class of the model that creates the rating.
+ # Defaults to User This class will NOT be created, so it must be defined in the app.
# Another option will be to keep a session or IP based ID here to prevent multiple ratings from the same client.
- # * :no_rater -
+ # * :no_rater -
# do not keep track of who created the rating. This will change the behaviour
# to one that just collects and averages ratings, but doesn't keep track of who
# posted the rating. Useful in a public application that doesn't care about
# individual votes
- # * :rating_range -
+ # * :rating_range -
# A range object for the acceptable rating value range. Defaults to not limited
# * :with_stats_table -
# Use a separate statistics table to hold the count/total/average rating of the rated object instead of adding the columns to the object's table.
# This means we do not have to change the model table. It still holds a big performance advantage over using SQL to get the statistics
# * :stats_class -
- # Class of the statics table model. Only needed if :with_stats_table is set to true. Default to RatingStat.
+ # Class of the statics table model. Only needed if :with_stats_table is set to true. Default to RatingStat.
# This class need to have the following defined: belongs_to :rated, :polymorphic => true.
# And must make sure that it has the attributes rating_count, rating_total and rating_avg and those
# must be initialized to 0 on new instances
- #
+ #
def acts_as_rated(options = {})
# don't allow multiple calls
return if self.included_modules.include?(ActiveRecord::Acts::Rated::RateMethods)
send :include, ActiveRecord::Acts::Rated::RateMethods
-
+
# Create the model for ratings if it doesn't yet exist
rating_class = options[:rating_class] || 'Rating'
rater_class = options[:rater_class] || 'User'
@@ -111,15 +111,15 @@ class #{stats_class} < ActiveRecord::Base
end
EOV
end
-
+
raise RatedError, ":rating_range must be a range object" unless options[:rating_range].nil? || (Range === options[:rating_range])
- write_inheritable_attribute( :acts_as_rated_options ,
- { :rating_range => options[:rating_range],
+ write_inheritable_attribute( :acts_as_rated_options ,
+ { :rating_range => options[:rating_range],
:rating_class => rating_class,
:stats_class => stats_class,
:rater_class => rater_class } )
class_inheritable_reader :acts_as_rated_options
-
+
class_eval do
has_many :ratings, :as => :rated, :dependent => :delete_all, :class_name => rating_class.to_s
has_many(:raters, :through => :ratings, :class_name => rater_class.to_s) unless options[:no_rater]
@@ -129,7 +129,7 @@ class #{stats_class} < ActiveRecord::Base
end
# Add to the User (or whatever the rater is) a has_many ratings if working with a rater
- return if options[:no_rater]
+ return if options[:no_rater]
rater_as_class = rater_class.constantize
return if rater_as_class.instance_methods.include?('find_in_ratings')
rater_as_class.class_eval <<-EOS
@@ -139,17 +139,17 @@ class #{stats_class} < ActiveRecord::Base
end
module RateMethods
-
+
def self.included(base) #:nodoc:
base.extend ClassMethods
end
- # Get the average based on the special fields,
+ # Get the average based on the special fields,
# or with a SQL query if the rated objects doesn't have the avg and count fields
def rating_average
return self.rating_avg if attributes.has_key?('rating_avg')
return (rating_statistic.rating_avg || 0) rescue 0 if acts_as_rated_options[:stats_class]
- avg = ratings.average(:rating)
+ avg = ratings.average(:rating)
avg = 0 if avg.nan?
avg
end
@@ -163,25 +163,25 @@ def rated?
end
# last is the one where we don't keep the statistics - go direct to the db
- !ratings.find(:first).nil?
+ !ratings.find(:first).nil?
end
-
- # Get the number of ratings for this object based on the special fields,
+
+ # Get the number of ratings for this object based on the special fields,
# or with a SQL query if the rated objects doesn't have the avg and count fields
def rated_count
return self.rating_count || 0 if attributes.has_key? 'rating_count'
return (rating_statistic.rating_count || 0) rescue 0 if acts_as_rated_options[:stats_class]
- ratings.count
+ ratings.count
end
- # Get the sum of all ratings for this object based on the special fields,
+ # Get the sum of all ratings for this object based on the special fields,
# or with a SQL query if the rated objects doesn't have the avg and count fields
def rated_total
return self.rating_total || 0 if attributes.has_key? 'rating_total'
return (rating_statistic.rating_total || 0) rescue 0 if acts_as_rated_options[:stats_class]
- ratings.sum(:rating)
+ ratings.sum(:rating)
end
-
+
# Rate the object with or without a rater - create new or update as needed
#
# * value - the value to rate by, if a rating range was specified will be checked that it is in range
@@ -198,11 +198,11 @@ def rate value, rater = nil
raise RateError, "rating with rater must receive a rater as parameter" if with_rater && (rater.nil? || rater.id.nil?)
r = with_rater ? ratings.find(:first, :conditions => ['rater_id = ?', rater.id]) : nil
raise RateError, "value is out of range!" unless acts_as_rated_options[:rating_range].nil? || acts_as_rated_options[:rating_range] === value
-
+
# Find the place to store the rating statistics if any...
# Take care of the case of a separate statistics table
unless acts_as_rated_options[:stats_class].nil? || @rating_statistic.class.to_s == acts_as_rated_options[:stats_class]
- self.rating_statistic = acts_as_rated_options[:stats_class].constantize.new
+ self.rating_statistic = acts_as_rated_options[:stats_class].constantize.new
end
target = self if attributes.has_key? 'rating_total'
target ||= self.rating_statistic if acts_as_rated_options[:stats_class]
@@ -211,7 +211,7 @@ def rate value, rater = nil
rate = rating_class.new
rate.rater_id = rater.id if with_rater
if target
- target.rating_count = (target.rating_count || 0) + 1
+ target.rating_count = (target.rating_count || 0) + 1
target.rating_total = (target.rating_total || 0) + value
target.rating_avg = target.rating_total.to_f / target.rating_count
end
@@ -240,7 +240,7 @@ def rate value, rater = nil
def unrate rater
rating_class = acts_as_rated_options[:rating_class].constantize
if !(acts_as_rated_options[:rater_class].constantize === rater)
- raise RateError, "The rater object must be the one used when defining acts_as_rated (or a descendent of it). other objects are not acceptable"
+ raise RateError, "The rater object must be the one used when defining acts_as_rated (or a descendent of it). other objects are not acceptable"
end
raise RateError, "Rater must be a valid and existing object" if rater.nil? || rater.id.nil?
raise RateError, 'Cannot unrate if not using a rater' if !rating_class.column_names.include? "rater_id"
@@ -267,24 +267,24 @@ def unrate rater
def rated_by? rater
rating_class = acts_as_rated_options[:rating_class].constantize
if !(acts_as_rated_options[:rater_class].constantize === rater)
- raise RateError, "The rater object must be the one used when defining acts_as_rated (or a descendent of it). other objects are not acceptable"
+ raise RateError, "The rater object must be the one used when defining acts_as_rated (or a descendent of it). other objects are not acceptable"
end
raise RateError, "Rater must be a valid and existing object" if rater.nil? || rater.id.nil?
raise RateError, 'Rater must be a valid rater' if !rating_class.column_names.include? "rater_id"
ratings.count(:conditions => ['rater_id = ?', rater.id]) > 0
end
-
+
private
def init_rating_fields #:nodoc:
if attributes.has_key? 'rating_total'
- self.rating_count ||= 0
+ self.rating_count ||= 0
self.rating_total ||= 0
self.rating_avg ||= 0
end
- end
+ end
- end
+ end
module ClassMethods
@@ -298,7 +298,7 @@ def generate_ratings_columns table
table.column :rating_avg, :decimal, :precision => 10, :scale => 2
end
- # Create the needed columns for acts_as_rated.
+ # Create the needed columns for acts_as_rated.
# To be used during migration, but can also be used in other places.
def add_ratings_columns
if !self.column_names.include? 'rating_count'
@@ -321,24 +321,24 @@ def remove_ratings_columns
# Create the ratings table
# === Options hash:
# * :with_rater - add the rated_id column
- # * :table_name - use a table name other than ratings
+ # * :table_name - use a table name other than ratings
# * :with_stats_table - create also a rating statistics table
# * :stats_table_name - the name of the rating statistics table. Defaults to :rating_statistics
# To be used during migration, but can also be used in other places
def create_ratings_table options = {}
- with_rater = options[:with_rater] != false
+ with_rater = options[:with_rater] != false
name = options[:table_name] || :ratings
stats_table = options[:stats_table_name] || :rating_statistics if options[:with_stats_table]
self.connection.create_table(name) do |t|
t.column(:rater_id, :integer) unless !with_rater
t.column :rated_id, :integer
t.column :rated_type, :string
- t.column :rating, :decimal
+ t.column :rating, :decimal
end
self.connection.add_index(name, :rater_id) unless !with_rater
self.connection.add_index name, [:rated_type, :rated_id]
-
+
unless stats_table.nil?
self.connection.create_table(stats_table) do |t|
t.column :rated_id, :integer
@@ -347,13 +347,13 @@ def create_ratings_table options = {}
t.column :rating_total, :decimal
t.column :rating_avg, :decimal, :precision => 10, :scale => 2
end
-
+
self.connection.add_index stats_table, [:rated_type, :rated_id]
end
end
- # Drop the ratings table.
+ # Drop the ratings table.
# === Options hash:
# * :table_name - the name of the ratings table, defaults to ratings
# * :with_stats_table - remove the special rating statistics as well
@@ -362,10 +362,10 @@ def create_ratings_table options = {}
def drop_ratings_table options = {}
name = options[:table_name] || :ratings
stats_table = options[:stats_table_name] || :rating_statistics if options[:with_stats_table]
- self.connection.drop_table name
- self.connection.drop_table stats_table unless stats_table.nil?
+ self.connection.drop_table name
+ self.connection.drop_table stats_table unless stats_table.nil?
end
-
+
# Find all ratings for a specific rater.
# Will raise an error if this acts_as_rated is without a rater.
def find_rated_by rater
@@ -378,7 +378,7 @@ def find_rated_by rater
acts_as_rated_options[:rating_class].constantize.find(:all, :conditions => conds).collect {|r| r.rated_type.constantize.find_by_id r.rated.id }
end
-
+
# Find by rating - pass either a specific value or a range and the precision to calculate with
# * value - the value to look for or a range
# * precision - number of decimal digits to round to. Default to 10. Use 0 for integer numbers comparision
@@ -386,8 +386,8 @@ def find_rated_by rater
def find_by_rating value, precision = 10, round = true
rating_class = acts_as_rated_options[:rating_class].constantize
if column_names.include? "rating_avg"
- if Range === value
- conds = round ? [ 'round(rating_avg, ?) BETWEEN ? AND ?', precision.to_i, value.begin, value.end ] :
+ if Range === value
+ conds = round ? [ 'round(rating_avg, ?) BETWEEN ? AND ?', precision.to_i, value.begin, value.end ] :
[ 'rating_avg BETWEEN ? AND ?', value.begin, value.end ]
else
conds = round ? [ 'round(rating_avg, ?) = ?', precision.to_i, value ] : [ 'rating_avg = ?', value ]
@@ -397,20 +397,20 @@ def find_by_rating value, precision = 10, round = true
if round
base_sql = <<-EOS
select #{table_name}.*,round(COALESCE(average,0), #{precision.to_i}) AS rating_average from #{table_name} left outer join
- (select avg(rating) as average, rated_id
+ (select avg(rating) as average, rated_id
from #{rating_class.table_name}
- where rated_type = '#{class_name}'
- group by rated_id) as rated
- on rated_id=id
+ where rated_type = '#{base_class}'
+ group by rated_id) as rated
+ on rated_id=id
EOS
else
base_sql = <<-EOS
select #{table_name}.*,COALESCE(average,0) AS rating_average from #{table_name} left outer join
- (select avg(rating) as average, rated_id
+ (select avg(rating) as average, rated_id
from #{rating_class.table_name}
- where rated_type = '#{class_name}'
- group by rated_id) as rated
- on rated_id=id
+ where rated_type = '#{base_class}'
+ group by rated_id) as rated
+ on rated_id=id
EOS
end
if Range === value
@@ -429,9 +429,9 @@ def find_by_rating value, precision = 10, round = true
find_by_sql base_sql + where_part
end
- end
+ end
end
-
+
end
end
end