acts_as_mongo_rateable 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/Manifest +13 -0
- data/README.markdown +88 -0
- data/Rakefile +35 -0
- data/acts_as_mongo_rateable.gemspec +33 -0
- data/install.rb +1 -0
- data/lib/acts_as_mongo_rateable/acts_as_mongo_rateable.rb +109 -0
- data/lib/acts_as_mongo_rateable/app/models/rating.rb +72 -0
- data/lib/mongo_rateable.rb +1 -0
- data/rails/init.rb +2 -0
- data/tasks/mongo_rateable_tasks.rake +4 -0
- data/test/mongo_rateable_test.rb +171 -0
- data/test/test_helper.rb +58 -0
- data/uninstall.rb +1 -0
- data.tar.gz.sig +3 -0
- metadata +112 -0
- metadata.gz.sig +0 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [name of plugin creator]
|
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/Manifest
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
MIT-LICENSE
|
2
|
+
README.markdown
|
3
|
+
Rakefile
|
4
|
+
install.rb
|
5
|
+
lib/acts_as_mongo_rateable/acts_as_mongo_rateable.rb
|
6
|
+
lib/acts_as_mongo_rateable/app/models/rating.rb
|
7
|
+
lib/mongo_rateable.rb
|
8
|
+
rails/init.rb
|
9
|
+
tasks/mongo_rateable_tasks.rake
|
10
|
+
test/mongo_rateable_test.rb
|
11
|
+
test/test_helper.rb
|
12
|
+
uninstall.rb
|
13
|
+
Manifest
|
data/README.markdown
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
ActsAsMongoRateable (with weights!)
|
2
|
+
===================================
|
3
|
+
|
4
|
+
Inspired by the old Rails+AR standby "acts_as_rateable," this rating plugin works with MongoDB+MongoMapper
|
5
|
+
and has weighted ratings, as well as bayesian and straight averages, and some friendly class-level helpers.
|
6
|
+
|
7
|
+
Intends to be super-performant by taking advantage of the benefits of document-driven db denormalization.
|
8
|
+
|
9
|
+
Requirements
|
10
|
+
------------
|
11
|
+
|
12
|
+
- MongoDB
|
13
|
+
- MongoMapper gem
|
14
|
+
- Expects you to have a User model that includes MongoMapper::Document
|
15
|
+
|
16
|
+
Installation
|
17
|
+
------------
|
18
|
+
|
19
|
+
Install the plugin:
|
20
|
+
|
21
|
+
./script/plugin install git://github.com/mepatterson/acts_as_mongo_rateable.git
|
22
|
+
|
23
|
+
Add the following 2 lines to the Model class that you want to make rateable:
|
24
|
+
|
25
|
+
include ActsAsMongoRateable
|
26
|
+
RATING_RANGE = (1..5)
|
27
|
+
|
28
|
+
Obviously, change the rating range if you want to rate on a 10-star system or a 14-star or whatever.
|
29
|
+
|
30
|
+
Usage
|
31
|
+
-----
|
32
|
+
|
33
|
+
class User
|
34
|
+
include MongoMapper::Document
|
35
|
+
end
|
36
|
+
|
37
|
+
class Widget
|
38
|
+
include ActsAsMongoRateable
|
39
|
+
RATING_RANGE = (1..5)
|
40
|
+
include MongoMapper::Document
|
41
|
+
end
|
42
|
+
|
43
|
+
widget = Widget.first
|
44
|
+
|
45
|
+
To rate it:
|
46
|
+
|
47
|
+
widget.rate(score, user, weight)
|
48
|
+
|
49
|
+
- score must be an Integer within your RATING_RANGE
|
50
|
+
- user is the User who is rating this widget
|
51
|
+
- weight is optional; defaults to 1)
|
52
|
+
|
53
|
+
Now try all these fun methods:
|
54
|
+
|
55
|
+
widget.average_rating
|
56
|
+
|
57
|
+
widget.bayesian_rating
|
58
|
+
|
59
|
+
widget.rating_stats
|
60
|
+
|
61
|
+
And some useful class methods:
|
62
|
+
|
63
|
+
Widget.highest_rated(how_many)
|
64
|
+
|
65
|
+
Widget.most_rated(how_many)
|
66
|
+
|
67
|
+
Widget.most_rated_by_authorities(how_many)
|
68
|
+
|
69
|
+
Widget.highest_bayesian_rated(how_many)
|
70
|
+
|
71
|
+
('how_many' is a limit and is optional. i.e. Do you want a highest_rated list of 5, 10, 15 widgets?
|
72
|
+
Defaults to just 1 if you don't pass any argument.)
|
73
|
+
|
74
|
+
Future
|
75
|
+
------
|
76
|
+
- Tests (I have tests in the project I cut this from, but they need to be extracted out and I'm lazy)
|
77
|
+
- More helper methods
|
78
|
+
- Performance improvements as I come across the need
|
79
|
+
- Investigate using map/reduce to improve the efficiency of the bayesian calc (?)
|
80
|
+
|
81
|
+
Thanks To...
|
82
|
+
------------
|
83
|
+
- John Nunemaker and the rest of the folks on the MongoMapper Google Group
|
84
|
+
- The MongoDB peoples and the MongoDB Google Group
|
85
|
+
- juixe for the original acts_as_rateable plugin for ActiveRecord
|
86
|
+
- sunlightlabs 'datacatalog-api', from which I borrowed the ratings_stats hash methodology
|
87
|
+
|
88
|
+
Copyright (c) 2009 [M. E. Patterson], released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
|
7
|
+
desc 'Default: run unit tests.'
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
desc 'Test the mongo_rateable plugin.'
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
12
|
+
t.libs << 'lib'
|
13
|
+
t.libs << 'test'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Generate documentation for the mongo_rateable plugin.'
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
21
|
+
rdoc.title = 'MongoRateable'
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
Echoe.new('acts_as_mongo_rateable', '0.2.0') do |p|
|
29
|
+
p.description = "A ratings system for Rails apps using MongoDB, with bayesian and straight averages, and weighting."
|
30
|
+
p.url = "http://github.com/mepatterson/acts_as_mongo_rateable"
|
31
|
+
p.author = "M. E. Patterson"
|
32
|
+
p.email = "madraziel @nospam@ gmail.com"
|
33
|
+
p.ignore_pattern = ["tmp/*", "script/*"]
|
34
|
+
p.development_dependencies = []
|
35
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{acts_as_mongo_rateable}
|
5
|
+
s.version = "0.2.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["M. E. Patterson"]
|
9
|
+
s.cert_chain = ["/Users/mattp/gem-public_cert.pem"]
|
10
|
+
s.date = %q{2010-06-04}
|
11
|
+
s.description = %q{A ratings system for Rails apps using MongoDB, with bayesian and straight averages, and weighting.}
|
12
|
+
s.email = %q{madraziel @nospam@ gmail.com}
|
13
|
+
s.extra_rdoc_files = ["README.markdown", "lib/acts_as_mongo_rateable/acts_as_mongo_rateable.rb", "lib/acts_as_mongo_rateable/app/models/rating.rb", "lib/mongo_rateable.rb", "tasks/mongo_rateable_tasks.rake"]
|
14
|
+
s.files = ["MIT-LICENSE", "README.markdown", "Rakefile", "install.rb", "lib/acts_as_mongo_rateable/acts_as_mongo_rateable.rb", "lib/acts_as_mongo_rateable/app/models/rating.rb", "lib/mongo_rateable.rb", "rails/init.rb", "tasks/mongo_rateable_tasks.rake", "test/mongo_rateable_test.rb", "test/test_helper.rb", "uninstall.rb", "Manifest", "acts_as_mongo_rateable.gemspec"]
|
15
|
+
s.homepage = %q{http://github.com/mepatterson/acts_as_mongo_rateable}
|
16
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Acts_as_mongo_rateable", "--main", "README.markdown"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubyforge_project = %q{acts_as_mongo_rateable}
|
19
|
+
s.rubygems_version = %q{1.3.7}
|
20
|
+
s.signing_key = %q{/Users/mattp/gem-private_key.pem}
|
21
|
+
s.summary = %q{A ratings system for Rails apps using MongoDB, with bayesian and straight averages, and weighting.}
|
22
|
+
s.test_files = ["test/mongo_rateable_test.rb", "test/test_helper.rb"]
|
23
|
+
|
24
|
+
if s.respond_to? :specification_version then
|
25
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
26
|
+
s.specification_version = 3
|
27
|
+
|
28
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
29
|
+
else
|
30
|
+
end
|
31
|
+
else
|
32
|
+
end
|
33
|
+
end
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module ActsAsMongoRateable
|
2
|
+
|
3
|
+
module ClassMethods
|
4
|
+
def highest_rated(limit=1)
|
5
|
+
all({:order => "rating_stats.average DESC", :limit => limit})
|
6
|
+
end
|
7
|
+
|
8
|
+
def most_rated(limit=1)
|
9
|
+
all({:order => "rating_stats.count DESC", :limit => limit})
|
10
|
+
end
|
11
|
+
|
12
|
+
def most_rated_by_authorities(limit=1)
|
13
|
+
all({:order => "rating_stats.sum_of_weights DESC", :limit => limit})
|
14
|
+
end
|
15
|
+
|
16
|
+
# TO DO this is awful, awful, awful! make it faster using map/reduce
|
17
|
+
def highest_bayesian_rated(limit=1)
|
18
|
+
stats = all({:select => 'id, rating_stats'})
|
19
|
+
all.sort_by do |doc|
|
20
|
+
rating = doc.bayesian_rating(stats)
|
21
|
+
doc.rating_stats['bayesian_rating'] = rating
|
22
|
+
rating
|
23
|
+
end.reverse[0,limit]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
|
30
|
+
def delete_all_ratings
|
31
|
+
ratings.delete_all
|
32
|
+
end
|
33
|
+
|
34
|
+
def average_rating
|
35
|
+
rating_stats['average']
|
36
|
+
end
|
37
|
+
|
38
|
+
def bayesian_rating(stats=nil)
|
39
|
+
return 0 if rating_stats['count'] == 0
|
40
|
+
stats ||= self.class.all({:select => 'id, rating_stats'})
|
41
|
+
system_counts = stats.map{ |p| [ p.id.to_s, p.rating_stats['count'] ] }
|
42
|
+
avg_rating = stats.map{|p| p.rating_stats['average'] || 0 }.sum / stats.size.to_f
|
43
|
+
avg_num_votes = system_counts.inject(0){|sum, r| sum += r.to_a.flatten[1] } / system_counts.size.to_f
|
44
|
+
my_rating = rating_stats['average'] || 0
|
45
|
+
my_count = rating_stats['count']
|
46
|
+
( (avg_num_votes * avg_rating) + (my_count * my_rating) ) / (avg_num_votes + my_count)
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete_ratings_by_user(user)
|
50
|
+
return false unless user
|
51
|
+
return 0 if ratings.blank?
|
52
|
+
ratings.delete_all(:user_id => user.id.to_s)
|
53
|
+
self.reload
|
54
|
+
end
|
55
|
+
|
56
|
+
def rate(value, user = nil, weight = 1)
|
57
|
+
delete_ratings_by_user(user)
|
58
|
+
validate_rating!(value)
|
59
|
+
r = Rating.new({
|
60
|
+
:value => value,
|
61
|
+
:user_id => user.id,
|
62
|
+
:rateable_id => self.id,
|
63
|
+
:rateable_class => self.class.to_s,
|
64
|
+
:weight => weight
|
65
|
+
})
|
66
|
+
self.ratings << r
|
67
|
+
self.reload
|
68
|
+
r
|
69
|
+
end
|
70
|
+
|
71
|
+
# returns the Rating object found if user has rated this project, else returns nil
|
72
|
+
def rated_by_user?(user)
|
73
|
+
return false unless user
|
74
|
+
ratings.detect{ |r| r.user_id == user.id}
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def validate_rating!(value)
|
80
|
+
if (range = self.class::RATING_RANGE) and !range.include?(value.to_i)
|
81
|
+
raise ArgumentError, "Rating not in range #{range}. Rating provided was #{value}."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.included(receiver)
|
88
|
+
receiver.class_eval do
|
89
|
+
many :ratings, :foreign_key => 'rateable_id', :dependent => :destroy
|
90
|
+
key :rating_stats, Hash, :default => {
|
91
|
+
:total => 0,
|
92
|
+
:count => 0,
|
93
|
+
:sum_of_weights => 0,
|
94
|
+
:average => nil
|
95
|
+
}
|
96
|
+
end
|
97
|
+
receiver.extend ClassMethods
|
98
|
+
receiver.send :include, InstanceMethods
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
%w{ models observers }.each do |dir|
|
105
|
+
path = File.join(File.dirname(__FILE__), 'app', dir)
|
106
|
+
$LOAD_PATH << path
|
107
|
+
ActiveSupport::Dependencies.load_paths << path
|
108
|
+
ActiveSupport::Dependencies.load_once_paths.delete(path)
|
109
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class Rating
|
2
|
+
include MongoMapper::Document
|
3
|
+
|
4
|
+
key :user_id, ObjectId, :required => true
|
5
|
+
key :rateable_id, ObjectId, :required => true
|
6
|
+
key :rateable_class, String, :required => true
|
7
|
+
key :value, Integer, :required => true
|
8
|
+
key :previous_value, Integer, :default => 0
|
9
|
+
key :weight, Integer, :default => 1
|
10
|
+
timestamps!
|
11
|
+
|
12
|
+
ensure_index :user_id
|
13
|
+
ensure_index :rateable_id
|
14
|
+
ensure_index :rateable_class
|
15
|
+
ensure_index :created_at
|
16
|
+
|
17
|
+
belongs_to :user
|
18
|
+
|
19
|
+
after_create :set_rating_stats
|
20
|
+
after_update :update_rating_stats
|
21
|
+
before_update :set_previous_value
|
22
|
+
after_destroy :reduce_rating_stats
|
23
|
+
|
24
|
+
def set_rating_stats
|
25
|
+
doc = find_rated_document!
|
26
|
+
count = (doc.rating_stats[:count] += 1)
|
27
|
+
total = (doc.rating_stats[:total] += ( value * weight ))
|
28
|
+
sow = (doc.rating_stats[:sum_of_weights] += weight)
|
29
|
+
doc.rating_stats[:average] = total.to_f / sow
|
30
|
+
doc.save!
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_previous_value
|
34
|
+
rating_from_db = Rating.find_by_id(id)
|
35
|
+
previous_value = (rating_from_db.value * weight) if rating_from_db
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_rating_stats
|
39
|
+
doc = find_rated_document!
|
40
|
+
value_delta = (value * weight) - (previous_value * weight)
|
41
|
+
total = (doc.rating_stats[:total] += value_delta)
|
42
|
+
count = doc.rating_stats[:count]
|
43
|
+
doc.rating_stats[:average] = total.to_f / count
|
44
|
+
doc.save
|
45
|
+
end
|
46
|
+
|
47
|
+
def reduce_rating_stats
|
48
|
+
doc = find_rated_document
|
49
|
+
return if doc.nil?
|
50
|
+
count = (doc.rating_stats[:count] -= 1)
|
51
|
+
doc.rating_stats[:total] -= (value * weight)
|
52
|
+
doc.rating_stats[:sum_of_weights] -= weight
|
53
|
+
doc.rating_stats[:average] = nil if count == 0
|
54
|
+
doc.save
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# == Various Instance Methods
|
59
|
+
def find_rated_document
|
60
|
+
klass = rateable_class.constantize
|
61
|
+
klass.find(rateable_id.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_rated_document!
|
65
|
+
doc = find_rated_document
|
66
|
+
raise "Associated document not found" if doc.nil?
|
67
|
+
doc
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'acts_as_mongo_rateable/acts_as_mongo_rateable'
|
data/rails/init.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class MongoRateableTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def create_user(name)
|
6
|
+
u = User.create({:name => name})
|
7
|
+
puts u.errors unless u.valid?
|
8
|
+
u
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_multiple_raters
|
12
|
+
@m_rater_1 = create_user "m_rater_1"
|
13
|
+
@m_rater_2 = create_user "m_rater_2"
|
14
|
+
@m_rater_3 = create_user "m_rater_3"
|
15
|
+
[@m_rater_1, @m_rater_2, @m_rater_3]
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_multiple_widgets
|
19
|
+
@widget = @owner.widgets.create({:name => "Test Widget"})
|
20
|
+
@widget_2 = @owner.widgets.create({:name => "Test Widget 2"})
|
21
|
+
@widget_3 = @owner.widgets.create({:name => "Test Widget 3"})
|
22
|
+
[@widget, @widget_2, @widget_3]
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_multiple_dongles
|
26
|
+
@dongle = @owner.dongles.create({:name => "Test dongle"})
|
27
|
+
@dongle_2 = @owner.dongles.create({:name => "Test dongle 2"})
|
28
|
+
@dongle_3 = @owner.dongles.create({:name => "Test dongle 3"})
|
29
|
+
[@dongle, @dongle_2, @dongle_3]
|
30
|
+
end
|
31
|
+
|
32
|
+
def rate_this_many_times(obj, count, authority=1)
|
33
|
+
x = 0
|
34
|
+
count.times do
|
35
|
+
x += 1
|
36
|
+
rater = create_user "fake_rater_#{x}"
|
37
|
+
obj.rate(4, rater, authority)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_and_randomly_rate_multiple_widgets
|
42
|
+
raters = load_multiple_raters
|
43
|
+
widgets = load_multiple_widgets
|
44
|
+
widgets.each do |w|
|
45
|
+
raters.each {|r| w.rate(rand(5)+1, r, rand(3)+1) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_and_randomly_rate_multiple_dongles
|
50
|
+
raters = load_multiple_raters
|
51
|
+
dongles = load_multiple_dongles
|
52
|
+
dongles.each do |d|
|
53
|
+
raters.each {|r| d.rate(rand(5)+1, r, rand(3)+1) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def multi_rate(obj)
|
58
|
+
load_multiple_raters
|
59
|
+
obj.rate(4, @m_rater_1, 1)
|
60
|
+
obj.rate(2, @m_rater_2, 3)
|
61
|
+
obj.rate(5, @m_rater_3, 5)
|
62
|
+
end
|
63
|
+
|
64
|
+
def multi_rate_many_widgets
|
65
|
+
load_multiple_widgets
|
66
|
+
raters = load_multiple_raters
|
67
|
+
raters.each do |r|
|
68
|
+
@widget.rate(5, r, 2)
|
69
|
+
@widget_2.rate(1, r, 2)
|
70
|
+
@widget_3.rate(3, r, 2)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def setup
|
75
|
+
@owner = create_user 'owner'
|
76
|
+
@rater = create_user 'rater'
|
77
|
+
@widget = @owner.widgets.create({:name => "Test Widget"})
|
78
|
+
end
|
79
|
+
|
80
|
+
test "single rating on a widget produces the correct rating stats" do
|
81
|
+
@widget.rate(3, @rater, 2)
|
82
|
+
expected_stats = {"total" => 6, "count" => 1, "average" => 3.to_f, "sum_of_weights" => 2}
|
83
|
+
%w( total count average sum_of_weights).each do |method|
|
84
|
+
assert_equal expected_stats[method], @widget.rating_stats[method]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
test "multiple ratings on a widget produce the correct rating stats" do
|
89
|
+
multi_rate(@widget)
|
90
|
+
expected_stats = {"total" => 35, "count" => 3, "average" => 3.88888888888889, "sum_of_weights" => 9}
|
91
|
+
%w( total count sum_of_weights).each do |method|
|
92
|
+
assert_equal expected_stats[method], @widget.rating_stats[method]
|
93
|
+
end
|
94
|
+
# doing average seperately, since we don't need to test the whole long float
|
95
|
+
assert_equal sprintf('%.2f',expected_stats['average']), sprintf('%.2f',@widget.rating_stats['average'])
|
96
|
+
end
|
97
|
+
|
98
|
+
test "deleting all ratings for a specific widget works" do
|
99
|
+
load_and_randomly_rate_multiple_widgets
|
100
|
+
assert_equal 3, @widget.ratings.count
|
101
|
+
assert_equal 9, Rating.count
|
102
|
+
@widget.delete_all_ratings
|
103
|
+
assert_equal 0, @widget.ratings.count
|
104
|
+
assert_equal 6, Rating.count
|
105
|
+
end
|
106
|
+
|
107
|
+
test "deleting all ratings for a particular object doesn't delete ratings for other object" do
|
108
|
+
load_and_randomly_rate_multiple_widgets
|
109
|
+
load_and_randomly_rate_multiple_dongles
|
110
|
+
assert_equal 18, Rating.count
|
111
|
+
@widget.delete_all_ratings
|
112
|
+
assert_equal 0, @widget.ratings.count
|
113
|
+
assert_equal 3, @dongle.ratings.count
|
114
|
+
end
|
115
|
+
|
116
|
+
test "rated_by_user? works" do
|
117
|
+
multi_rate(@widget)
|
118
|
+
assert @widget.rated_by_user?(@m_rater_1)
|
119
|
+
end
|
120
|
+
|
121
|
+
test "delete by user only deletes ratings by that user for that object" do
|
122
|
+
@dongle = @owner.dongles.create({:name => "Test dongle"})
|
123
|
+
multi_rate(@widget)
|
124
|
+
widget_rater = @m_rater_1
|
125
|
+
multi_rate(@dongle)
|
126
|
+
assert_equal 6, Rating.count
|
127
|
+
assert_equal 3, @widget.ratings.count
|
128
|
+
assert @widget.rated_by_user?(widget_rater)
|
129
|
+
@widget.delete_ratings_by_user(widget_rater)
|
130
|
+
assert ! @widget.rated_by_user?(widget_rater)
|
131
|
+
assert_equal 2, @widget.ratings.count
|
132
|
+
assert_equal 5, Rating.count
|
133
|
+
end
|
134
|
+
|
135
|
+
test "bayesian_rating returns correct number" do
|
136
|
+
multi_rate_many_widgets
|
137
|
+
assert_equal sprintf('%.2f',3.82), sprintf('%.2f',@widget.bayesian_rating)
|
138
|
+
assert_equal sprintf('%.2f',1.54), sprintf('%.2f',@widget_2.bayesian_rating)
|
139
|
+
assert_equal sprintf('%.2f',2.68), sprintf('%.2f',@widget_3.bayesian_rating)
|
140
|
+
end
|
141
|
+
|
142
|
+
test "bayesian_rating for one class of objects not corrupted by other ratings for other classes" do
|
143
|
+
multi_rate_many_widgets
|
144
|
+
load_and_randomly_rate_multiple_dongles
|
145
|
+
assert_equal sprintf('%.2f',3.82), sprintf('%.2f',@widget.bayesian_rating)
|
146
|
+
assert_equal sprintf('%.2f',1.54), sprintf('%.2f',@widget_2.bayesian_rating)
|
147
|
+
assert_equal sprintf('%.2f',2.68), sprintf('%.2f',@widget_3.bayesian_rating)
|
148
|
+
end
|
149
|
+
|
150
|
+
test "highest_rated returns correct widgets in order" do
|
151
|
+
multi_rate_many_widgets
|
152
|
+
assert_equal [@widget, @widget_3, @widget_2], Widget.highest_rated(3)
|
153
|
+
end
|
154
|
+
|
155
|
+
test "most_rated returns correct widgets in order" do
|
156
|
+
load_multiple_widgets
|
157
|
+
rate_this_many_times(@widget, 6)
|
158
|
+
rate_this_many_times(@widget_2, 1)
|
159
|
+
rate_this_many_times(@widget_3, 3)
|
160
|
+
assert_equal [@widget, @widget_3, @widget_2], Widget.most_rated(3)
|
161
|
+
end
|
162
|
+
|
163
|
+
test "most_rated_by_authorities returns correct widgets in order" do
|
164
|
+
load_multiple_widgets
|
165
|
+
rate_this_many_times(@widget, 6, 2)
|
166
|
+
rate_this_many_times(@widget_2, 1, 5)
|
167
|
+
rate_this_many_times(@widget_3, 3, 1)
|
168
|
+
assert_equal [@widget, @widget_2, @widget_3], Widget.most_rated_by_authorities(3)
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/test_case'
|
4
|
+
|
5
|
+
ENV['RAILS_ENV'] = 'test'
|
6
|
+
ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
|
7
|
+
|
8
|
+
require 'test/unit'
|
9
|
+
require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
|
10
|
+
|
11
|
+
class ActiveSupport::TestCase
|
12
|
+
# Drop all columns after each test case.
|
13
|
+
def teardown
|
14
|
+
MongoMapper.database.collections.each do |coll|
|
15
|
+
coll.drop
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Make sure that each test case has a teardown
|
20
|
+
# method to clear the db after each test.
|
21
|
+
def inherited(base)
|
22
|
+
base.define_method teardown do
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# kinda weird, but we have to do this so we can ignore the app's User class and use our own for testing
|
29
|
+
Object.send(:remove_const, :User) if Object.const_defined?(:User)
|
30
|
+
|
31
|
+
class User
|
32
|
+
include MongoMapper::Document
|
33
|
+
key :name, String
|
34
|
+
has_many :widgets
|
35
|
+
has_many :dongles
|
36
|
+
end
|
37
|
+
|
38
|
+
class Widget
|
39
|
+
include MongoMapper::Document
|
40
|
+
include ActsAsMongoRateable
|
41
|
+
RATING_RANGE = (1..5)
|
42
|
+
|
43
|
+
belongs_to :user
|
44
|
+
|
45
|
+
key :user_id, ObjectId
|
46
|
+
key :name, String
|
47
|
+
end
|
48
|
+
|
49
|
+
class Dongle
|
50
|
+
include MongoMapper::Document
|
51
|
+
include ActsAsMongoRateable
|
52
|
+
RATING_RANGE = (1..5)
|
53
|
+
|
54
|
+
belongs_to :user
|
55
|
+
|
56
|
+
key :user_id, ObjectId
|
57
|
+
key :name, String
|
58
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
data.tar.gz.sig
ADDED
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_mongo_rateable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- M. E. Patterson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain:
|
17
|
+
- |
|
18
|
+
-----BEGIN CERTIFICATE-----
|
19
|
+
MIIDNDCCAhygAwIBAgIBADANBgkqhkiG9w0BAQUFADBAMRIwEAYDVQQDDAltYWRy
|
20
|
+
YXppZWwxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
|
21
|
+
bTAeFw0xMDA2MDUwNDUxMThaFw0xMTA2MDUwNDUxMThaMEAxEjAQBgNVBAMMCW1h
|
22
|
+
ZHJhemllbDEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYD
|
23
|
+
Y29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2YYU8nBcWrz3Gls3
|
24
|
+
B4tKCLGU/v11vuLeDU51r0/mUL/ev4GAZOrlBRaIn4U3J53W4Wa3fHGNqDyB7dt4
|
25
|
+
eJS2a//eBv5MXtDxS0kuOeLen4sJiACN7WzoX/WaMiCdeLFebRFr6zDKtYMQZ1lM
|
26
|
+
x+DWXbFLBuWfihZ8TapkBBG8Yuw2J+OcJla7t0IR3KHJSpOe0e9D/rHydly2v7kH
|
27
|
+
6naqt5n4jtpTXrmgLeGUQeVR60BJrHIbHXjgUCkPz3b/prreaRl/a4Cufv0cTPMD
|
28
|
+
BFSOlmK6AKLii9gdaZMnLbF7b1Jc3rAcYNAsLHoUBGoq0Top8CTC732YrNZ7NmFp
|
29
|
+
/Jn0nQIDAQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU
|
30
|
+
drmilB9ksqsW9hFPopqQEhcFdh8wDQYJKoZIhvcNAQEFBQADggEBAHPZLmxi4DYD
|
31
|
+
qdSsUv4oTlWoK5nCbEpnJRpJZXn1rn30Zaoc5v7kQfVwz/x/4uZmLdvfysZmuvKZ
|
32
|
+
GTMlYv/qSylO0SqbZeKhvsehQ+ENQdTon10zPtVvvnxiUE/As79eRmmxNGypJnwy
|
33
|
+
WZH2x1I+Hd25mRbt1UspT2vmuEwTgzkmxGHnl7cmiO4BYXlzHHk8KRSEdIDZUm5T
|
34
|
+
B+pavzaE15aDJrbpy+/awgxQEZb7O/mgzkFoTKlgV4VyDWSDjtU+aEI1sJk0DiSs
|
35
|
+
Zm3n8B2/BZXgeEcymjLxZGMrvkax12f02NF2l+C8f0/DzS+jlVsewUzux0WmYCR5
|
36
|
+
47YifzlLjIM=
|
37
|
+
-----END CERTIFICATE-----
|
38
|
+
|
39
|
+
date: 2010-06-04 00:00:00 -05:00
|
40
|
+
default_executable:
|
41
|
+
dependencies: []
|
42
|
+
|
43
|
+
description: A ratings system for Rails apps using MongoDB, with bayesian and straight averages, and weighting.
|
44
|
+
email: madraziel @nospam@ gmail.com
|
45
|
+
executables: []
|
46
|
+
|
47
|
+
extensions: []
|
48
|
+
|
49
|
+
extra_rdoc_files:
|
50
|
+
- README.markdown
|
51
|
+
- lib/acts_as_mongo_rateable/acts_as_mongo_rateable.rb
|
52
|
+
- lib/acts_as_mongo_rateable/app/models/rating.rb
|
53
|
+
- lib/mongo_rateable.rb
|
54
|
+
- tasks/mongo_rateable_tasks.rake
|
55
|
+
files:
|
56
|
+
- MIT-LICENSE
|
57
|
+
- README.markdown
|
58
|
+
- Rakefile
|
59
|
+
- install.rb
|
60
|
+
- lib/acts_as_mongo_rateable/acts_as_mongo_rateable.rb
|
61
|
+
- lib/acts_as_mongo_rateable/app/models/rating.rb
|
62
|
+
- lib/mongo_rateable.rb
|
63
|
+
- rails/init.rb
|
64
|
+
- tasks/mongo_rateable_tasks.rake
|
65
|
+
- test/mongo_rateable_test.rb
|
66
|
+
- test/test_helper.rb
|
67
|
+
- uninstall.rb
|
68
|
+
- Manifest
|
69
|
+
- acts_as_mongo_rateable.gemspec
|
70
|
+
has_rdoc: true
|
71
|
+
homepage: http://github.com/mepatterson/acts_as_mongo_rateable
|
72
|
+
licenses: []
|
73
|
+
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options:
|
76
|
+
- --line-numbers
|
77
|
+
- --inline-source
|
78
|
+
- --title
|
79
|
+
- Acts_as_mongo_rateable
|
80
|
+
- --main
|
81
|
+
- README.markdown
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 3
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 11
|
99
|
+
segments:
|
100
|
+
- 1
|
101
|
+
- 2
|
102
|
+
version: "1.2"
|
103
|
+
requirements: []
|
104
|
+
|
105
|
+
rubyforge_project: acts_as_mongo_rateable
|
106
|
+
rubygems_version: 1.3.7
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: A ratings system for Rails apps using MongoDB, with bayesian and straight averages, and weighting.
|
110
|
+
test_files:
|
111
|
+
- test/mongo_rateable_test.rb
|
112
|
+
- test/test_helper.rb
|
metadata.gz.sig
ADDED
Binary file
|