Module: ActiveRecord::Acts::Rated::ClassMethods

Defined in:
lib/acts_as_rated.rb,
lib/acts_as_rated.rb

Instance Method Summary collapse

Instance Method Details

#acts_as_rated(options = {}) ⇒ Object

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 - 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 :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. Another option will be to keep a session or IP based ID here to prevent multiple ratings from the same client.

  • :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 - 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 <tt>: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

Raises:

  • (RatedError)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/acts_as_rated.rb', line 88

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'
  stats_class  = options[:stats_class]  || 'RatingStatistic' if options[:with_stats_table]

  unless Object.const_defined?(rating_class)
    Object.class_eval <<-EOV
      class #{rating_class} < ActiveRecord::Base
        belongs_to :rated, :polymorphic => true
        #{options[:no_rater] ? '' : "belongs_to :rater, :class_name => #{rater_class}, :foreign_key => :rater_id"}
      end
    EOV
  end

  unless stats_class.nil? || Object.const_defined?(stats_class)
    Object.class_eval <<-EOV
      class #{stats_class} < ActiveRecord::Base
        belongs_to :rated, :polymorphic => true
      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], 
                                   :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]
    has_one(:rating_statistic, :class_name => stats_class.to_s, :as => :rated, :dependent => :delete) unless stats_class.nil?

    before_create :init_rating_fields
  end

  # Add to the User (or whatever the rater is) a has_many ratings if working with a 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
    has_many :ratings, :foreign_key => :rater_id, :class_name => #{rating_class.to_s}
  EOS
end

#add_ratings_columnsObject

Create the needed columns for acts_as_rated. To be used during migration, but can also be used in other places.



303
304
305
306
307
308
309
310
# File 'lib/acts_as_rated.rb', line 303

def add_ratings_columns
  if !self.column_names.include? 'rating_count'
    self.connection.add_column table_name, :rating_count, :integer
    self.connection.add_column table_name, :rating_total, :decimal
    self.connection.add_column table_name, :rating_avg,   :decimal, :precision => 10, :scale => 2
    self.reset_column_information
  end
end

#create_ratings_table(options = {}) ⇒ Object

Create the ratings table

Options hash:

  • :with_rater - add the rated_id column

  • :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



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/acts_as_rated.rb', line 328

def create_ratings_table options = {}
  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 
  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
      t.column :rated_type,   :string
      t.column :rating_count, :integer
      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_ratings_table(options = {}) ⇒ Object

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

  • :stats_table_name - the statistics table name. Defaults to :rating_statistics

To be used during migration, but can also be used in other places



362
363
364
365
366
367
# File 'lib/acts_as_rated.rb', line 362

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? 
end

#find_by_rating(value, precision = 10, round = true) ⇒ Object

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

  • round_it - round the rating average before comparing?. Defaults to true. Passing false will result in a faster query



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/acts_as_rated.rb', line 386

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 ] : 
                      [ 'rating_avg BETWEEN ? AND ?', value.begin, value.end ]
    else
      conds = round ? [ 'round(rating_avg, ?) = ?', precision.to_i, value ] : [ 'rating_avg = ?', value ]
    end
    find :all, :conditions => conds
  else
    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  
             from #{rating_class.table_name}
             where rated_type = '#{class_name}' 
             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  
             from #{rating_class.table_name}
             where rated_type = '#{class_name}' 
             group by rated_id) as rated 
             on rated_id=id 
      EOS
    end
    if Range === value
      if round
        where_part = " WHERE round(COALESCE(average,0), #{precision.to_i}) BETWEEN  #{connection.quote(value.begin)} AND #{connection.quote(value.end)}"
      else
        where_part = " WHERE COALESCE(average,0) BETWEEN #{connection.quote(value.begin)} AND #{connection.quote(value.end)}"
      end
    else
      if round
        where_part = " WHERE round(COALESCE(average,0), #{precision.to_i}) = #{connection.quote(value)}"
      else
        where_part = " WHERE COALESCE(average,0) = #{connection.quote(value)}"
      end
    end

    find_by_sql base_sql + where_part
  end
end

#find_rated_by(rater) ⇒ Object

Find all ratings for a specific rater. Will raise an error if this acts_as_rated is without a rater.

Raises:



371
372
373
374
375
376
377
378
379
# File 'lib/acts_as_rated.rb', line 371

def find_rated_by rater
  rating_class = acts_as_rated_options[:rating_class].constantize
  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" if !(acts_as_rated_options[:rater_class].constantize === rater)
  raise RateError, 'Cannot find_rated_by if not using a rater' if !rating_class.column_names.include? "rater_id"
  raise RateError, "Rater must be an existing object with an id" if rater.id.nil?
  rated_class = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
  conds = [ 'rated_type = ? AND rater_id = ?', rated_class, rater.id ]
  acts_as_rated_options[:rating_class].constantize.find(:all, :conditions => conds).collect {|r| r.rated_type.constantize.find_by_id r.rated.id }
end

#generate_ratings_columns(table) ⇒ Object

Generate the ratings columns on a table, to be used when creating the table in a migration. This is the preferred way to do in a migration that creates new tables as it will make it as part of the table creation, and not generate ALTER TABLE calls after the fact



295
296
297
298
299
# File 'lib/acts_as_rated.rb', line 295

def generate_ratings_columns table
  table.column :rating_count, :integer
  table.column :rating_total, :decimal
  table.column :rating_avg,   :decimal, :precision => 10, :scale => 2
end

#remove_ratings_columnsObject

Remove the acts_as_rated specific columns added with add_ratings_columns To be used during migration, but can also be used in other places



314
315
316
317
318
319
# File 'lib/acts_as_rated.rb', line 314

def remove_ratings_columns
  if self.column_names.include? 'rating_count'
    self.connection.remove_columns table_name, :rating_count, :rating_total, :rating_avg
    self.reset_column_information
  end
end