rs_voteable_mongo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,203 @@
1
+ require 'voteable_mongo/voting'
2
+ require 'voteable_mongo/integrations/mongoid'
3
+
4
+ module Mongo
5
+ module Voteable
6
+ extend ActiveSupport::Concern
7
+
8
+ DEFAULT_VOTES = {
9
+ 'up' => [],
10
+ 'down' => [],
11
+ 'up_count' => 0,
12
+ 'down_count' => 0,
13
+ 'count' => 0,
14
+ 'point' => 0
15
+ }
16
+
17
+ included do
18
+ include Mongo::Voteable::Voting
19
+
20
+ if defined?(Mongoid) && defined?(field)
21
+ include Mongo::Voteable::Integrations::Mongoid
22
+ elsif defined?(MongoMapper)
23
+ include Mongo::Voteable::Integrations::MongoMapper
24
+ end
25
+
26
+ scope :voted_by, lambda { |voter|
27
+ voter_id = Helpers.get_mongo_id(voter)
28
+ where('$or' => [{ 'votes.up' => voter_id }, { 'votes.down' => voter_id }])
29
+ }
30
+
31
+ scope :up_voted_by, lambda { |voter|
32
+ voter_id = Helpers.get_mongo_id(voter)
33
+ where('votes.up' => voter_id)
34
+ }
35
+
36
+ scope :down_voted_by, lambda { |voter|
37
+ voter_id = Helpers.get_mongo_id(voter)
38
+ where('votes.down' => voter_id)
39
+ }
40
+ end
41
+
42
+ # How many points should be assigned for each up or down vote and other options
43
+ # This hash should manipulated using voteable method
44
+ VOTEABLE = {}
45
+
46
+ module ClassMethods
47
+ # Set vote point for each up (down) vote on an object of this class
48
+ #
49
+ # @param [Hash] options a hash containings:
50
+ #
51
+ # voteable self, :up => +1, :down => -3
52
+ # voteable Post, :up => +2, :down => -1, :update_counters => false # skip counter update
53
+ def voteable(klass = self, options = nil)
54
+ VOTEABLE[name] ||= {}
55
+ VOTEABLE[name][klass.name] ||= options
56
+ if klass == self
57
+ if options[:index] == true
58
+ create_voteable_indexes
59
+ end
60
+ else
61
+ VOTEABLE[name][name][:update_parents] ||= true
62
+ end
63
+ end
64
+
65
+ # Check if voter_id do a vote on votee_id
66
+ #
67
+ # @param [Hash] options a hash containings:
68
+ # - :votee_id: the votee document id
69
+ # - :voter_id: the voter document id
70
+ #
71
+ # @return [true, false]
72
+ def voted?(options)
73
+ validate_and_normalize_vote_options(options)
74
+ up_voted?(options) || down_voted?(options)
75
+ end
76
+
77
+ # Check if voter_id do an up vote on votee_id
78
+ #
79
+ # @param [Hash] options a hash containings:
80
+ # - :votee_id: the votee document id
81
+ # - :voter_id: the voter document id
82
+ #
83
+ # @return [true, false]
84
+ def up_voted?(options)
85
+ validate_and_normalize_vote_options(options)
86
+ up_voted_by(options[:voter_id]).where(:_id => options[:votee_id]).count == 1
87
+ end
88
+
89
+ # Check if voter_id do a down vote on votee_id
90
+ #
91
+ # @param [Hash] options a hash containings:
92
+ # - :votee_id: the votee document id
93
+ # - :voter_id: the voter document id
94
+ #
95
+ # @return [true, false]
96
+ def down_voted?(options)
97
+ validate_and_normalize_vote_options(options)
98
+ down_voted_by(options[:voter_id]).where(:_id => options[:votee_id]).count == 1
99
+ end
100
+
101
+ def create_voteable_indexes
102
+ # Compound index _id and voters.up, _id and voters.down
103
+ # to make up_voted_by, down_voted_by, voted_by scopes and voting faster
104
+ # Should run in background since it introduce new index value and
105
+ # while waiting to build, the system can use _id for voting
106
+ # http://www.mongodb.org/display/DOCS/Indexing+as+a+Background+Operation
107
+ voteable_index({'votes.up' => 1, '_id' => 1}, {unique: true})
108
+ voteable_index({'votes.down' => 1, '_id' => 1}, {unique: true})
109
+
110
+ # Index counters and point for desc ordering
111
+ voteable_index({'votes.up_count' => -1})
112
+ voteable_index({'votes.down_count' => -1})
113
+ voteable_index({'votes.count' => -1})
114
+ voteable_index({'votes.point' => -1})
115
+ create_indexes
116
+ end
117
+ end
118
+
119
+ # Make a vote on this votee
120
+ #
121
+ # @param [Hash] options a hash containings:
122
+ # - :voter_id: the voter document id
123
+ # - :value: vote :up or vote :down
124
+ # - :revote: change from vote up to vote down
125
+ # - :unvote: unvote the vote value (:up or :down)
126
+ def vote(options)
127
+ options[:votee_id] = id
128
+ options[:votee] = self
129
+ options[:voter_id] ||= options[:voter].id
130
+
131
+ if options[:unvote]
132
+ options[:value] ||= vote_value(options[:voter_id])
133
+ else
134
+ options[:revote] ||= vote_value(options[:voter_id]).present?
135
+ end
136
+
137
+ self.class.vote(options)
138
+ end
139
+
140
+ # Get a voted value on this votee
141
+ #
142
+ # @param voter is object or the id of the voter who made the vote
143
+ def vote_value(voter)
144
+ voter_id = Helpers.get_mongo_id(voter)
145
+ return :up if up_voter_ids.include?(voter_id)
146
+ return :down if down_voter_ids.include?(voter_id)
147
+ end
148
+
149
+ def voted_by?(voter)
150
+ !!vote_value(voter)
151
+ end
152
+
153
+ # Array of up voter ids
154
+ def up_voter_ids
155
+ votes.try(:[], 'up') || []
156
+ end
157
+
158
+ # Array of down voter ids
159
+ def down_voter_ids
160
+ votes.try(:[], 'down') || []
161
+ end
162
+
163
+ # Array of voter ids
164
+ def voter_ids
165
+ up_voter_ids + down_voter_ids
166
+ end
167
+
168
+ # Get the number of up votes
169
+ def up_votes_count
170
+ votes.try(:[], 'up_count') || 0
171
+ end
172
+
173
+ # Get the number of down votes
174
+ def down_votes_count
175
+ votes.try(:[], 'down_count') || 0
176
+ end
177
+
178
+ # Get the number of votes
179
+ def votes_count
180
+ votes.try(:[], 'count') || 0
181
+ end
182
+
183
+ # Get the votes point
184
+ def votes_point
185
+ votes.try(:[], 'point') || 0
186
+ end
187
+
188
+ # Get up voters
189
+ def up_voters(klass)
190
+ klass.where(:_id => { '$in' => up_voter_ids })
191
+ end
192
+
193
+ # Get down voters
194
+ def down_voters(klass)
195
+ klass.where(:_id => { '$in' => down_voter_ids })
196
+ end
197
+
198
+ # Get voters
199
+ def voters(klass)
200
+ klass.where(:_id => { '$in' => voter_ids })
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,91 @@
1
+ module Mongo
2
+ module Voter
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ scope :up_voted_for, lambda { |votee| where(:_id => { '$in' => votee.up_voter_ids }) }
7
+ scope :down_voted_for, lambda { |votee| where(:_id => { '$in' => votee.down_voter_ids }) }
8
+ scope :voted_for, lambda { |votee| where(:_id => { '$in' => votee.voter_ids }) }
9
+ end
10
+
11
+ # Check to see if this voter voted on the votee or not
12
+ #
13
+ # @param [Hash, Object] options the hash containing the votee, or the votee itself
14
+ # @return [true, false] true if voted, false otherwise
15
+ def voted?(options)
16
+ unless options.is_a?(Hash)
17
+ votee_class = options.class
18
+ votee_id = options.id
19
+ else
20
+ votee = options[:votee]
21
+ if votee
22
+ votee_class = votee.class
23
+ votee_id = votee.id
24
+ else
25
+ votee_class = options[:votee_class]
26
+ votee_id = options[:votee_id]
27
+ end
28
+ end
29
+
30
+ votee_class.voted?(:voter_id => id, :votee_id => votee_id)
31
+ end
32
+
33
+ # Get the voted value on a votee
34
+ #
35
+ # @param (see #voted?)
36
+ # @return [Symbol, nil] :up or :down or nil if not voted
37
+ def vote_value(options)
38
+ votee = unless options.is_a?(Hash)
39
+ options
40
+ else
41
+ options[:votee] || options[:votee_class].find(options[:votee_id])
42
+ end
43
+ votee.vote_value(_id)
44
+ end
45
+
46
+ # Cancel the vote on a votee
47
+ #
48
+ # @param [Object] votee the votee to be unvoted
49
+ def unvote(options)
50
+ unless options.is_a?(Hash)
51
+ options = { :votee => options }
52
+ end
53
+ options[:unvote] = true
54
+ options[:revote] = false
55
+ vote(options)
56
+ end
57
+
58
+ # Vote on a votee
59
+ #
60
+ # @param (see #voted?)
61
+ # @param [:up, :down] vote_value vote up or vote down, nil to unvote
62
+ def vote(options, value = nil)
63
+ if options.is_a?(Hash)
64
+ votee = options[:votee]
65
+ else
66
+ votee = options
67
+ options = { :votee => votee, :value => value }
68
+ end
69
+
70
+ if votee
71
+ options[:votee_id] = votee.id
72
+ votee_class = votee.class
73
+ else
74
+ votee_class = options[:votee_class]
75
+ end
76
+
77
+ if options[:value].nil?
78
+ options[:unvote] = true
79
+ options[:value] = vote_value(options)
80
+ else
81
+ options[:revote] = options.has_key?(:revote) ? !options[:revote].blank? : voted?(options)
82
+ end
83
+
84
+ options[:voter] = self
85
+ options[:voter_id] = id
86
+
87
+ (votee || votee_class).vote(options)
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,214 @@
1
+ module Mongo
2
+ module Voteable
3
+ module Voting
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Make a vote on an object of this class
8
+ #
9
+ # @param [Hash] options a hash containings:
10
+ # - :votee_id: the votee document id
11
+ # - :voter_id: the voter document id
12
+ # - :value: :up or :down
13
+ # - :revote: if true change vote vote from :up to :down and vise versa
14
+ # - :unvote: if true undo the voting
15
+ #
16
+ # @return [votee, false]
17
+ def vote(options)
18
+ validate_and_normalize_vote_options(options)
19
+ options[:voteable] = VOTEABLE[name][name]
20
+
21
+ if options[:voteable]
22
+ query, update = if options[:revote]
23
+ revote_query_and_update(options)
24
+ elsif options[:unvote]
25
+ unvote_query_and_update(options)
26
+ else
27
+ new_vote_query_and_update(options)
28
+ end
29
+
30
+ # http://www.mongodb.org/display/DOCS/findAndModify+Command
31
+ begin
32
+ doc = where(query).find_and_modify(
33
+ update,
34
+ :new => true
35
+ )
36
+ rescue Moped::Errors::OperationFailure
37
+ doc = nil
38
+ end
39
+
40
+ if doc
41
+ update_parent_votes(doc, options) if options[:voteable][:update_parents]
42
+ # Update new votes data
43
+ options[:votee].write_attribute('votes', doc['votes']) if options[:votee]
44
+ options[:votee] || new(doc.as_document)
45
+ else
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+
52
+ private
53
+ def validate_and_normalize_vote_options(options)
54
+ options.symbolize_keys!
55
+ options[:votee_id] = Helpers.try_to_convert_string_to_object_id(options[:votee_id])
56
+ options[:voter_id] = Helpers.try_to_convert_string_to_object_id(options[:voter_id])
57
+ options[:value] &&= options[:value].to_sym
58
+ end
59
+
60
+ def new_vote_query_and_update(options)
61
+ if options[:value] == :up
62
+ positive_voter_ids = 'votes.up'
63
+ positive_votes_count = 'votes.up_count'
64
+ else
65
+ positive_voter_ids = 'votes.down'
66
+ positive_votes_count = 'votes.down_count'
67
+ end
68
+
69
+ return {
70
+ # Validate voter_id did not vote for votee_id yet
71
+ :_id => options[:votee_id],
72
+ 'votes.up' => { '$ne' => options[:voter_id] },
73
+ 'votes.down' => { '$ne' => options[:voter_id] }
74
+ }, {
75
+ # then update
76
+ '$push' => { positive_voter_ids => options[:voter_id] },
77
+ '$inc' => {
78
+ 'votes.count' => +1,
79
+ positive_votes_count => +1,
80
+ 'votes.point' => options[:voteable][options[:value]]
81
+ }
82
+ }
83
+ end
84
+
85
+
86
+ def revote_query_and_update(options)
87
+ if options[:value] == :up
88
+ positive_voter_ids = 'votes.up'
89
+ negative_voter_ids = 'votes.down'
90
+ positive_votes_count = 'votes.up_count'
91
+ negative_votes_count = 'votes.down_count'
92
+ point_delta = options[:voteable][:up] - options[:voteable][:down]
93
+ else
94
+ positive_voter_ids = 'votes.down'
95
+ negative_voter_ids = 'votes.up'
96
+ positive_votes_count = 'votes.down_count'
97
+ negative_votes_count = 'votes.up_count'
98
+ point_delta = -options[:voteable][:up] + options[:voteable][:down]
99
+ end
100
+
101
+ return {
102
+ # Validate voter_id did a vote with value for votee_id
103
+ :_id => options[:votee_id],
104
+ # Can skip $ne validation since creating a new vote
105
+ # already warranty that a voter can vote one only
106
+ # positive_voter_ids => { '$ne' => options[:voter_id] },
107
+ negative_voter_ids => options[:voter_id]
108
+ }, {
109
+ # then update
110
+ '$pull' => { negative_voter_ids => options[:voter_id] },
111
+ '$push' => { positive_voter_ids => options[:voter_id] },
112
+ '$inc' => {
113
+ positive_votes_count => +1,
114
+ negative_votes_count => -1,
115
+ 'votes.point' => point_delta
116
+ }
117
+ }
118
+ end
119
+
120
+
121
+ def unvote_query_and_update(options)
122
+ if options[:value] == :up
123
+ positive_voter_ids = 'votes.up'
124
+ negative_voter_ids = 'votes.down'
125
+ positive_votes_count = 'votes.up_count'
126
+ else
127
+ positive_voter_ids = 'votes.down'
128
+ negative_voter_ids = 'votes.up'
129
+ positive_votes_count = 'votes.down_count'
130
+ end
131
+
132
+ return {
133
+ :_id => options[:votee_id],
134
+ # Validate if voter_id did a vote with value for votee_id
135
+ # Can skip $ne validation since creating a new vote
136
+ # already warranty that a voter can vote one only
137
+ # negative_voter_ids => { '$ne' => options[:voter_id] },
138
+ positive_voter_ids => options[:voter_id]
139
+ }, {
140
+ # then update
141
+ '$pull' => { positive_voter_ids => options[:voter_id] },
142
+ '$inc' => {
143
+ positive_votes_count => -1,
144
+ 'votes.count' => -1,
145
+ 'votes.point' => - options[:voteable][options[:value]]
146
+ }
147
+ }
148
+ end
149
+
150
+
151
+ def update_parent_votes(doc, options)
152
+ VOTEABLE[name].each do |class_name, voteable|
153
+
154
+ if metadata = voteable_relation(class_name)
155
+
156
+ if (parent_id = doc[voteable_foreign_key(metadata)]).present?
157
+ parent_ids = parent_id.is_a?(Array) ? parent_id : [ parent_id ]
158
+ class_name.constantize.collection.find({'_id' => {'$in' => parent_ids}}).update_all(
159
+ { '$inc' => parent_inc_options(voteable, options) },
160
+ )
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+
167
+ def parent_inc_options(voteable, options)
168
+ inc_options = {}
169
+
170
+ if options[:revote]
171
+ if options[:value] == :up
172
+ inc_options['votes.point'] = voteable[:up] - voteable[:down]
173
+ unless voteable[:update_counters] == false
174
+ inc_options['votes.up_count'] = +1
175
+ inc_options['votes.down_count'] = -1
176
+ end
177
+ else
178
+ inc_options['votes.point'] = -voteable[:up] + voteable[:down]
179
+ unless voteable[:update_counters] == false
180
+ inc_options['votes.up_count'] = -1
181
+ inc_options['votes.down_count'] = +1
182
+ end
183
+ end
184
+
185
+ elsif options[:unvote]
186
+ inc_options['votes.point'] = -voteable[options[:value]]
187
+ unless voteable[:update_counters] == false
188
+ inc_options['votes.count'] = -1
189
+ if options[:value] == :up
190
+ inc_options['votes.up_count'] = -1
191
+ else
192
+ inc_options['votes.down_count'] = -1
193
+ end
194
+ end
195
+
196
+ else # new vote
197
+ inc_options['votes.point'] = voteable[options[:value]]
198
+ unless voteable[:update_counters] == false
199
+ inc_options['votes.count'] = +1
200
+ if options[:value] == :up
201
+ inc_options['votes.up_count'] = +1
202
+ else
203
+ inc_options['votes.down_count'] = +1
204
+ end
205
+ end
206
+ end
207
+
208
+ inc_options
209
+ end
210
+ end
211
+
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,8 @@
1
+ require 'voteable_mongo/helpers'
2
+ require 'voteable_mongo/voteable'
3
+ require 'voteable_mongo/voter'
4
+ require 'voteable_mongo/tasks'
5
+
6
+ if defined?(Rails)
7
+ require 'voteable_mongo/railtie'
8
+ end
data/spec/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --debugger
@@ -0,0 +1,10 @@
1
+ class Category
2
+ include Mongoid::Document
3
+ include Mongo::Voteable
4
+
5
+ field :name
6
+
7
+ has_and_belongs_to_many :posts
8
+
9
+ voteable self, :index => true
10
+ end
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), 'post')
2
+
3
+ class Comment
4
+ include Mongoid::Document
5
+ include Mongo::Voteable
6
+
7
+ field :content
8
+
9
+ belongs_to :post
10
+
11
+ voteable self, :up => +1, :down => -3
12
+ voteable Post, :up => +2, :down => -1
13
+ end
@@ -0,0 +1,17 @@
1
+ require File.join(File.dirname(__FILE__), 'category')
2
+
3
+ class Post
4
+ include Mongoid::Document
5
+ include Mongo::Voteable
6
+
7
+ field :title
8
+ field :content
9
+
10
+ has_and_belongs_to_many :categories
11
+ has_many :comments
12
+
13
+ field :_id, type: String, default: -> { title }
14
+
15
+ voteable self, :up => +1, :down => -1, :index => true
16
+ voteable Category, :up => +3, :down => -5, :update_counters => false
17
+ end
@@ -0,0 +1,4 @@
1
+ class User
2
+ include Mongoid::Document
3
+ include Mongo::Voter
4
+ end
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'mongoid'
6
+ models_folder = File.join(File.dirname(__FILE__), 'mongoid/models')
7
+ Mongoid.configure do |config|
8
+ name = 'voteable_mongo_test'
9
+ host = 'localhost'
10
+ config.connect_to(name)
11
+ end
12
+
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+
16
+
17
+ require 'voteable_mongo'
18
+ require 'rspec'
19
+ require 'rspec/autorun'
20
+
21
+ Dir[ File.join(models_folder, '*.rb') ].each { |file|
22
+ require file
23
+ file_name = File.basename(file).sub('.rb', '')
24
+ klass = file_name.classify.constantize
25
+ begin
26
+ klass.collection.drop
27
+ rescue Exception => e
28
+ print e.message
29
+ end
30
+ }
@@ -0,0 +1,34 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Mongo::Voteable::Tasks do
4
+ describe 'Mongo::Voteable::Tasks.init_stats' do
5
+ before :all do
6
+ @post1 = Post.create!(:title => 'post1')
7
+ @post2 = Post.create!(:title => 'post2')
8
+ end
9
+
10
+ it 'after create votes has default value' do
11
+ @post1.votes.should == ::Mongo::Voteable::DEFAULT_VOTES
12
+ @post2.votes.should == ::Mongo::Voteable::DEFAULT_VOTES
13
+ end
14
+
15
+ it 'reset votes data' do
16
+ @post1.votes = nil
17
+ @post1.save
18
+
19
+ @post2.votes = nil
20
+ @post2.save
21
+ end
22
+
23
+ it 'init_stats recover votes default value' do
24
+ ::Mongo::Voteable::Tasks.init_stats
25
+ ::Mongo::Voteable::Tasks.migrate_old_votes
26
+
27
+ @post1.reload
28
+ @post2.reload
29
+
30
+ @post1.votes.should == ::Mongo::Voteable::DEFAULT_VOTES
31
+ @post2.votes.should == ::Mongo::Voteable::DEFAULT_VOTES
32
+ end
33
+ end
34
+ end