voteable_mongo 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/.rvmrc +2 -0
- data/.watchr +23 -0
- data/CHANGELOG.rdoc +71 -0
- data/Gemfile +4 -0
- data/README.rdoc +159 -0
- data/Rakefile +10 -0
- data/TODO +3 -0
- data/lib/voteable_mongo.rb +10 -0
- data/lib/voteable_mongo/railtie.rb +17 -0
- data/lib/voteable_mongo/railties/database.rake +18 -0
- data/lib/voteable_mongo/version.rb +3 -0
- data/lib/voteable_mongo/voteable.rb +199 -0
- data/lib/voteable_mongo/voteable/tasks.rb +162 -0
- data/lib/voteable_mongo/voteable/votes.rb +19 -0
- data/lib/voteable_mongo/voteable/voting.rb +229 -0
- data/lib/voteable_mongo/voter.rb +97 -0
- data/spec/.rspec +1 -0
- data/spec/models/category.rb +10 -0
- data/spec/models/comment.rb +13 -0
- data/spec/models/post.rb +13 -0
- data/spec/models/user.rb +4 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/voteable_mongo/tasks_spec.rb +33 -0
- data/spec/voteable_mongo/voteable_spec.rb +426 -0
- data/spec/voteable_mongo/voter_spec.rb +148 -0
- data/voteable_mongo.gemspec +26 -0
- metadata +131 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/.watchr
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# vim:set filetype=ruby:
|
2
|
+
def run(cmd)
|
3
|
+
puts cmd
|
4
|
+
system cmd
|
5
|
+
end
|
6
|
+
|
7
|
+
def spec(file)
|
8
|
+
if File.exists?(file)
|
9
|
+
run("rspec #{file}")
|
10
|
+
else
|
11
|
+
puts("Spec: #{file} does not exist.")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
watch("spec/.*/*_spec\.rb") do |match|
|
16
|
+
puts(match[0])
|
17
|
+
spec(match[0])
|
18
|
+
end
|
19
|
+
|
20
|
+
watch("lib/(.*/.*)\.rb") do |match|
|
21
|
+
puts(match[1])
|
22
|
+
spec("spec/#{match[1]}_spec.rb")
|
23
|
+
end
|
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
== 0.8.1
|
2
|
+
* Fix gem release bug
|
3
|
+
|
4
|
+
== 0.8.0
|
5
|
+
* Rename to voteable_mongo to support other MongoDB Object-Document Mappers like MongoMapper
|
6
|
+
* Minor fixes and refactoring
|
7
|
+
|
8
|
+
== 0.7.4
|
9
|
+
* Add Votee#up_voters(VoterClass), Votee#down_voters(VoterClass), Votee#voters(VoterClass)
|
10
|
+
* Add Voter scopes: Voter.up_voted_for(votee), Voter.down_voted_for(votee), Voter.voted_for(votee)
|
11
|
+
* Add voteable ..., :index => true options
|
12
|
+
* Optimization on unvote and revote validations
|
13
|
+
* Fix for :up & :down points are nil in rake tasks
|
14
|
+
|
15
|
+
== 0.7.3
|
16
|
+
* Add :return_votee => true option to vote function to warranty always return voteable object
|
17
|
+
* Add Votee.voted?, Votee.up_voted?, Votee.down_voted?
|
18
|
+
* Update parent for ManyToMany relationship
|
19
|
+
* Refactor
|
20
|
+
|
21
|
+
== 0.7.2
|
22
|
+
* Use Collection#find_and_modify to retrieve updated votes data and parent_ids (don't need an extra query to get parent_ids)
|
23
|
+
|
24
|
+
== 0.7.1
|
25
|
+
* Add votee#voted_by?(voter or voter_id)
|
26
|
+
* Better doc
|
27
|
+
* Refactor & cleanup source code
|
28
|
+
|
29
|
+
== 0.7.0
|
30
|
+
* Use readable field names (up, down, up_count, down_count, count, point) instead of very short field names (u, d, uc, dc, c, p)
|
31
|
+
|
32
|
+
== 0.6.4
|
33
|
+
* Drop Voter#votees, Voter#up_votees, Voter#down_votees in favor of Votee#voted_by(voter), Votee#up_voted_by(voter), Votee#down_voted_by(voter) scopes
|
34
|
+
|
35
|
+
== 0.6.3
|
36
|
+
* Add rake db:mongoid:voteable:migrate_old_votes to migrate vote data created by version < 0.6.0 to new vote data storage
|
37
|
+
|
38
|
+
== 0.6.2
|
39
|
+
* Fix bug: use before_create instead of after_after_initialize
|
40
|
+
|
41
|
+
== 0.6.1
|
42
|
+
* Set counters and point to 0 for uninitialized voteable objects in order sort and query
|
43
|
+
|
44
|
+
== 0.6.0
|
45
|
+
* Minimize vote data store (using short field names votes.u, votes.d, votes.c ...)
|
46
|
+
* Add Voter#up_votees, Voter#down_votees
|
47
|
+
* Remove index and scope from statistic module. User have to add indexes and scopes manually (see https://github.com/vinova/simple_qa/blob/master/app/models/question.rb)
|
48
|
+
* Bug fixes
|
49
|
+
|
50
|
+
== 0.5.0
|
51
|
+
* Rename vote_point to voteable
|
52
|
+
|
53
|
+
== 0.4.5
|
54
|
+
* Can use rake db:mongoid:voteable:remake_stats in Rails apps
|
55
|
+
* Use mongoid 2.0.0
|
56
|
+
|
57
|
+
== 0.4.4
|
58
|
+
* Add up_votes_count, down_votes_count
|
59
|
+
* Re-generate vote statistic data (counters and point)
|
60
|
+
|
61
|
+
== 0.4.3
|
62
|
+
* Wrap vote data in voteable namespace (voteable.up_voters_id, voteable.down_voters_ids, voteable.votes_count ...)
|
63
|
+
|
64
|
+
== 0.4.2
|
65
|
+
* Bug fixes
|
66
|
+
|
67
|
+
== 0.4.0
|
68
|
+
* Can unvote
|
69
|
+
|
70
|
+
== 0.3.5
|
71
|
+
* Use mongoid 2.0.0.rc
|
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
= Voteable Mongo
|
2
|
+
|
3
|
+
voteable_mongo allows you to make your Mongoid::Document (mongo_mapper support coming soon) objects voteable (up or down) and tabulate votes count and votes point for you. For instance, in a forum, a user can vote up (or down) on a post or a comment.
|
4
|
+
|
5
|
+
voteable_mongo is built for speed. It uses only one database request per collection to validate data, update data, and get updated data. Initial idea is based on http://cookbook.mongodb.org/patterns/votes
|
6
|
+
|
7
|
+
Sample app at https://github.com/vinova/simple_qa
|
8
|
+
|
9
|
+
Benchmarks at https://github.com/vinova/voteable_benchmarks
|
10
|
+
|
11
|
+
=== Sites using voteable_mongo
|
12
|
+
* http://www.amorveneris.com
|
13
|
+
* http://zheye.org (https://github.com/huacnlee/quora)
|
14
|
+
|
15
|
+
== Installation
|
16
|
+
|
17
|
+
=== Rails 3.0.x
|
18
|
+
|
19
|
+
To install the gem, add this to your Gemfile
|
20
|
+
|
21
|
+
gem 'mongoid'
|
22
|
+
gem 'voteable_mongo'
|
23
|
+
|
24
|
+
After that, remember to run "bundle install"
|
25
|
+
|
26
|
+
== Usage
|
27
|
+
|
28
|
+
=== Make Post and Comment voteable, User become the voter
|
29
|
+
|
30
|
+
post.rb
|
31
|
+
|
32
|
+
class Post
|
33
|
+
include Mongoid::Document
|
34
|
+
include Mongo::Voteable
|
35
|
+
|
36
|
+
# set points for each vote
|
37
|
+
voteable self, :up => +1, :down => -1
|
38
|
+
|
39
|
+
has_many :comments
|
40
|
+
end
|
41
|
+
|
42
|
+
comment.rb
|
43
|
+
|
44
|
+
require 'post'
|
45
|
+
|
46
|
+
class Comment
|
47
|
+
include Mongoid::Document
|
48
|
+
include Mongo::Voteable
|
49
|
+
|
50
|
+
belongs_to :post
|
51
|
+
|
52
|
+
voteable self, :up => +1, :down => -3
|
53
|
+
|
54
|
+
# each vote on a comment can affect votes count and point of the related post as well
|
55
|
+
voteable Post, :up => +2, :down => -1
|
56
|
+
end
|
57
|
+
|
58
|
+
user.rb
|
59
|
+
|
60
|
+
class User
|
61
|
+
include Mongoid::Document
|
62
|
+
include Mongo::Voter
|
63
|
+
end
|
64
|
+
|
65
|
+
=== Make a vote
|
66
|
+
|
67
|
+
@user.vote(@post, :up)
|
68
|
+
|
69
|
+
Is equivalent to
|
70
|
+
@user.vote(:votee => @post, :value => :up)
|
71
|
+
@post.vote(:voter => @user, :value => :up)
|
72
|
+
|
73
|
+
In case you don't need to init voter and / or votee objects you can
|
74
|
+
@user.vote(:votee_type => 'Post', :votee_id => post_id, :value => :down)
|
75
|
+
@post.vote(:voter_id => user_id, :value => :up)
|
76
|
+
Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up)
|
77
|
+
|
78
|
+
=== Undo a vote
|
79
|
+
|
80
|
+
@user.unvote(@comment)
|
81
|
+
|
82
|
+
=== If have voter_id, votee_id and vote value you don't need to init voter and votee objects (suitable for API calls)
|
83
|
+
|
84
|
+
New vote
|
85
|
+
Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up)
|
86
|
+
|
87
|
+
Re-vote
|
88
|
+
Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up, :revote => true)
|
89
|
+
|
90
|
+
Un-vote
|
91
|
+
Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up, :unvote => true)
|
92
|
+
|
93
|
+
In-case you need updated voteable object, add :return_votee => true
|
94
|
+
votee = Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up, :return_votee => true)
|
95
|
+
|
96
|
+
=== Get vote_value
|
97
|
+
|
98
|
+
@user.vote_value(@post)
|
99
|
+
@user.vote_value(:class_type => 'Post', :votee_id => post_id)
|
100
|
+
@post.vote_value(@user)
|
101
|
+
@post.vote_value(user_id)
|
102
|
+
|
103
|
+
=== Check if voted?
|
104
|
+
|
105
|
+
@user.voted?(@post)
|
106
|
+
@user.voted?(:class_type => 'Post', :votee_id => post_id)
|
107
|
+
@post.voted_by?(@user)
|
108
|
+
@post.voted_by?(user_id)
|
109
|
+
|
110
|
+
=== Get votes counts and points
|
111
|
+
|
112
|
+
puts @post.votes_point
|
113
|
+
puts @post.votes_count
|
114
|
+
puts @post.up_votes_count
|
115
|
+
puts @post.down_votes_count
|
116
|
+
|
117
|
+
=== Get voters given voted object and voter class
|
118
|
+
|
119
|
+
@post.up_voters(User)
|
120
|
+
@post.down_voters(User)
|
121
|
+
@post.voters(User)
|
122
|
+
- or -
|
123
|
+
User.up_voted_for(@post)
|
124
|
+
User.down_voted_for(@post)
|
125
|
+
User.voted_for(@post)
|
126
|
+
|
127
|
+
=== Get the list of voted objects of a class
|
128
|
+
|
129
|
+
Post.voted_by(@user)
|
130
|
+
Post.up_voted_by(@user)
|
131
|
+
Post.down_voted_by(@user)
|
132
|
+
|
133
|
+
== Utilities
|
134
|
+
|
135
|
+
=== Re-generate counters and vote points in case you change :up / :down vote points
|
136
|
+
Rails
|
137
|
+
rake mongo:voteable:remake_stats
|
138
|
+
Ruby
|
139
|
+
Mongo::Voteable::Tasks.remake_stats
|
140
|
+
|
141
|
+
=== Set counters and point to 0 for uninitialized voteable objects in order sort and query
|
142
|
+
Rails
|
143
|
+
rake mongo:voteable:init_stats
|
144
|
+
Ruby
|
145
|
+
Mongo::Voteable::Tasks::init_stats
|
146
|
+
|
147
|
+
=== Migrate from version < 0.7.0
|
148
|
+
Rails
|
149
|
+
rake mongo:voteable:migrate_old_votes
|
150
|
+
Ruby
|
151
|
+
Mongo::Voteable::Tasks.migrate_old_votes
|
152
|
+
|
153
|
+
== Credits
|
154
|
+
* Alex Nguyen (alex@vinova.sg) - Author
|
155
|
+
* Stefan Nguyen (stefan@vinova.sg) - Unvoting
|
156
|
+
|
157
|
+
Copyright (c) 2010-2011 Vinova Pte Ltd (http://vinova.sg)
|
158
|
+
|
159
|
+
Licensed under the MIT license.
|
data/Rakefile
ADDED
data/TODO
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Rails #:nodoc:
|
2
|
+
module VoteableMongo #:nodoc:
|
3
|
+
class Railtie < Rails::Railtie #:nodoc:
|
4
|
+
|
5
|
+
initializer "preload all application models" do |app|
|
6
|
+
config.to_prepare do
|
7
|
+
::Rails::Mongoid.load_models(app)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
rake_tasks do
|
12
|
+
load 'voteable_mongo/railties/database.rake'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
namespace :mongo do
|
2
|
+
namespace :voteable do
|
3
|
+
desc 'Update up_votes_count, down_votes_count, votes_count and votes_point'
|
4
|
+
task :remake_stats => :environment do
|
5
|
+
Mongo::Voteable::Tasks.remake_stats(:log)
|
6
|
+
end
|
7
|
+
|
8
|
+
desc 'Set counters and point to 0 for uninitizized voteable objects'
|
9
|
+
task :init_stats => :environment do
|
10
|
+
Mongo::Voteable::Tasks.init_stats(:log)
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Migrate vote data created by version < 0.7.0 to new vote data storage'
|
14
|
+
task :migrate_old_votes => :environment do
|
15
|
+
Mongo::Voteable::Tasks.migrate_old_votes(:log)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'voteable_mongo/voteable/votes'
|
2
|
+
require 'voteable_mongo/voteable/voting'
|
3
|
+
|
4
|
+
module Mongo
|
5
|
+
module Voteable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ::Mongo::Voteable::Voting
|
10
|
+
|
11
|
+
field :votes, :type => Votes
|
12
|
+
|
13
|
+
before_create do
|
14
|
+
# Init votes so that counters and point have numeric values (0)
|
15
|
+
self.votes = Votes::DEFAULT_ATTRIBUTES
|
16
|
+
end
|
17
|
+
|
18
|
+
scope :voted_by, lambda { |voter|
|
19
|
+
voter_id = voter.is_a?(BSON::ObjectId) ? voter : voter.id
|
20
|
+
any_of({ 'votes.up' => voter_id }, { 'votes.down' => voter_id })
|
21
|
+
}
|
22
|
+
|
23
|
+
scope :up_voted_by, lambda { |voter|
|
24
|
+
voter_id = voter.is_a?(BSON::ObjectId) ? voter : voter.id
|
25
|
+
where( 'votes.up' => voter_id )
|
26
|
+
}
|
27
|
+
|
28
|
+
scope :down_voted_by, lambda { |voter|
|
29
|
+
voter_id = voter.is_a?(BSON::ObjectId) ? voter : voter.id
|
30
|
+
where( 'votes.down' => voter_id )
|
31
|
+
}
|
32
|
+
end # include
|
33
|
+
|
34
|
+
# How many points should be assigned for each up or down vote and other options
|
35
|
+
# This hash should manipulated using voteable method
|
36
|
+
VOTEABLE = {}
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
# Set vote point for each up (down) vote on an object of this class
|
40
|
+
#
|
41
|
+
# @param [Hash] options a hash containings:
|
42
|
+
#
|
43
|
+
# voteable self, :up => +1, :down => -3
|
44
|
+
# voteable Post, :up => +2, :down => -1, :update_counters => false # skip counter update
|
45
|
+
def voteable(klass = self, options = nil)
|
46
|
+
VOTEABLE[name] ||= {}
|
47
|
+
VOTEABLE[name][klass.name] ||= options
|
48
|
+
if klass == self
|
49
|
+
if options[:index] == true
|
50
|
+
create_voteable_indexes
|
51
|
+
end
|
52
|
+
else
|
53
|
+
VOTEABLE[name][name][:update_parents] ||= true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if voter_id do a vote on votee_id
|
58
|
+
#
|
59
|
+
# @param [Hash] options a hash containings:
|
60
|
+
# - :votee_id: the votee document id
|
61
|
+
# - :voter_id: the voter document id
|
62
|
+
#
|
63
|
+
# @return [true, false]
|
64
|
+
def voted?(options)
|
65
|
+
validate_and_normalize_vote_options(options)
|
66
|
+
up_voted?(options) || down_voted?(options)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if voter_id do an up vote on votee_id
|
70
|
+
#
|
71
|
+
# @param [Hash] options a hash containings:
|
72
|
+
# - :votee_id: the votee document id
|
73
|
+
# - :voter_id: the voter document id
|
74
|
+
#
|
75
|
+
# @return [true, false]
|
76
|
+
def up_voted?(options)
|
77
|
+
validate_and_normalize_vote_options(options)
|
78
|
+
up_voted_by(options[:voter_id]).where(:_id => options[:votee_id]).count == 1
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check if voter_id do a down vote on votee_id
|
82
|
+
#
|
83
|
+
# @param [Hash] options a hash containings:
|
84
|
+
# - :votee_id: the votee document id
|
85
|
+
# - :voter_id: the voter document id
|
86
|
+
#
|
87
|
+
# @return [true, false]
|
88
|
+
def down_voted?(options)
|
89
|
+
validate_and_normalize_vote_options(options)
|
90
|
+
down_voted_by(options[:voter_id]).where(:_id => options[:votee_id]).count == 1
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def create_voteable_indexes
|
95
|
+
class_eval do
|
96
|
+
# Compound index _id and voters.up, _id and voters.down
|
97
|
+
# to make up_voted_by, down_voted_by, voted_by scopes and voting faster
|
98
|
+
# Should run in background since it introduce new index value and
|
99
|
+
# while waiting to build, the system can use _id for voting
|
100
|
+
# http://www.mongodb.org/display/DOCS/Indexing+as+a+Background+Operation
|
101
|
+
index [['votes.up', 1], ['_id', 1]], :unique => true
|
102
|
+
index [['votes.down', 1], ['_id', 1]], :unique => true
|
103
|
+
|
104
|
+
# Index counters and point for desc ordering
|
105
|
+
index [['votes.up_count', -1]]
|
106
|
+
index [['votes.down_count', -1]]
|
107
|
+
index [['votes.count', -1]]
|
108
|
+
index [['votes.point', -1]]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
module InstanceMethods
|
114
|
+
# Make a vote on this votee
|
115
|
+
#
|
116
|
+
# @param [Hash] options a hash containings:
|
117
|
+
# - :voter_id: the voter document id
|
118
|
+
# - :value: vote :up or vote :down
|
119
|
+
# - :revote: change from vote up to vote down
|
120
|
+
# - :unvote: unvote the vote value (:up or :down)
|
121
|
+
def vote(options)
|
122
|
+
options[:votee_id] = id
|
123
|
+
options[:votee] = self
|
124
|
+
options[:voter_id] ||= options[:voter].id
|
125
|
+
|
126
|
+
if options[:unvote]
|
127
|
+
options[:value] ||= vote_value(options[:voter_id])
|
128
|
+
else
|
129
|
+
options[:revote] ||= vote_value(options[:voter_id]).present?
|
130
|
+
end
|
131
|
+
|
132
|
+
self.class.vote(options)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Get a voted value on this votee
|
136
|
+
#
|
137
|
+
# @param [Mongoid Object, BSON::ObjectId] voter is Mongoid object or the id of the voter who made the vote
|
138
|
+
def vote_value(voter)
|
139
|
+
voter_id = voter.is_a?(BSON::ObjectId) ? voter : voter.id
|
140
|
+
return :up if up_voter_ids.include?(voter_id)
|
141
|
+
return :down if down_voter_ids.include?(voter_id)
|
142
|
+
end
|
143
|
+
|
144
|
+
def voted_by?(voter)
|
145
|
+
!!vote_value(voter)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Array of up voter ids
|
149
|
+
def up_voter_ids
|
150
|
+
votes.try(:[], 'up') || []
|
151
|
+
end
|
152
|
+
|
153
|
+
# Array of down voter ids
|
154
|
+
def down_voter_ids
|
155
|
+
votes.try(:[], 'down') || []
|
156
|
+
end
|
157
|
+
|
158
|
+
# Array of voter ids
|
159
|
+
def voter_ids
|
160
|
+
up_voter_ids + down_voter_ids
|
161
|
+
end
|
162
|
+
|
163
|
+
# Get the number of up votes
|
164
|
+
def up_votes_count
|
165
|
+
votes.try(:[], 'up_count') || 0
|
166
|
+
end
|
167
|
+
|
168
|
+
# Get the number of down votes
|
169
|
+
def down_votes_count
|
170
|
+
votes.try(:[], 'down_count') || 0
|
171
|
+
end
|
172
|
+
|
173
|
+
# Get the number of votes
|
174
|
+
def votes_count
|
175
|
+
votes.try(:[], 'count') || 0
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get the votes point
|
179
|
+
def votes_point
|
180
|
+
votes.try(:[], 'point') || 0
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get up voters
|
184
|
+
def up_voters(klass)
|
185
|
+
klass.where(:_id => { '$in' => up_voter_ids })
|
186
|
+
end
|
187
|
+
|
188
|
+
# Get down voters
|
189
|
+
def down_voters(klass)
|
190
|
+
klass.where(:_id => { '$in' => down_voter_ids })
|
191
|
+
end
|
192
|
+
|
193
|
+
# Get voters
|
194
|
+
def voters(klass)
|
195
|
+
klass.where(:_id => { '$in' => voter_ids })
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|