rs_voteable_mongo 1.0.0

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