voteable_mongo 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+
5
+ .DS_Store
6
+
7
+ coverage
8
+ rdoc
9
+ pkg
10
+
11
+ .idea/
12
+ .yardoc/
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm_gemset_create_on_use_flag=1
2
+ rvm use 1.9.2@voteable
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
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in voteable_mongo.gemspec
4
+ gemspec
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
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec"
5
+ require "rspec/core/rake_task"
6
+ Rspec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = "spec/**/*_spec.rb"
8
+ end
9
+
10
+ task :default => ["spec"]
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ * Add support for mongo_mapper
2
+ * Add options hash validations
3
+ * Refactor specs
@@ -0,0 +1,10 @@
1
+ require 'mongoid'
2
+ require 'voteable_mongo/voteable'
3
+ require 'voteable_mongo/voter'
4
+
5
+ require 'voteable_mongo/voteable/tasks'
6
+
7
+ # Add railtie
8
+ if defined?(Rails)
9
+ require 'voteable_mongo/railtie'
10
+ end
@@ -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,3 @@
1
+ module VoteableMongo
2
+ VERSION = '0.8.1'
3
+ 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