acts-as-rated 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Guy Naor (Famundo LLC)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,147 @@
1
+ = acts_as_rated
2
+
3
+ The ultimate rating system for ActiveRecord models. Highly flexible and configurable, while easy to use with the defaults. Supports 3 different ways to manage the statistics, and creates all the needed associations for easy access to everything.
4
+
5
+ Comes complete with the needed migrations code to make it easy to add to any project.
6
+
7
+ <em>NOTE:</em> It uses some advanced SQL constructs that might not be supported by all servers. It was tested on Postgres only. If you have patches/fixes for other databases, please send them and I will add them to the plugin. <em>UPDATE:</em> Thanks to work done by Tiago Serafim it now passes all but one tests on MySQL. And this test fails due to strangeness in the avg() function in MySQL, according to Tiago.
8
+
9
+
10
+ == Features
11
+
12
+ * Rate any model
13
+ * Optionally add fields to the rated objects to optimize speed
14
+ * Optionally add an external rating statistics table with a record for each rated model
15
+ * Can work with the added fields, external table or just using direct SQL count/avg calls
16
+ * Use any model as the rater (defaults to User)
17
+ * Limit the range of the ratings
18
+ * Average, total and number of ratings
19
+ * Find objects by ratings or rating ranges
20
+ * Find objects by rater
21
+ * Check if an object is rated by a specific rater. (Added by Tiago Serafim, thanks!)
22
+ * Extensively tested
23
+
24
+ == Basic Details
25
+
26
+ Install
27
+
28
+ * script/plugin install svn://rubyforge.org/var/svn/acts-as-rated/trunk/acts_as_rated
29
+ * gem install - <b>coming soon</b>
30
+
31
+ Rubyforge project
32
+
33
+ * http://rubyforge.org/projects/acts-as-rated
34
+
35
+ RDocs
36
+
37
+ * http://acts-as-rated.rubyforge.org
38
+
39
+ Subversion
40
+
41
+ * svn://rubyforge.org/var/svn/acts-as-rated
42
+
43
+ Agile Web Development directory
44
+
45
+ * http://www.agilewebdevelopment.com/plugins/acts_as_rated
46
+
47
+ My blog with some comments about the plugin
48
+
49
+ * http://devblog.famundo.com
50
+
51
+ Work done as part of Famundo development
52
+
53
+ * http://www.famundo.com
54
+
55
+ Contact me at
56
+
57
+ * guy.naor@famundo.com
58
+
59
+ == Changes
60
+ * V3 - Added improved find_by_rating as proposed by Ian McIntosh
61
+ * V2 - Passing MySQL tests, check if rated by a specific rater as proposed by Tiago Serafim
62
+
63
+ == TODO
64
+ * Test with more databases
65
+ * Test with other versions of Rails (tested against 1.2.1)
66
+ * Add view helpers for easy display and entering of the ratings
67
+
68
+ == Example of usage:
69
+
70
+ === Simple rating system
71
+ Look at the file <tt>test/rating_test.rb</tt> for many usage examples covering all variations of the plugin.
72
+
73
+ class Book < ActiveRecord::Base
74
+ acts_as_rated
75
+ end
76
+
77
+ bill = User.find_by_name 'bill'
78
+ jill = User.find_by_name 'jill'
79
+ catch22 = Book.find_by_title 'Catch 22'
80
+ hobbit = Book.find_by_title 'Hobbit'
81
+
82
+ catch22.rate 5, bill
83
+ hobbit.rate 3, bill
84
+ catch22.rate 1, jill
85
+ hobbit.rate 5, jill
86
+
87
+ hobbit.rating_average # => 4
88
+ hobbit.rated_total # => 8
89
+ hobbit.rated_count # => 2
90
+
91
+ hobbit.unrate bill
92
+ hobbit.rating_average # => 5
93
+ hobbit.rated_total # => 5
94
+ hobbit.rated_count # => 1
95
+
96
+ bks = Book.find_by_rating 5 # => [hobbit]
97
+ bks = Book.find_by_rating 1..5 # => [catch22, hobbit]
98
+
99
+ usr = Book.find_rated_by jill # => [catch22, hobbit]
100
+
101
+ === Migration
102
+ The file <tt>test/fixtures/migrations/001_add_rating_tables.rb</tt> shows examples of all types of migration options.
103
+
104
+ See also the detailed documentation for the <tt>acts_as_rated</tt> method on how to declare it, and the rest of the documentation for how to generate the migration columns/files and how to use it.
105
+
106
+ class AddRatingTables < ActiveRecord::Migration
107
+ def self.up
108
+ ActiveRecord::Base.create_ratings_table
109
+
110
+ # Movies table has the columns for the ratings added while it's created
111
+ create_table(:movies) do |t|
112
+ t.column :title, :text
113
+ Movie.generate_ratings_columns t
114
+ end
115
+
116
+ # Cars table has the columns for the ratings added, but after the fact, using ALTER TABLE calls.
117
+ # Usually used if the model already exist and we want to add the ratings after the fact
118
+ create_table(:cars) do |t|
119
+ t.column :title, :text
120
+ end
121
+ Car.add_ratings_columns
122
+ end
123
+
124
+ def self.down
125
+ # Remove the columns we added
126
+ Car.remove_ratings_columns
127
+
128
+ drop_table :movies rescue nil
129
+ drop_table :cars rescue nil
130
+
131
+ ActiveRecord::Base.drop_ratings_table
132
+ end
133
+ end
134
+
135
+
136
+ == Testing the plugin
137
+
138
+ The plugin comes with a full set of tests, both for migrations and for the code itself. The framework was taken from the acts_as_versioned plugin, allowing it to run stand-alone in the test directory.
139
+
140
+ run the tests:
141
+ rake test
142
+
143
+ In order for testing to work, you need to create a database (default name is acts_as_rated_plugin_test) and edit test/database.yml to make sure the login and password are correct. You can also change there the name of the database.
144
+
145
+ Testing defaults to postgresql, to change it set the environment variable DB to the driver you want to use:
146
+ env DB='mysql' rake test
147
+
@@ -0,0 +1,190 @@
1
+ require 'rubygems'
2
+
3
+ Gem::manage_gems
4
+
5
+ require 'rake/rdoctask'
6
+ require 'rake/packagetask'
7
+ require 'rake/gempackagetask'
8
+ require 'rake/testtask'
9
+ require 'rake/contrib/rubyforgepublisher'
10
+
11
+ PKG_NAME = 'acts_as_rated'
12
+ PKG_VERSION = '0.2.0'
13
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14
+ PROD_HOST = "guy.naor@famundo.com"
15
+ RUBY_FORGE_PROJECT = 'acts-as-rated'
16
+ RUBY_FORGE_USER = 'guynaor'
17
+
18
+ desc 'Default: run all tests.'
19
+ task :default => :test
20
+
21
+ task :test => [:test_plugin, :test_migrations ]
22
+
23
+ desc 'Test the acts_as_rated plugin.'
24
+ Rake::TestTask.new(:test_plugin) do |t|
25
+ t.libs << 'lib'
26
+ t.pattern = 'test/rated_test.rb'
27
+ t.verbose = true
28
+ end
29
+
30
+ desc 'Test the acts_as_rated plugin.'
31
+ Rake::TestTask.new(:test_migrations) do |t|
32
+ t.libs << 'lib'
33
+ t.pattern = 'test/migration_test.rb'
34
+ t.verbose = true
35
+ end
36
+
37
+ desc 'Generate documentation for the acts_as_rated plugin.'
38
+ Rake::RDocTask.new(:rdoc) do |rdoc|
39
+ rdoc.rdoc_dir = 'doc'
40
+ rdoc.title = "#{PKG_NAME} -- Rating system for ActiveRecord models"
41
+ rdoc.options << '--line-numbers'
42
+ rdoc.options << '--inline-source'
43
+ rdoc.rdoc_files.include('README')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
46
+
47
+ spec = Gem::Specification.new do |s|
48
+ s.name = PKG_NAME
49
+ s.version = PKG_VERSION
50
+ s.platform = Gem::Platform::RUBY
51
+ s.summary = "Rating system for active record models"
52
+ s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE)
53
+ s.files.delete "test/debug.log"
54
+ s.require_path = 'lib'
55
+ s.autorequire = 'acts_as_versioned'
56
+ s.has_rdoc = true
57
+ s.test_files = Dir['test/**/*_test.rb']
58
+ s.add_dependency 'activerecord', '>= 1.10.1'
59
+ s.add_dependency 'activesupport', '>= 1.1.1'
60
+ s.author = "Guy Naor"
61
+ s.email = "guy.naor@famundo.com"
62
+ s.homepage = "http://devblog.famundo.com"
63
+ end
64
+
65
+ Rake::GemPackageTask.new(spec) do |pkg|
66
+ pkg.need_tar = true
67
+ end
68
+
69
+ desc "Publish the API documentation"
70
+ task :pdoc => [:rdoc] do
71
+ Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
72
+ end
73
+
74
+ desc 'Publish the gem and API docs'
75
+ task :publish => [:pdoc, :rubyforge_upload]
76
+
77
+ desc "Publish the release files to RubyForge."
78
+ task :rubyforge_upload => :package do
79
+ files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
80
+
81
+ if RUBY_FORGE_PROJECT then
82
+ require 'net/http'
83
+ require 'open-uri'
84
+
85
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
86
+ project_data = open(project_uri) { |data| data.read }
87
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]
88
+ raise "Couldn't get group id" unless group_id
89
+
90
+ # This echos password to shell which is a bit sucky
91
+ if ENV["RUBY_FORGE_PASSWORD"]
92
+ password = ENV["RUBY_FORGE_PASSWORD"]
93
+ else
94
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
95
+ password = STDIN.gets.chomp
96
+ end
97
+
98
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
99
+ data = [
100
+ "login=1",
101
+ "form_loginname=#{RUBY_FORGE_USER}",
102
+ "form_pw=#{password}"
103
+ ].join("&")
104
+ http.post("/account/login.php", data)
105
+ end
106
+
107
+ cookie = login_response["set-cookie"]
108
+ raise "Login failed" unless cookie
109
+ headers = { "Cookie" => cookie }
110
+
111
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
112
+ release_data = open(release_uri, headers) { |data| data.read }
113
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]
114
+ raise "Couldn't get package id" unless package_id
115
+
116
+ first_file = true
117
+ release_id = ""
118
+
119
+ files.each do |filename|
120
+ basename = File.basename(filename)
121
+ file_ext = File.extname(filename)
122
+ file_data = File.open(filename, "rb") { |file| file.read }
123
+
124
+ puts "Releasing #{basename}..."
125
+
126
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
127
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
128
+ type_map = {
129
+ ".zip" => "3000",
130
+ ".tgz" => "3110",
131
+ ".gz" => "3110",
132
+ ".gem" => "1400"
133
+ }; type_map.default = "9999"
134
+ type = type_map[file_ext]
135
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
136
+
137
+ query_hash = if first_file then
138
+ {
139
+ "group_id" => group_id,
140
+ "package_id" => package_id,
141
+ "release_name" => PKG_FILE_NAME,
142
+ "release_date" => release_date,
143
+ "type_id" => type,
144
+ "processor_id" => "8000", # Any
145
+ "release_notes" => "",
146
+ "release_changes" => "",
147
+ "preformatted" => "1",
148
+ "submit" => "1"
149
+ }
150
+ else
151
+ {
152
+ "group_id" => group_id,
153
+ "release_id" => release_id,
154
+ "package_id" => package_id,
155
+ "step2" => "1",
156
+ "type_id" => type,
157
+ "processor_id" => "8000", # Any
158
+ "submit" => "Add This File"
159
+ }
160
+ end
161
+
162
+ query = "?" + query_hash.map do |(name, value)|
163
+ [name, URI.encode(value)].join("=")
164
+ end.join("&")
165
+
166
+ data = [
167
+ "--" + boundary,
168
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
169
+ "Content-Type: application/octet-stream",
170
+ "Content-Transfer-Encoding: binary",
171
+ "", file_data, ""
172
+ ].join("\x0D\x0A")
173
+
174
+ release_headers = headers.merge(
175
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
176
+ )
177
+
178
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
179
+ http.post(target + query, data, release_headers)
180
+ end
181
+
182
+ if first_file then
183
+ release_id = release_response.body[/release_id=(\d+)/, 1]
184
+ raise("Couldn't get release id") unless release_id
185
+ end
186
+
187
+ first_file = false
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "acts-as-rated"
3
+ s.version = "0.4"
4
+ s.date = "2009-01-08"
5
+ s.summary = "Rails plugin rating system for ActiveRecord models."
6
+ s.email = "guy.naor@famundo.com"
7
+ s.homepage = "http://github.com/jasherai/acts-as-rated"
8
+ s.description = "Flexible, configurable, and easy to use with the defaults. Supports 3 different ways to manage rating statistics."
9
+ s.has_rdoc = false
10
+ s.authors = "Guy Noar"
11
+ s.files = [
12
+ "acts-as-rated.gemspec",
13
+ "init.rb",
14
+ "lib/acts_as_rated.rb",
15
+ "MIT-LICENSE",
16
+ "Rakefile",
17
+ "README"
18
+ ]
19
+ s.test_files = ["test/rated_test.rb"]
20
+ end
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'acts_as_rated'
2
+
3
+
@@ -0,0 +1,441 @@
1
+ module ActiveRecord #:nodoc:
2
+ module Acts #:nodoc:
3
+
4
+ # == acts_as_rated
5
+ # Adds rating capabilities to any ActiveRecord object.
6
+ # It has the ability to work with objects that have or don't special fields to keep a tally of the
7
+ # ratings and number of votes for each object.
8
+ # In addition it will by default use the User model as the rater object and keep the ratings per-user.
9
+ # It can be configured to use another class, or not use a rater at all, just keeping a global rating
10
+ #
11
+ # Special methods are provided to create the ratings table and if needed, to add the special fields needed
12
+ # to keep per-objects ratings fast for access to rated objects. Can be easily used in migrations.
13
+ #
14
+ # == Example of usage:
15
+ #
16
+ # class Book < ActiveRecord::Base
17
+ # acts_as_rated
18
+ # end
19
+ #
20
+ # bill = User.find_by_name 'bill'
21
+ # jill = User.find_by_name 'jill'
22
+ # catch22 = Book.find_by_title 'Catch 22'
23
+ # hobbit = Book.find_by_title 'Hobbit'
24
+ #
25
+ # catch22.rate 5, bill
26
+ # hobbit.rate 3, bill
27
+ # catch22.rate 1, jill
28
+ # hobbit.rate 5, jill
29
+ #
30
+ # hobbit.rating_average # => 4
31
+ # hobbit.rated_total # => 8
32
+ # hobbit.rated_count # => 2
33
+ #
34
+ # hobbit.unrate bill
35
+ # hobbit.rating_average # => 5
36
+ # hobbit.rated_total # => 5
37
+ # hobbit.rated_count # => 1
38
+ #
39
+ # bks = Book.find_by_rating 5 # => [hobbit]
40
+ # bks = Book.find_by_rating 1..5 # => [catch22, hobbit]
41
+ #
42
+ # usr = Book.find_rated_by jill # => [catch22, hobbit]
43
+ #
44
+ module Rated
45
+
46
+ class RateError < RuntimeError; end
47
+
48
+ def self.included(base) #:nodoc:
49
+ base.extend(ClassMethods)
50
+ end
51
+
52
+ module ClassMethods
53
+
54
+ # Make the model ratable. Can work both with and without a rater entity (defaults to User).
55
+ # The Rating model, holding the details of the ratings, will be created dynamically if it doesn't exist.
56
+ #
57
+ # * Adds a <tt>has_many :ratings</tt> association to the model for easy retrieval of the detailed ratings.
58
+ # * Adds a <tt>has_many :raters</tt> association to the onject, unless <tt>:no_rater</tt> is given as a configuration parameter.
59
+ # * Adds a <tt>has_many :ratings</tt> associations to the rater class.
60
+ # * Adds a <tt>has_one :rating_statistic</tt> association to the model, if <tt>:with_stats_table => true</tt> is given as a configuration param.
61
+ #
62
+ # === Options
63
+ # * <tt>:rating_class</tt> -
64
+ # class of the model used for the ratings. Defaults to Rating. This class will be dynamically created if not already defined.
65
+ # If the class is predefined, it must have in it the following definitions:
66
+ # <tt>belongs_to :rated, :polymorphic => true</tt> and if using a rater (which is true in most cases, see below) also
67
+ # <tt>belongs_to :rater, :class_name => 'User', :foreign_key => :rater_id</tt> replace user with the rater class if needed.
68
+ # * <tt>:rater_class</tt> -
69
+ # class of the model that creates the rating.
70
+ # Defaults to User This class will NOT be created, so it must be defined in the app.
71
+ # Another option will be to keep a session or IP based ID here to prevent multiple ratings from the same client.
72
+ # * <tt>:no_rater</tt> -
73
+ # do not keep track of who created the rating. This will change the behaviour
74
+ # to one that just collects and averages ratings, but doesn't keep track of who
75
+ # posted the rating. Useful in a public application that doesn't care about
76
+ # individual votes
77
+ # * <tt>:rating_range</tt> -
78
+ # A range object for the acceptable rating value range. Defaults to not limited
79
+ # * <tt>:with_stats_table</tt> -
80
+ # 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.
81
+ # 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
82
+ # * <tt>:stats_class -
83
+ # Class of the statics table model. Only needed if <tt>:with_stats_table</tt> is set to true. Default to RatingStat.
84
+ # This class need to have the following defined: <tt>belongs_to :rated, :polymorphic => true</tt>.
85
+ # And must make sure that it has the attributes <tt>rating_count</tt>, <tt>rating_total</tt> and <tt>rating_avg</tt> and those
86
+ # must be initialized to 0 on new instances
87
+ #
88
+ def acts_as_rated(options = {})
89
+ # don't allow multiple calls
90
+ return if self.included_modules.include?(ActiveRecord::Acts::Rated::RateMethods)
91
+ send :include, ActiveRecord::Acts::Rated::RateMethods
92
+
93
+ # Create the model for ratings if it doesn't yet exist
94
+ rating_class = options[:rating_class] || 'Rating'
95
+ rater_class = options[:rater_class] || 'User'
96
+ stats_class = options[:stats_class] || 'RatingStatistic' if options[:with_stats_table]
97
+
98
+ unless Object.const_defined?(rating_class)
99
+ Object.class_eval <<-EOV
100
+ class #{rating_class} < ActiveRecord::Base
101
+ belongs_to :rated, :polymorphic => true
102
+ #{options[:no_rater] ? '' : "belongs_to :rater, :class_name => #{rater_class}, :foreign_key => :rater_id"}
103
+ end
104
+ EOV
105
+ end
106
+
107
+ unless stats_class.nil? || Object.const_defined?(stats_class)
108
+ Object.class_eval <<-EOV
109
+ class #{stats_class} < ActiveRecord::Base
110
+ belongs_to :rated, :polymorphic => true
111
+ end
112
+ EOV
113
+ end
114
+
115
+ raise RatedError, ":rating_range must be a range object" unless options[:rating_range].nil? || (Range === options[:rating_range])
116
+ write_inheritable_attribute( :acts_as_rated_options ,
117
+ { :rating_range => options[:rating_range],
118
+ :rating_class => rating_class,
119
+ :stats_class => stats_class,
120
+ :rater_class => rater_class } )
121
+ class_inheritable_reader :acts_as_rated_options
122
+
123
+ class_eval do
124
+ has_many :ratings, :as => :rated, :dependent => :delete_all, :class_name => rating_class.to_s
125
+ has_many(:raters, :through => :ratings, :class_name => rater_class.to_s) unless options[:no_rater]
126
+ has_one(:rating_statistic, :class_name => stats_class.to_s, :as => :rated, :dependent => :delete) unless stats_class.nil?
127
+
128
+ before_create :init_rating_fields
129
+ end
130
+
131
+ # Add to the User (or whatever the rater is) a has_many ratings if working with a rater
132
+ return if options[:no_rater]
133
+ rater_as_class = rater_class.constantize
134
+ return if rater_as_class.instance_methods.include?('find_in_ratings')
135
+ rater_as_class.class_eval <<-EOS
136
+ has_many :ratings, :foreign_key => :rater_id, :class_name => #{rating_class.to_s}
137
+ EOS
138
+ end
139
+ end
140
+
141
+ module RateMethods
142
+
143
+ def self.included(base) #:nodoc:
144
+ base.extend ClassMethods
145
+ end
146
+
147
+ # Get the average based on the special fields,
148
+ # or with a SQL query if the rated objects doesn't have the avg and count fields
149
+ def rating_average
150
+ return self.rating_avg if attributes.has_key?('rating_avg')
151
+ return (rating_statistic.rating_avg || 0) rescue 0 if acts_as_rated_options[:stats_class]
152
+ avg = ratings.average(:rating)
153
+ avg = 0 if avg.nan?
154
+ avg
155
+ end
156
+
157
+ # Is this object rated already?
158
+ def rated?
159
+ return (!self.rating_count.nil? && self.rating_count > 0) if attributes.has_key? 'rating_count'
160
+ if acts_as_rated_options[:stats_class]
161
+ stats = (rating_statistic.rating_count || 0) rescue 0
162
+ return stats > 0
163
+ end
164
+
165
+ # last is the one where we don't keep the statistics - go direct to the db
166
+ !ratings.find(:first).nil?
167
+ end
168
+
169
+ # Get the number of ratings for this object based on the special fields,
170
+ # or with a SQL query if the rated objects doesn't have the avg and count fields
171
+ def rated_count
172
+ return self.rating_count || 0 if attributes.has_key? 'rating_count'
173
+ return (rating_statistic.rating_count || 0) rescue 0 if acts_as_rated_options[:stats_class]
174
+ ratings.count
175
+ end
176
+
177
+ # Get the sum of all ratings for this object based on the special fields,
178
+ # or with a SQL query if the rated objects doesn't have the avg and count fields
179
+ def rated_total
180
+ return self.rating_total || 0 if attributes.has_key? 'rating_total'
181
+ return (rating_statistic.rating_total || 0) rescue 0 if acts_as_rated_options[:stats_class]
182
+ ratings.sum(:rating)
183
+ end
184
+
185
+ # Rate the object with or without a rater - create new or update as needed
186
+ #
187
+ # * <tt>value</tt> - the value to rate by, if a rating range was specified will be checked that it is in range
188
+ # * <tt>rater</tt> - an object of the rater class. Must be valid and with an id to be used.
189
+ # If the acts_as_rated was passed :with_rater => false, this parameter is not required
190
+ def rate value, rater = nil
191
+ # Sanity checks for the parameters
192
+ rating_class = acts_as_rated_options[:rating_class].constantize
193
+ with_rater = rating_class.column_names.include? "rater_id"
194
+ raise RateError, "rating with no rater cannot accept a rater as a parameter" if !with_rater && !rater.nil?
195
+ if with_rater && !(acts_as_rated_options[:rater_class].constantize === rater)
196
+ 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"
197
+ end
198
+ raise RateError, "rating with rater must receive a rater as parameter" if with_rater && (rater.nil? || rater.id.nil?)
199
+ r = with_rater ? ratings.find(:first, :conditions => ['rater_id = ?', rater.id]) : nil
200
+ raise RateError, "value is out of range!" unless acts_as_rated_options[:rating_range].nil? || acts_as_rated_options[:rating_range] === value
201
+
202
+ # Find the place to store the rating statistics if any...
203
+ # Take care of the case of a separate statistics table
204
+ unless acts_as_rated_options[:stats_class].nil? || @rating_statistic.class.to_s == acts_as_rated_options[:stats_class]
205
+ self.rating_statistic = acts_as_rated_options[:stats_class].constantize.new
206
+ end
207
+ target = self if attributes.has_key? 'rating_total'
208
+ target ||= self.rating_statistic if acts_as_rated_options[:stats_class]
209
+ rating_class.transaction do
210
+ if r.nil?
211
+ rate = rating_class.new
212
+ rate.rater_id = rater.id if with_rater
213
+ if target
214
+ target.rating_count = (target.rating_count || 0) + 1
215
+ target.rating_total = (target.rating_total || 0) + value
216
+ target.rating_avg = target.rating_total.to_f / target.rating_count
217
+ end
218
+ ratings << rate
219
+ else
220
+ rate = r
221
+ if target
222
+ target.rating_total += value - rate.rating # Update the total rating with the new one
223
+ target.rating_avg = target.rating_total.to_f / target.rating_count
224
+ end
225
+ end
226
+
227
+ # Remove the actual ratings table entry
228
+ rate.rating = value
229
+ if !new_record?
230
+ rate.save
231
+ target.save if target
232
+ end
233
+ end
234
+ end
235
+
236
+ # Unrate the rating of the specified rater object.
237
+ # * <tt>rater</tt> - an object of the rater class. Must be valid and with an id to be used
238
+ #
239
+ # Unrate cannot be called for acts_as_rated with :with_rater => false
240
+ def unrate rater
241
+ rating_class = acts_as_rated_options[:rating_class].constantize
242
+ if !(acts_as_rated_options[:rater_class].constantize === rater)
243
+ 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"
244
+ end
245
+ raise RateError, "Rater must be a valid and existing object" if rater.nil? || rater.id.nil?
246
+ raise RateError, 'Cannot unrate if not using a rater' if !rating_class.column_names.include? "rater_id"
247
+ r = ratings.find(:first, :conditions => ['rater_id = ?', rater.id])
248
+ if !r.nil?
249
+ target = self if attributes.has_key? 'rating_total'
250
+ target ||= self.rating_statistic if acts_as_rated_options[:stats_class]
251
+ if target
252
+ rating_class.transaction do
253
+ target.rating_count -= 1
254
+ target.rating_total -= r.rating
255
+ target.rating_avg = target.rating_total.to_f / target.rating_count
256
+ target.rating_avg = 0 if target.rating_avg.nan?
257
+ end
258
+ end
259
+
260
+ # Removing the ratings table entry
261
+ r.destroy
262
+ target.save if !target.nil?
263
+ end
264
+ end
265
+
266
+ # Check if an item was already rated by the given rater
267
+ def rated_by? rater
268
+ rating_class = acts_as_rated_options[:rating_class].constantize
269
+ if !(acts_as_rated_options[:rater_class].constantize === rater)
270
+ 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"
271
+ end
272
+ raise RateError, "Rater must be a valid and existing object" if rater.nil? || rater.id.nil?
273
+ raise RateError, 'Rater must be a valid rater' if !rating_class.column_names.include? "rater_id"
274
+ ratings.count(:conditions => ['rater_id = ?', rater.id]) > 0
275
+ end
276
+
277
+ private
278
+
279
+ def init_rating_fields #:nodoc:
280
+ if attributes.has_key? 'rating_total'
281
+ self.rating_count ||= 0
282
+ self.rating_total ||= 0
283
+ self.rating_avg ||= 0
284
+ end
285
+ end
286
+
287
+ end
288
+
289
+ module ClassMethods
290
+
291
+ # Generate the ratings columns on a table, to be used when creating the table
292
+ # in a migration. This is the preferred way to do in a migration that creates
293
+ # new tables as it will make it as part of the table creation, and not generate
294
+ # ALTER TABLE calls after the fact
295
+ def generate_ratings_columns table
296
+ table.column :rating_count, :integer
297
+ table.column :rating_total, :decimal
298
+ table.column :rating_avg, :decimal, :precision => 10, :scale => 2
299
+ end
300
+
301
+ # Create the needed columns for acts_as_rated.
302
+ # To be used during migration, but can also be used in other places.
303
+ def add_ratings_columns
304
+ if !self.column_names.include? 'rating_count'
305
+ self.connection.add_column table_name, :rating_count, :integer
306
+ self.connection.add_column table_name, :rating_total, :decimal
307
+ self.connection.add_column table_name, :rating_avg, :decimal, :precision => 10, :scale => 2
308
+ self.reset_column_information
309
+ end
310
+ end
311
+
312
+ # Remove the acts_as_rated specific columns added with add_ratings_columns
313
+ # To be used during migration, but can also be used in other places
314
+ def remove_ratings_columns
315
+ if self.column_names.include? 'rating_count'
316
+ self.connection.remove_columns table_name, :rating_count, :rating_total, :rating_avg
317
+ self.reset_column_information
318
+ end
319
+ end
320
+
321
+ # Create the ratings table
322
+ # === Options hash:
323
+ # * <tt>:with_rater</tt> - add the rated_id column
324
+ # * <tt>:table_name</tt> - use a table name other than ratings
325
+ # * <tt>:with_stats_table</tt> - create also a rating statistics table
326
+ # * <tt>:stats_table_name</tt> - the name of the rating statistics table. Defaults to :rating_statistics
327
+ # To be used during migration, but can also be used in other places
328
+ def create_ratings_table options = {}
329
+ with_rater = options[:with_rater] != false
330
+ name = options[:table_name] || :ratings
331
+ stats_table = options[:stats_table_name] || :rating_statistics if options[:with_stats_table]
332
+ self.connection.create_table(name) do |t|
333
+ t.column(:rater_id, :integer) unless !with_rater
334
+ t.column :rated_id, :integer
335
+ t.column :rated_type, :string
336
+ t.column :rating, :decimal
337
+ end
338
+
339
+ self.connection.add_index(name, :rater_id) unless !with_rater
340
+ self.connection.add_index name, [:rated_type, :rated_id]
341
+
342
+ unless stats_table.nil?
343
+ self.connection.create_table(stats_table) do |t|
344
+ t.column :rated_id, :integer
345
+ t.column :rated_type, :string
346
+ t.column :rating_count, :integer
347
+ t.column :rating_total, :decimal
348
+ t.column :rating_avg, :decimal, :precision => 10, :scale => 2
349
+ end
350
+
351
+ self.connection.add_index stats_table, [:rated_type, :rated_id]
352
+ end
353
+
354
+ end
355
+
356
+ # Drop the ratings table.
357
+ # === Options hash:
358
+ # * <tt>:table_name</tt> - the name of the ratings table, defaults to ratings
359
+ # * <tt>:with_stats_table</tt> - remove the special rating statistics as well
360
+ # * <tt>:stats_table_name</tt> - the statistics table name. Defaults to :rating_statistics
361
+ # To be used during migration, but can also be used in other places
362
+ def drop_ratings_table options = {}
363
+ name = options[:table_name] || :ratings
364
+ stats_table = options[:stats_table_name] || :rating_statistics if options[:with_stats_table]
365
+ self.connection.drop_table name
366
+ self.connection.drop_table stats_table unless stats_table.nil?
367
+ end
368
+
369
+ # Find all ratings for a specific rater.
370
+ # Will raise an error if this acts_as_rated is without a rater.
371
+ def find_rated_by rater
372
+ rating_class = acts_as_rated_options[:rating_class].constantize
373
+ 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)
374
+ raise RateError, 'Cannot find_rated_by if not using a rater' if !rating_class.column_names.include? "rater_id"
375
+ raise RateError, "Rater must be an existing object with an id" if rater.id.nil?
376
+ rated_class = ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s
377
+ conds = [ 'rated_type = ? AND rater_id = ?', rated_class, rater.id ]
378
+ acts_as_rated_options[:rating_class].constantize.find(:all, :conditions => conds).collect {|r| r.rated_type.constantize.find_by_id r.rated.id }
379
+ end
380
+
381
+
382
+ # Find by rating - pass either a specific value or a range and the precision to calculate with
383
+ # * <tt>value</tt> - the value to look for or a range
384
+ # * <tt>precision</tt> - number of decimal digits to round to. Default to 10. Use 0 for integer numbers comparision
385
+ # * <tt>round_it</tt> - round the rating average before comparing?. Defaults to true. Passing false will result in a faster query
386
+ def find_by_rating value, precision = 10, round = true
387
+ rating_class = acts_as_rated_options[:rating_class].constantize
388
+ if column_names.include? "rating_avg"
389
+ if Range === value
390
+ conds = round ? [ 'round(rating_avg, ?) BETWEEN ? AND ?', precision.to_i, value.begin, value.end ] :
391
+ [ 'rating_avg BETWEEN ? AND ?', value.begin, value.end ]
392
+ else
393
+ conds = round ? [ 'round(rating_avg, ?) = ?', precision.to_i, value ] : [ 'rating_avg = ?', value ]
394
+ end
395
+ find :all, :conditions => conds
396
+ else
397
+ if round
398
+ base_sql = <<-EOS
399
+ select #{table_name}.*,round(COALESCE(average,0), #{precision.to_i}) AS rating_average from #{table_name} left outer join
400
+ (select avg(rating) as average, rated_id
401
+ from #{rating_class.table_name}
402
+ where rated_type = '#{class_name}'
403
+ group by rated_id) as rated
404
+ on rated_id=id
405
+ EOS
406
+ else
407
+ base_sql = <<-EOS
408
+ select #{table_name}.*,COALESCE(average,0) AS rating_average from #{table_name} left outer join
409
+ (select avg(rating) as average, rated_id
410
+ from #{rating_class.table_name}
411
+ where rated_type = '#{class_name}'
412
+ group by rated_id) as rated
413
+ on rated_id=id
414
+ EOS
415
+ end
416
+ if Range === value
417
+ if round
418
+ where_part = " WHERE round(COALESCE(average,0), #{precision.to_i}) BETWEEN #{connection.quote(value.begin)} AND #{connection.quote(value.end)}"
419
+ else
420
+ where_part = " WHERE COALESCE(average,0) BETWEEN #{connection.quote(value.begin)} AND #{connection.quote(value.end)}"
421
+ end
422
+ else
423
+ if round
424
+ where_part = " WHERE round(COALESCE(average,0), #{precision.to_i}) = #{connection.quote(value)}"
425
+ else
426
+ where_part = " WHERE COALESCE(average,0) = #{connection.quote(value)}"
427
+ end
428
+ end
429
+
430
+ find_by_sql base_sql + where_part
431
+ end
432
+ end
433
+ end
434
+
435
+ end
436
+ end
437
+ end
438
+
439
+
440
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Rated
441
+
@@ -0,0 +1,426 @@
1
+ require File.join(File.dirname(__FILE__), 'abstract_unit')
2
+ require File.join(File.dirname(__FILE__), 'dummy_classes')
3
+
4
+ class RatedTest < Test::Unit::TestCase
5
+ fixtures :cars, :movies, :books, :users, :ratings, :no_rater_ratings, :videos, :stats_ratings, :my_stats_ratings, :rating_statistics, :my_statistics
6
+
7
+ def test_rate
8
+ # Regular one...
9
+ m = movies(:gone_with_the_wind)
10
+ check_average m, 4.33
11
+ m.rate 1, users(:sarah)
12
+ check_average m, 3
13
+ m = Movie.new :title => 'King Kong'
14
+ m.rate 4, users(:john)
15
+ assert m.new_record?
16
+ assert_equal 4, m.rating_average
17
+ assert_equal 1, m.rating_count
18
+ assert_equal 4, m.rating_total
19
+ assert m.save
20
+ m = Movie.find m.id
21
+ assert_equal 4, m.rating_average
22
+ assert_equal 1, m.rating_count
23
+ assert_equal 4, m.rating_total
24
+ m.rate 6, users(:bill)
25
+ m.rate 2, users(:sarah)
26
+ assert_equal 4, m.rating_average
27
+ assert_equal 3, m.rating_count
28
+ assert_equal 12, m.rating_total
29
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { m.rate 6 }
30
+
31
+ # Ratring with norating columns
32
+ b = books(:shogun)
33
+ assert_raise(NoMethodError) { b.rating_total }
34
+ check_average b, 3.75
35
+ b.rate 10, Worker.find(users(:jane).id)
36
+ check_average b, 5.5
37
+
38
+ # Rating with no rater
39
+ c = cars(:bug)
40
+ check_average c, 4
41
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { c.rate 10, users(:jill) }
42
+ c.rate 10
43
+ c.rate 10
44
+ c.rate 10
45
+ c.rate 10
46
+ c.rate 10
47
+ check_average c, 9
48
+
49
+ # Ranged ratings
50
+ f = Film.find :first, :order => 'title'
51
+ assert_equal 'Crash', f.title
52
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { f.rate 0, users(:sarah) }
53
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { f.rate 5.0001, users(:sarah) }
54
+ f.rate 1, users(:sarah)
55
+ f.rate 5, users(:jane)
56
+ check_average f, 3
57
+
58
+ # rating with an external statistics table
59
+ v = videos(:ten)
60
+ rc = v.ratings.count
61
+ assert_raise(NoMethodError) { v.rating_total }
62
+ check_average v, 1
63
+ v.rate 9, users(:jane)
64
+ check_average v, 3
65
+ assert_equal rc, v.ratings.count
66
+ v.rate 3, users(:jack)
67
+ check_average v, 3
68
+ assert_equal rc + 1, v.ratings.count
69
+ t = Tape.find(videos(:fame).id)
70
+ rc = t.ratings.count
71
+ assert_raise(NoMethodError) { t.rating_total }
72
+ check_average t, 5
73
+ t.rate 2, users(:jane)
74
+ check_average t, 4
75
+ assert_equal rc, t.ratings.count
76
+ t.rate 8, users(:jack)
77
+ check_average t, 5
78
+ assert_equal rc + 1, t.ratings.count
79
+ v = Video.new :title => 'Hair'
80
+ v.save
81
+ check_average v, 0
82
+ v.rate 4, users(:bill)
83
+ check_average v, 4
84
+ t = Tape.new :title => 'Friends'
85
+ t.save
86
+ check_average t, 0
87
+ t.rate 4, users(:bill)
88
+ check_average t, 4
89
+ t.rate 6, users(:jill)
90
+ check_average t, 5
91
+
92
+ # Rating with the wrong rater class or one that's not initialized
93
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.rate 10, users(:jane) }
94
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.rate 10, 3 }
95
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.rate 10, Worker.new }
96
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.rate 10 }
97
+ end
98
+
99
+ def test_unrate
100
+ # Regular one...
101
+ m = movies(:gone_with_the_wind)
102
+ check_average m, 4.33
103
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { m.unrate nil }
104
+ m.unrate users(:john)
105
+ m.unrate users(:bill)
106
+ m.unrate users(:sarah)
107
+ m.unrate users(:jane)
108
+ m.unrate users(:jill)
109
+ check_average m, 0
110
+ m = Movie.new :title => 'King Kong'
111
+ m.rate 4, users(:john)
112
+ m.rate 4, users(:bill)
113
+ assert m.new_record?
114
+ assert_equal 4, m.rating_average
115
+ assert_equal 2, m.rating_count
116
+ assert_equal 8, m.rating_total
117
+ assert m.save
118
+ m = Movie.find m.id
119
+ assert_equal 4, m.rating_average
120
+ assert_equal 2, m.rating_count
121
+ assert_equal 8, m.rating_total
122
+ m.unrate users(:john)
123
+ assert_equal 4, m.rating_average
124
+ assert_equal 1, m.rating_count
125
+ assert_equal 4, m.rating_total
126
+
127
+ # Unrating with norating columns
128
+ b = books(:shogun)
129
+ assert_raise(NoMethodError) { b.ratings[0].rating_total }
130
+ check_average b, 3.75
131
+ b.unrate Worker.find(users(:bill).id)
132
+ check_average b, 4
133
+
134
+ # Unrating with external stats table
135
+ v = videos(:fields_of_dreams)
136
+ check_average v, 3.2
137
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { v.unrate nil }
138
+ v.unrate users(:john)
139
+ v.unrate users(:bill)
140
+ v.unrate users(:sarah)
141
+ v.unrate users(:jane)
142
+ v.unrate users(:jill)
143
+ check_average v, 0
144
+ v = Video.new :title => 'King Kong'
145
+ assert v.new_record?
146
+ assert v.save
147
+ v = Video.find v.id
148
+ v.rate 4, users(:john)
149
+ v.rate 4, users(:bill)
150
+ assert_equal 4, v.rating_average
151
+ assert_equal 2, v.rated_count
152
+ assert_equal 8, v.rated_total
153
+ v.unrate users(:john)
154
+ assert_equal 4, v.rating_average
155
+ assert_equal 1, v.rated_count
156
+ assert_equal 4, v.rated_total
157
+
158
+ t = Tape.find(videos(:fields_of_dreams).id)
159
+ check_average t, 3.2
160
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { t.unrate nil }
161
+ t.unrate users(:john)
162
+ t.unrate users(:bill)
163
+ t.unrate users(:sarah)
164
+ t.unrate users(:jane)
165
+ t.unrate users(:jill)
166
+ check_average t, 0
167
+ t = Tape.new :title => 'Scream'
168
+ assert t.save
169
+ t.rate 4, users(:john)
170
+ t.rate 6, users(:bill)
171
+ t = Tape.find t.id
172
+ assert_equal 5, t.rating_average
173
+ assert_equal 2, t.rated_count
174
+ assert_equal 10, t.rated_total
175
+ t.unrate users(:john)
176
+ assert_equal 6, t.rating_average
177
+ assert_equal 1, t.rated_count
178
+ assert_equal 6, t.rated_total
179
+
180
+ # No unrating with no rater
181
+ c = cars(:bug)
182
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { c.unrate users(:jill) }
183
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { c.unrate nil }
184
+
185
+ # Check unrater validity
186
+ b = books(:shogun)
187
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.unrate users(:jane) }
188
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.unrate 3 }
189
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { b.unrate Worker.new }
190
+ end
191
+
192
+ def test_rated?
193
+ [Car, Movie, Book, Video, Tape, Truck, Film].each do |c|
194
+ # First check all the ones we have in the fixtures
195
+ c.find(:all).each do |o|
196
+ assert o.rated? if o.rated_count > 0
197
+ end
198
+
199
+ # Then create some new ones and test those as well
200
+ o = c.new(:title => 'Test Title')
201
+ assert o.save
202
+ assert !o.rated?
203
+ o.rate 4, Worker.find(users(:john).id) if [Book].include? c
204
+ o.rate 4, users(:john) if [Movie, Video, Tape, Film].include? c
205
+ o.rate 4 if [Car, Truck].include? c
206
+ #o.reload
207
+ assert o.rated?
208
+ end
209
+ end
210
+
211
+ def test_rating_average
212
+ m = movies(:gone_with_the_wind)
213
+ check_average m, 4.33
214
+ m = movies(:oz)
215
+ check_average m, 5
216
+ m = movies(:crash)
217
+ check_average m, 0
218
+ m.rate 3, users(:john)
219
+ m.rate 5, users(:bill)
220
+ check_average m, 4
221
+ m.rate 3, users(:bill)
222
+ check_average m, 3
223
+ m.unrate users(:bill)
224
+ check_average m, 3
225
+
226
+ c = cars(:camry)
227
+ check_average c, 3
228
+ c = cars(:bug)
229
+ check_average c, 4
230
+ c = cars(:expedition)
231
+ check_average c, 0
232
+ c.rate 3
233
+ c.rate 5
234
+ check_average c, 4
235
+ c.rate 3
236
+ check_average c, 3.66
237
+ end
238
+
239
+ def test_count
240
+ m = movies(:gone_with_the_wind)
241
+ assert_equal 3, m.rated_count
242
+ m.rate 4, users(:john)
243
+ m.rate 4, users(:bill)
244
+ m.rate 4, users(:sarah)
245
+ m.rate 4, users(:jane)
246
+ m.rate 4, users(:jill)
247
+ assert_equal 5, m.rated_count
248
+
249
+ c = cars(:expedition)
250
+ assert_equal 0, c.rated_count
251
+ c.rate 4
252
+ c.rate 4
253
+ c.rate 4
254
+ c.rate 4
255
+ c.rate 4
256
+ assert_equal 5, c.rated_count
257
+
258
+ b = books(:animal_farm)
259
+ assert_equal 4, b.rated_count
260
+ b.rate 4, Worker.find(users(:john).id)
261
+ b.rate 4, Worker.find(users(:bill).id)
262
+ b.rate 4, Worker.find(users(:sarah).id)
263
+ b.rate 4, Worker.find(users(:jane).id)
264
+ b.rate 4, Worker.find(users(:jill).id)
265
+ assert_equal 5, b.rated_count
266
+ end
267
+
268
+ def test_total
269
+ m = movies(:gone_with_the_wind)
270
+ assert_equal 13, m.rated_total
271
+ m.rate 4, users(:john)
272
+ m.rate 4, users(:bill)
273
+ m.rate 4, users(:sarah)
274
+ m.rate 4, users(:jane)
275
+ m.rate 4, users(:jill)
276
+ assert_equal 20, m.rated_total
277
+
278
+ c = cars(:expedition)
279
+ assert_equal 0, c.rated_total
280
+ c.rate 4
281
+ c.rate 4
282
+ c.rate 4
283
+ c.rate 4
284
+ c.rate 4
285
+ assert_equal 20, c.rated_total
286
+
287
+ b = books(:animal_farm)
288
+ assert_equal 12, b.rated_total
289
+ b.rate 4, Worker.find(users(:john).id)
290
+ b.rate 4, Worker.find(users(:bill).id)
291
+ b.rate 4, Worker.find(users(:sarah).id)
292
+ b.rate 4, Worker.find(users(:jane).id)
293
+ b.rate 4, Worker.find(users(:jill).id)
294
+ assert_equal 20, b.rated_total
295
+ end
296
+
297
+ def test_rated_by?
298
+ m = movies(:gone_with_the_wind)
299
+ m.rate 4, users(:john)
300
+ m.rate 4, users(:bill)
301
+ m.rate 4, users(:sarah)
302
+ m.rate 4, users(:jane)
303
+ m.rate 4, users(:jill)
304
+ m.unrate users(:jill)
305
+ m.unrate users(:sarah)
306
+ assert m.rated_by?(users(:john))
307
+ assert m.rated_by?(users(:bill))
308
+ assert m.rated_by?(users(:jane))
309
+ assert !m.rated_by?(users(:jill))
310
+ assert !m.rated_by?(users(:sarah))
311
+
312
+ b = books(:animal_farm)
313
+ b.rate 4, Worker.find(users(:john).id)
314
+ b.rate 4, Worker.find(users(:bill).id)
315
+ b.rate 4, Worker.find(users(:sarah).id)
316
+ b.rate 4, Worker.find(users(:jane).id)
317
+ b.rate 4, Worker.find(users(:jill).id)
318
+ b.unrate Worker.find(users(:john).id)
319
+ b.unrate Worker.find(users(:bill).id)
320
+ assert !b.rated_by?(Worker.find(users(:john).id) )
321
+ assert !b.rated_by?(Worker.find(users(:bill).id) )
322
+ assert b.rated_by?(Worker.find(users(:sarah).id))
323
+ assert b.rated_by?(Worker.find(users(:jane).id) )
324
+ assert b.rated_by?(Worker.find(users(:jill).id) )
325
+ end
326
+
327
+ def test_find_by_rating
328
+ cs = Car.find_by_rating 0
329
+ assert_equal 1, cs.size
330
+ assert_equal 'Ford Expedition', cs[0].title
331
+ cs = Car.find_by_rating 3
332
+ assert_equal 1, cs.size
333
+ assert_equal 'Toyota Camry', cs[0].title
334
+ cs = Car.find_by_rating 3.5
335
+ assert_equal 1, cs.size
336
+ assert_equal 'VW Golf', cs[0].title
337
+ cs = Car.find_by_rating 4, 0
338
+ check_returned_array cs, ['VW Golf', 'Carrera', 'VW Bug']
339
+ cs = Car.find_by_rating 3..4, 0
340
+ check_returned_array cs, ['Toyota Camry', 'VW Golf', 'Carrera', 'VW Bug']
341
+ cs = Car.find_by_rating 3..4
342
+ check_returned_array cs, ['Toyota Camry', 'VW Golf', 'VW Bug']
343
+ fs = Film.find_by_rating 1..4, 0
344
+ check_returned_array fs, ["Rambo 3", "Gone With The Wind", "Phantom Menace"]
345
+ cs = Car.find_by_rating 3..4, 0, false
346
+ check_returned_array cs, ['Toyota Camry', 'VW Golf', 'VW Bug']
347
+ cs = Car.find_by_rating 3..4.5, 0, false
348
+ check_returned_array cs, ['Toyota Camry', 'VW Golf', 'Carrera', 'VW Bug']
349
+ fs = Film.find_by_rating 1..4, 0, false
350
+ check_returned_array fs, ["Rambo 3", "Phantom Menace"]
351
+ ms = Movie.find_by_rating 5
352
+ check_returned_array ms, ["The Wizard of Oz"]
353
+ bs = Book.find_by_rating 3..3.7
354
+ check_returned_array bs, ["Alice in Wonderland", "Aminal Farm", "The Lord of the Rings", "Catch 22"]
355
+ bs = Book.find_by_rating 3..3.7, 0
356
+ check_returned_array bs, ["Alice in Wonderland", "Aminal Farm", "The Lord of the Rings"]
357
+ bs = Book.find_by_rating 1..3, 0
358
+ check_returned_array bs, ["Alice in Wonderland", "Aminal Farm", "The Lord of the Rings"]
359
+ bs = Book.find_by_rating 3, 0
360
+ check_returned_array bs, ["Alice in Wonderland", "Aminal Farm", "The Lord of the Rings"]
361
+
362
+ bs = Book.find_by_rating 3..3.7, 0, false
363
+ check_returned_array bs, ["Alice in Wonderland", "Aminal Farm", "The Lord of the Rings", "Catch 22"]
364
+ bs = Book.find_by_rating 1..3.3, 0, false
365
+ check_returned_array bs, ["Alice in Wonderland", "Aminal Farm", "The Lord of the Rings"]
366
+ bs = Book.find_by_rating 3.75, 0, false
367
+ check_returned_array bs, ["Shogun"]
368
+ end
369
+
370
+ def test_find_rated_by
371
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { Car.find_rated_by 5 }
372
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { Movie.find_rated_by nil }
373
+ assert_raise(ActiveRecord::Acts::Rated::RateError) { Movie.find_rated_by 1 }
374
+ ms = Movie.find_rated_by users(:john)
375
+ check_returned_array ms, ["Gone With The Wind", "The Wizard of Oz", "Phantom Menace", "Rambo 3"]
376
+ ms = Movie.find_rated_by users(:jack)
377
+ check_returned_array ms, []
378
+ m = Movie.new :title => 'Borat'
379
+ m.save
380
+ m.rate 5, users(:jack)
381
+ ms = Movie.find_rated_by users(:jack)
382
+ check_returned_array ms, ["Borat"]
383
+ bs = Book.find_rated_by Worker.find(users(:john).id)
384
+ check_returned_array bs, ["The Lord of the Rings", "Alice in Wonderland", "Catch 22", "Aminal Farm"]
385
+ fs = Film.find_rated_by users(:john)
386
+ check_returned_array fs, ["Gone With The Wind", "Phantom Menace", "The Wizard of Oz", "Rambo 3"]
387
+ f = Film.new :title => 'Kill Bill'
388
+ f.save
389
+ f.rate 4, users(:jill)
390
+ fs = Film.find_rated_by users(:jill)
391
+ check_returned_array fs, ["Rambo 3", "Phantom Menace", "Kill Bill"]
392
+ end
393
+
394
+ def test_associations
395
+ assert User.new.respond_to?(:ratings)
396
+ assert !Mechanic.new.respond_to?(:ratings)
397
+ assert Book.new.respond_to?(:ratings)
398
+ assert Book.new.respond_to?(:raters)
399
+ assert Car.new.respond_to?(:ratings)
400
+ assert !Car.new.respond_to?(:raters)
401
+ assert Truck.new.respond_to?(:ratings)
402
+ assert !Truck.new.respond_to?(:raters)
403
+ end
404
+
405
+ # This just test that the fixtures data makes sense
406
+ def test_all_fixtures
407
+ [Car, Movie, Book, Video, Tape, Truck, Film].each do |c|
408
+ c.find(:all).each do |o|
409
+ check_average o, o.rating_average
410
+ end
411
+ end
412
+ end
413
+
414
+ def check_average obj, value
415
+ assert_equal (value * 100).to_i, (obj.rating_average * 100).to_i
416
+ assert_equal (obj.ratings.average(:rating) * 100).to_i, (obj.rating_average * 100).to_i
417
+ end
418
+
419
+ def check_returned_array ar, expected_list
420
+ names = ar.collect {|e| e.title }
421
+ assert_equal expected_list.size, names.size
422
+ assert_equal [], names - expected_list
423
+ end
424
+
425
+ end
426
+
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts-as-rated
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.4"
5
+ platform: ruby
6
+ authors:
7
+ - Guy Noar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-08 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Flexible, configurable, and easy to use with the defaults. Supports 3 different ways to manage rating statistics.
17
+ email: guy.naor@famundo.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - acts-as-rated.gemspec
26
+ - init.rb
27
+ - lib/acts_as_rated.rb
28
+ - MIT-LICENSE
29
+ - Rakefile
30
+ - README
31
+ has_rdoc: true
32
+ homepage: http://github.com/jasherai/acts-as-rated
33
+ licenses: []
34
+
35
+ post_install_message:
36
+ rdoc_options: []
37
+
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.3.5
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: Rails plugin rating system for ActiveRecord models.
59
+ test_files:
60
+ - test/rated_test.rb