voteable_mongo 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|