rs_voteable_mongo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 53b6e38a882ecb70db25219bd78159c08e226bbf
4
+ data.tar.gz: 10a70421818708ecc107023550022bb31d1db72d
5
+ SHA512:
6
+ metadata.gz: 167fc70a55bfc367fd4a441a8a7123267b0fc62c5aa58f0a58a20e5824e2847a47f45b1ee01cb5e95c93c2026e187fa1a634c26fe8516f5c09b5694578ad9dad
7
+ data.tar.gz: 6ca2f3486223cfc1554a2587b42d5884b830f49b50b94917822bf811e1d7ec6bf2edebe082b9198362b6339f8cb70bd57baaa82b42869a20814df60fe530ab6f
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/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ votable_mongo
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0-p247
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,87 @@
1
+ == 0.9.3
2
+ * Support mongoid ~> 2.0, mongo_mapper ~> 0.9
3
+
4
+ == 0.9.2
5
+ * Replace votee_type to votee_class
6
+ * Bug fixes
7
+
8
+ == 0.9.1
9
+ * Update gem description
10
+
11
+ == 0.9.0
12
+ * Add MongoMapper support
13
+ * Simplify voting algorithm
14
+ * vote / revote / unvote always return voteable object (votee)
15
+ * Tasks module bug fixes
16
+
17
+ == 0.8.1
18
+ * Fix gem release bug
19
+
20
+ == 0.8.0
21
+ * Rename to voteable_mongo to support other MongoDB Object-Document Mappers like MongoMapper
22
+ * Minor fixes and refactoring
23
+
24
+ == 0.7.4
25
+ * Add Votee#up_voters(VoterClass), Votee#down_voters(VoterClass), Votee#voters(VoterClass)
26
+ * Add Voter scopes: Voter.up_voted_for(votee), Voter.down_voted_for(votee), Voter.voted_for(votee)
27
+ * Add voteable ..., :index => true options
28
+ * Optimization on unvote and revote validations
29
+ * Fix for :up & :down points are nil in rake tasks
30
+
31
+ == 0.7.3
32
+ * Add :return_votee => true option to vote function to warranty always return voteable object
33
+ * Add Votee.voted?, Votee.up_voted?, Votee.down_voted?
34
+ * Update parent for ManyToMany relationship
35
+ * Refactor
36
+
37
+ == 0.7.2
38
+ * Use Collection#find_and_modify to retrieve updated votes data and parent_ids (don't need an extra query to get parent_ids)
39
+
40
+ == 0.7.1
41
+ * Add votee#voted_by?(voter or voter_id)
42
+ * Better doc
43
+ * Refactor & cleanup source code
44
+
45
+ == 0.7.0
46
+ * Use readable field names (up, down, up_count, down_count, count, point) instead of very short field names (u, d, uc, dc, c, p)
47
+
48
+ == 0.6.4
49
+ * 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
50
+
51
+ == 0.6.3
52
+ * Add rake db:mongoid:voteable:migrate_old_votes to migrate vote data created by version < 0.6.0 to new vote data storage
53
+
54
+ == 0.6.2
55
+ * Fix bug: use before_create instead of after_after_initialize
56
+
57
+ == 0.6.1
58
+ * Set counters and point to 0 for uninitialized voteable objects in order sort and query
59
+
60
+ == 0.6.0
61
+ * Minimize vote data store (using short field names votes.u, votes.d, votes.c ...)
62
+ * Add Voter#up_votees, Voter#down_votees
63
+ * 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)
64
+ * Bug fixes
65
+
66
+ == 0.5.0
67
+ * Rename vote_point to voteable
68
+
69
+ == 0.4.5
70
+ * Can use rake db:mongoid:voteable:remake_stats in Rails apps
71
+ * Use mongoid 2.0.0
72
+
73
+ == 0.4.4
74
+ * Add up_votes_count, down_votes_count
75
+ * Re-generate vote statistic data (counters and point)
76
+
77
+ == 0.4.3
78
+ * Wrap vote data in voteable namespace (voteable.up_voters_id, voteable.down_voters_ids, voteable.votes_count ...)
79
+
80
+ == 0.4.2
81
+ * Bug fixes
82
+
83
+ == 0.4.0
84
+ * Can unvote
85
+
86
+ == 0.3.5
87
+ * Use mongoid 2.0.0.rc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in voteable_mongo.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,212 @@
1
+ = Voteable Mongo (for Mongoid 3 / 4)
2
+
3
+ voteable_mongo allows you to make your Mongoid::Document or MongoMapper::Document objects voteable 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. It's optimized for speed by using only ONE database request per collection to validate, update, and retrieve updated data.
4
+
5
+ Initial idea based on http://cookbook.mongodb.org/patterns/votes.
6
+
7
+ Sample app at https://github.com/vinova/simple_qa.
8
+
9
+ Wonder how fast voteable_mongo is compare to other SQL & MongoDB solutions?
10
+ Visit benchmarks at https://github.com/vinova/voteable_benchmarks
11
+
12
+ == Why voteable_mongo?
13
+
14
+ There are various solutions for up / down voting problem (1[https://github.com/medihack/make_voteable], 2[https://github.com/brady8/thumbs_up], 3[https://github.com/icaruswings/mm-voteable], 4[https://github.com/jcoene/mongoid_voteable], ...). Most of them using additional votes table (SQL) or votes collection (MongoDB) to store votes and do data tabulation on that votes table or votes collection.
15
+
16
+ voteable_mongo is different. It takes advantage of document-oriented database to store all related votes data inside voteable document. That has following benefits:
17
+
18
+ * Don't have to maintain additional votes table or votes collection.
19
+
20
+ * When voteable document is loaded, all votes data related to it also be loaded, no more additional database requests to see how many votes this document got, who give up votes who give down vote, total vote points, votes count ...
21
+
22
+ * When vote up, vote down, revote, unvote, voteable_mongo validates vote data, updates voteable document and retrieves updated data using only ONE database request thanks to atomic findAndModify operation.
23
+
24
+ * Atomic operations on single document warranty data integrity that makes sure if votes created / changed / deleted their associated counters and points will be updated.
25
+
26
+ So use voteable_mongo for less maintain cost, data integrity and save database requests for other tasks.
27
+
28
+ == Sites using voteable_mongo
29
+ * http://www.naiku.net
30
+ * http://www.amorveneris.com
31
+ * http://zheye.org
32
+
33
+ == Installation
34
+
35
+ === Rails 3.x
36
+
37
+ To install the gem, add this to your Gemfile
38
+
39
+ gem 'rs_voteable_mongo'
40
+
41
+ After that, remember to run "bundle install"
42
+
43
+ == Usage
44
+
45
+ === Make Post and Comment voteable, User become the voter
46
+
47
+ == Mongoid
48
+ post.rb
49
+
50
+ class Post
51
+ include Mongoid::Document
52
+ include Mongo::Voteable
53
+
54
+ # set points for each vote
55
+ voteable self, :up => +1, :down => -1
56
+
57
+ has_many :comments
58
+ end
59
+
60
+ comment.rb
61
+
62
+ require 'post'
63
+
64
+ class Comment
65
+ include Mongoid::Document
66
+ include Mongo::Voteable
67
+
68
+ belongs_to :post
69
+
70
+ voteable self, :up => +1, :down => -3
71
+
72
+ # each vote on a comment can affect votes count and point of the related post as well
73
+ voteable Post, :up => +2, :down => -1
74
+ end
75
+
76
+ user.rb
77
+
78
+ class User
79
+ include Mongoid::Document
80
+ include Mongo::Voter
81
+ end
82
+
83
+
84
+ == MongoMapper
85
+ post.rb
86
+
87
+ class Post
88
+ include MongoMapper::Document
89
+ include Mongo::Voteable
90
+
91
+ # set points for each vote
92
+ voteable self, :up => +1, :down => -1
93
+
94
+ many :comments
95
+ end
96
+
97
+ comment.rb
98
+
99
+ require 'post'
100
+
101
+ class Comment
102
+ include MongoMapper::Document
103
+ include Mongo::Voteable
104
+
105
+ belongs_to :post
106
+
107
+ voteable self, :up => +1, :down => -3
108
+ voteable Post, :up => +2, :down => -1
109
+ end
110
+
111
+ user.rb
112
+
113
+ class User
114
+ include MongoMapper::Document
115
+ include Mongo::Voter
116
+ end
117
+
118
+
119
+ === Make a vote
120
+
121
+ @user.vote(@post, :up)
122
+
123
+ Is equivalent to
124
+ @user.vote(:votee => @post, :value => :up)
125
+ @post.vote(:voter => @user, :value => :up)
126
+
127
+ In case you don't need to init voter and / or votee objects you can
128
+ @user.vote(:votee_class => Post, :votee_id => post_id, :value => :down)
129
+ @post.vote(:voter_id => user_id, :value => :up)
130
+ Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up)
131
+
132
+ === Undo a vote
133
+
134
+ @user.unvote(@comment)
135
+
136
+ === If have voter_id, votee_id and vote value you don't need to init voter and votee objects (suitable for API calls)
137
+
138
+ New vote
139
+ Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up)
140
+
141
+ Re-vote
142
+ Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up, :revote => true)
143
+
144
+ Un-vote
145
+ Post.vote(:voter_id => user_id, :votee_id => post_id, :value => :up, :unvote => true)
146
+
147
+ Note: vote function always return updated votee object
148
+
149
+ === Get vote_value
150
+
151
+ @user.vote_value(@post)
152
+ @user.vote_value(:votee_class => Post, :votee_id => post_id)
153
+ @post.vote_value(@user)
154
+ @post.vote_value(user_id)
155
+
156
+ === Check if voted?
157
+
158
+ @user.voted?(@post)
159
+ @user.voted?(:votee_class => Post, :votee_id => post_id)
160
+ @post.voted_by?(@user)
161
+ @post.voted_by?(user_id)
162
+
163
+ === Get votes counts and points
164
+
165
+ puts @post.votes_point
166
+ puts @post.votes_count
167
+ puts @post.up_votes_count
168
+ puts @post.down_votes_count
169
+
170
+ === Get voters given voted object and voter class
171
+
172
+ @post.up_voters(User)
173
+ @post.down_voters(User)
174
+ @post.voters(User)
175
+ - or -
176
+ User.up_voted_for(@post)
177
+ User.down_voted_for(@post)
178
+ User.voted_for(@post)
179
+
180
+ === Get the list of voted objects of a class
181
+
182
+ Post.voted_by(@user)
183
+ Post.up_voted_by(@user)
184
+ Post.down_voted_by(@user)
185
+
186
+ == Utilities
187
+
188
+ === Set counters and point to 0 for uninitialized voteable objects in order sort and query
189
+ Rails
190
+ rake mongo:voteable:init_stats
191
+ Ruby
192
+ Mongo::Voteable::Tasks::init_stats
193
+
194
+ === Re-generate counters and vote points in case you change :up / :down vote points
195
+ Rails
196
+ rake mongo:voteable:remake_stats
197
+ Ruby
198
+ Mongo::Voteable::Tasks.remake_stats
199
+
200
+ === Migrate from voteable_mongoid version < 0.7.0
201
+ Rails
202
+ rake mongo:voteable:migrate_old_votes
203
+ Ruby
204
+ Mongo::Voteable::Tasks.migrate_old_votes
205
+
206
+ == Credits
207
+ * Alex Nguyen - Author
208
+ * Contributors[https://github.com/vinova/voteable_mongo/contributors]
209
+
210
+ Copyright (c) 2010-2011 Vinova Pte Ltd
211
+
212
+ 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,17 @@
1
+ * Support Mongoid / MongoMapper embedded documents
2
+
3
+ * Add :foreign_key => ... option to update parents' votes
4
+ - Reduce time to check relationships and foreign keys
5
+ - More flexible
6
+ - Need to change votable interface?
7
+
8
+ * Support https://github.com/benmyles/mongomatic &
9
+ https://github.com/carlosparamio/mongo_odm
10
+ - Don't have scope
11
+ - Don't have relationships
12
+
13
+ * Add options hash validations
14
+
15
+ * Refactor specs
16
+
17
+ * More test cases for Tasks module
@@ -0,0 +1 @@
1
+ require 'votable_mongo'
@@ -0,0 +1,18 @@
1
+ module Mongo
2
+ module Voteable
3
+ module Helpers
4
+
5
+ def self.try_to_convert_string_to_object_id(x)
6
+ if defined?(Moped::BSON)
7
+ x.is_a?(String) && Moped::BSON::ObjectId.legal?(x) ? Moped::BSON::ObjectId.from_string(x) : x
8
+ else
9
+ x.is_a?(String) && BSON::ObjectId.legal?(x) ? BSON::ObjectId.from_string(x) : x
10
+ end
11
+ end
12
+
13
+ def self.get_mongo_id(x)
14
+ x.respond_to?(:id) ? x.id : x
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ module Mongo
2
+ module Voteable
3
+ module Integrations
4
+ module Mongoid
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ field :votes, :type => Hash, :default => DEFAULT_VOTES
9
+
10
+ class << self
11
+ alias_method :voteable_index, :index
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def voteable_relation(class_name)
17
+ relations.find{ |x, r| r.class_name == class_name }.try(:last)
18
+ end
19
+
20
+ def voteable_collection
21
+ collection
22
+ end
23
+
24
+ def voteable_foreign_key(metadata)
25
+ metadata.foreign_key.to_s
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
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
+ if defined?(Mongoid)
8
+ ::Rails::Mongoid.load_models(app)
9
+ end
10
+ end
11
+ end
12
+
13
+ rake_tasks do
14
+ load 'voteable_mongo/railties/database.rake'
15
+ end
16
+
17
+ end
18
+ end
19
+ 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,152 @@
1
+ module Mongo
2
+ module Voteable
3
+ module Tasks
4
+
5
+ # Set counters and point to 0 for uninitialized voteable objects
6
+ # in order sort and query
7
+ def self.init_stats(log = false)
8
+ VOTEABLE.each do |class_name, voteable|
9
+ klass = class_name.constantize
10
+ klass_voteable = voteable[class_name]
11
+ puts "Init stats for #{class_name}" if log
12
+ klass.with(safe: true).where(votes: nil).update_all({ '$set' => {votes: DEFAULT_VOTES} })
13
+ end
14
+ end
15
+
16
+ # Re-generate vote counters and vote points
17
+ def self.remake_stats(log = false)
18
+ remake_stats_for_all_voteable_classes(log)
19
+ update_parent_stats(log)
20
+ end
21
+
22
+ # Convert votes from from version < 0.7.0 to new data store
23
+ def self.migrate_old_votes(log = false)
24
+ VOTEABLE.each do |class_name, voteable|
25
+ klass = class_name.constantize
26
+ klass_voteable = voteable[class_name]
27
+ puts "* Migrating old vote data for #{class_name} ..." if log
28
+ migrate_old_votes_for(klass, klass_voteable)
29
+ end
30
+ end
31
+
32
+ def self.migrate_old_votes_for(klass, voteable)
33
+ klass.all.each do |doc|
34
+ # Version 0.6.x use very short field names (u, d, uc, dc, c, p) to minimize
35
+ # votes storage but it's not human friendly
36
+ # Version >= 0.7.0 use readable field names (up, down, up_count, down_count,
37
+ # count, point)
38
+ votes = doc['votes'] || doc['voteable'] || {}
39
+
40
+ up_voter_ids = votes['up'] || votes['u'] ||
41
+ votes['up_voter_ids'] || doc['up_voter_ids'] || []
42
+
43
+ down_voter_ids = votes['down'] || votes['d'] ||
44
+ votes['down_voter_ids'] || doc['down_voter_ids'] || []
45
+
46
+ up_count = up_voter_ids.size
47
+ down_count = down_voter_ids.size
48
+
49
+ klass.with(safe: true).where(_id: doc.id).update_all(
50
+ '$set' => {
51
+ 'votes' => {
52
+ 'up' => up_voter_ids,
53
+ 'down' => down_voter_ids,
54
+ 'up_count' => up_count,
55
+ 'down_count' => down_count,
56
+ 'count' => up_count + down_count,
57
+ 'point' => voteable[:up].to_i*up_count + voteable[:down].to_i*down_count
58
+ }
59
+ },
60
+ '$unset' => {
61
+ 'up_voter_ids' => true,
62
+ 'down_voter_ids' => true,
63
+ 'votes_count' => true,
64
+ 'votes_point' => true,
65
+ 'voteable' => true
66
+ }
67
+ )
68
+ end
69
+ end
70
+
71
+
72
+ def self.remake_stats_for_all_voteable_classes(log)
73
+ VOTEABLE.each do |class_name, voteable|
74
+ klass = class_name.constantize
75
+ klass_voteable = voteable[class_name]
76
+ puts "Generating stats for #{class_name}" if log
77
+ klass.all.each{ |doc|
78
+ remake_stats_for(doc, klass_voteable)
79
+ }
80
+ end
81
+ end
82
+
83
+
84
+ def self.remake_stats_for(doc, voteable)
85
+ up_count = doc.up_voter_ids.length
86
+ down_count = doc.down_voter_ids.length
87
+ doc.update_attributes(
88
+ 'votes' => {
89
+ 'up' => doc.up_voter_ids,
90
+ 'down' => doc.down_voter_ids,
91
+ 'up_count' => up_count,
92
+ 'down_count' => down_count,
93
+ 'count' => up_count + down_count,
94
+ 'point' => voteable[:up].to_i*up_count + voteable[:down].to_i*down_count
95
+ }
96
+ )
97
+ end
98
+
99
+
100
+ def self.update_parent_stats(log)
101
+ VOTEABLE.each do |class_name, voteable|
102
+ klass = class_name.constantize
103
+ voteable.each do |parent_class_name, parent_voteable|
104
+ metadata = klass.voteable_relation(parent_class_name)
105
+ if metadata
106
+ parent_class = parent_class_name.constantize
107
+ foreign_key = klass.voteable_foreign_key(metadata)
108
+ puts "Updating stats for #{class_name} > #{parent_class_name}" if log
109
+ klass.all.each{ |doc|
110
+ update_parent_stats_for(doc, parent_class, foreign_key, parent_voteable)
111
+ }
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ def self.update_parent_stats_for(doc, parent_class, foreign_key, voteable)
119
+ parent_id = doc.read_attribute(foreign_key.to_sym)
120
+ if parent_id
121
+ up_count = doc.up_voter_ids.length
122
+ down_count = doc.down_voter_ids.length
123
+
124
+ return if up_count == 0 && down_count == 0
125
+
126
+ inc_options = {
127
+ 'votes.point' => voteable[:up].to_i*up_count + voteable[:down].to_i*down_count
128
+ }
129
+
130
+ unless voteable[:update_counters] == false
131
+ inc_options.merge!(
132
+ 'votes.count' => up_count + down_count,
133
+ 'votes.up_count' => up_count,
134
+ 'votes.down_count' => down_count
135
+ )
136
+ end
137
+
138
+ parent_ids = parent_id.is_a?(Array) ? parent_id : [ parent_id ]
139
+
140
+ parent_class.with(safe: true).where(:_id.in => parent_ids).update_all({ '$inc' => inc_options })
141
+ end
142
+ end
143
+
144
+ private_class_method :migrate_old_votes_for,
145
+ :remake_stats_for,
146
+ :remake_stats_for_all_voteable_classes,
147
+ :update_parent_stats,
148
+ :update_parent_stats_for
149
+
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,3 @@
1
+ module VoteableMongo
2
+ VERSION = '1.0.0'
3
+ end