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 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