voteable_mongo 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,162 @@
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.collection.update({:votes => nil}, {
13
+ '$set' => { :votes => Votes::DEFAULT_ATTRIBUTES }
14
+ }, {
15
+ :safe => true,
16
+ :multi => true
17
+ })
18
+ end
19
+ end
20
+
21
+ # Re-generate vote counters and vote points
22
+ def self.remake_stats(log = false)
23
+ remake_stats_for_all_voteable_classes(log)
24
+ update_parent_stats(log)
25
+ end
26
+
27
+ # Convert votes from from version < 0.7.0 to new data store
28
+ def self.migrate_old_votes(log = false)
29
+ VOTEABLE.each do |class_name, voteable|
30
+ klass = class_name.constantize
31
+ klass_voteable = voteable[class_name]
32
+ puts "* Migrating old vote data for #{class_name} ..." if log
33
+ migrate_old_votes_for(klass, klass_voteable)
34
+ end
35
+ end
36
+
37
+ def self.migrate_old_votes_for(klass, voteable)
38
+ klass.all.each do |doc|
39
+ # Version 0.6.x use very short field names (u, d, uc, dc, c, p) to minimize
40
+ # votes storage but it's not human friendly
41
+ # Version >= 0.7.0 use readable field names (up, down, up_count, down_count,
42
+ # count, point)
43
+ votes = doc['votes'] || doc['voteable'] || {}
44
+
45
+ up_voter_ids = votes['up'] || votes['u'] ||
46
+ votes['up_voter_ids'] || doc['up_voter_ids'] || []
47
+
48
+ down_voter_ids = votes['down'] || votes['d'] ||
49
+ votes['down_voter_ids'] || doc['down_voter_ids'] || []
50
+
51
+ up_count = up_voter_ids.size
52
+ down_count = down_voter_ids.size
53
+
54
+ klass.collection.update({ :_id => doc.id }, {
55
+ '$set' => {
56
+ 'votes' => {
57
+ 'up' => up_voter_ids,
58
+ 'down' => down_voter_ids,
59
+ 'up_count' => up_count,
60
+ 'down_count' => down_count,
61
+ 'count' => up_count + down_count,
62
+ 'point' => voteable[:up].to_i*up_count + voteable[:down].to_i*down_count
63
+ }
64
+ },
65
+ '$unset' => {
66
+ 'up_voter_ids' => true,
67
+ 'down_voter_ids' => true,
68
+ 'votes_count' => true,
69
+ 'votes_point' => true,
70
+ 'voteable' => true
71
+ # 'votes.u' => true,
72
+ # 'votes.d' => true,
73
+ # 'votes.uc' => true,
74
+ # 'votes.dc' => true,
75
+ # 'votes.c' => true,
76
+ # 'votes.p' => true
77
+ }
78
+ }, { :safe => true })
79
+ end
80
+ end
81
+
82
+
83
+ def self.remake_stats_for_all_voteable_classes(log)
84
+ VOTEABLE.each do |class_name, voteable|
85
+ klass = class_name.constantize
86
+ klass_voteable = voteable[class_name]
87
+ puts "Generating stats for #{class_name}" if log
88
+ klass.all.each{ |doc|
89
+ remake_stats_for(doc, klass_voteable)
90
+ }
91
+ end
92
+ end
93
+
94
+
95
+ def self.remake_stats_for(doc, voteable)
96
+ up_count = doc.up_voter_ids.length
97
+ down_count = doc.down_voter_ids.length
98
+
99
+ doc.update_attributes(
100
+ 'votes.up_count' => up_count,
101
+ 'votes.down_count' => down_count,
102
+ 'votes.count' => up_count + down_count,
103
+ 'votes.point' => voteable[:up].to_i*up_count + voteable[:down].to_i*down_count
104
+ )
105
+ end
106
+
107
+
108
+ def self.update_parent_stats(log)
109
+ VOTEABLE.each do |class_name, voteable|
110
+ klass = class_name.constantize
111
+ voteable.each do |parent_class_name, parent_voteable|
112
+ relation_metadata = klass.relations[parent_class_name.underscore]
113
+ if relation_metadata
114
+ parent_class = parent_class_name.constantize
115
+ foreign_key = relation_metadata.foreign_key
116
+ puts "Updating stats for #{class_name} > #{parent_class_name}" if log
117
+ klass.all.each{ |doc|
118
+ update_parent_stats_for(doc, parent_class, foreign_key, parent_voteable)
119
+ }
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+ def self.update_parent_stats_for(doc, parent_class, foreign_key, voteable)
127
+ parent_id = doc.read_attribute(foreign_key.to_sym)
128
+ if parent_id
129
+ up_count = doc.up_voter_ids.length
130
+ down_count = doc.down_voter_ids.length
131
+
132
+ return if up_count == 0 && down_count == 0
133
+
134
+ inc_options = {
135
+ 'votes.point' => voteable[:up].to_i*up_count + voteable[:down].to_i*down_count
136
+ }
137
+
138
+ unless voteable[:update_counters] == false
139
+ inc_options.merge!(
140
+ 'votes.count' => up_count + down_count,
141
+ 'votes.up_count' => up_count,
142
+ 'votes.down_count' => down_count
143
+ )
144
+ end
145
+
146
+ parent_class.collection.update(
147
+ { '_id' => parent_id },
148
+ { '$inc' => inc_options },
149
+ { :safe => true }
150
+ )
151
+ end
152
+ end
153
+
154
+ private_class_method :migrate_old_votes_for,
155
+ :remake_stats_for,
156
+ :remake_stats_for_all_voteable_classes,
157
+ :update_parent_stats,
158
+ :update_parent_stats_for
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,19 @@
1
+ module Mongo
2
+ module Voteable
3
+
4
+ class Votes
5
+ include Mongoid::Document
6
+
7
+ field :up, :type => Array, :default => []
8
+ field :down, :type => Array, :default => []
9
+ field :up_count, :type => Integer, :default => 0
10
+ field :down_count, :type => Integer, :default => 0
11
+ field :count, :type => Integer, :default => 0
12
+ field :point, :type => Integer, :default => 0
13
+
14
+ DEFAULT_ATTRIBUTES = new.attributes
15
+ DEFAULT_ATTRIBUTES.delete('_id')
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,229 @@
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
+ # - :return_votee: if true always return updated voteable object
16
+ #
17
+ # @return [votes, false, nil]
18
+ def vote(options)
19
+ validate_and_normalize_vote_options(options)
20
+ options[:voteable] = VOTEABLE[name][name]
21
+
22
+ if options[:voteable]
23
+ query, update = if options[:revote]
24
+ revote_query_and_update(options)
25
+ elsif options[:unvote]
26
+ unvote_query_and_update(options)
27
+ else
28
+ new_vote_query_and_update(options)
29
+ end
30
+
31
+ if options[:voteable][:update_parents] || options[:votee] || options[:return_votee]
32
+ # If votee exits or need to update parent
33
+ # use Collection#find_and_modify to retrieve updated votes data and parent_ids
34
+ begin
35
+ doc = collection.master.collection.find_and_modify(
36
+ :query => query,
37
+ :update => update,
38
+ :new => true
39
+ )
40
+ # Update new votes data
41
+ options[:votee].write_attribute('votes', doc['votes']) if options[:votee]
42
+ update_parent_votes(doc, options) if options[:voteable][:update_parents]
43
+ return options[:votee] || new(doc)
44
+ rescue Exception => e
45
+ # $stderr.puts "Mongo error: #{e.inspect}" # DEBUG
46
+ # Don't update parents if operation fail or no matched object found
47
+ return false
48
+ end
49
+ else
50
+ # Just update and don't care the result
51
+ collection.update(query, update)
52
+ end
53
+ end
54
+ end
55
+
56
+
57
+ private
58
+ def validate_and_normalize_vote_options(options)
59
+ options.symbolize_keys!
60
+ options[:votee_id] = BSON::ObjectId(options[:votee_id]) if options[:votee_id].is_a?(String)
61
+ options[:voter_id] = BSON::ObjectId(options[:voter_id]) if options[:voter_id].is_a?(String)
62
+ options[:value] &&= options[:value].to_sym
63
+ end
64
+
65
+ def new_vote_query_and_update(options)
66
+ if options[:value] == :up
67
+ positive_voter_ids = 'votes.up'
68
+ positive_votes_count = 'votes.up_count'
69
+ else
70
+ positive_voter_ids = 'votes.down'
71
+ positive_votes_count = 'votes.down_count'
72
+ end
73
+
74
+ return {
75
+ # Validate voter_id did not vote for votee_id yet
76
+ :_id => options[:votee_id],
77
+ 'votes.up' => { '$ne' => options[:voter_id] },
78
+ 'votes.down' => { '$ne' => options[:voter_id] }
79
+ }, {
80
+ # then update
81
+ '$push' => { positive_voter_ids => options[:voter_id] },
82
+ '$inc' => {
83
+ 'votes.count' => +1,
84
+ positive_votes_count => +1,
85
+ 'votes.point' => options[:voteable][options[:value]]
86
+ }
87
+ }
88
+ end
89
+
90
+
91
+ def revote_query_and_update(options)
92
+ if options[:value] == :up
93
+ positive_voter_ids = 'votes.up'
94
+ negative_voter_ids = 'votes.down'
95
+ positive_votes_count = 'votes.up_count'
96
+ negative_votes_count = 'votes.down_count'
97
+ point_delta = options[:voteable][:up] - options[:voteable][:down]
98
+ else
99
+ positive_voter_ids = 'votes.down'
100
+ negative_voter_ids = 'votes.up'
101
+ positive_votes_count = 'votes.down_count'
102
+ negative_votes_count = 'votes.up_count'
103
+ point_delta = -options[:voteable][:up] + options[:voteable][:down]
104
+ end
105
+
106
+ return {
107
+ # Validate voter_id did a vote with value for votee_id
108
+ :_id => options[:votee_id],
109
+ # Can skip $ne validation since creating a new vote
110
+ # already warranty that a voter can vote one only
111
+ # positive_voter_ids => { '$ne' => options[:voter_id] },
112
+ negative_voter_ids => options[:voter_id]
113
+ }, {
114
+ # then update
115
+ '$pull' => { negative_voter_ids => options[:voter_id] },
116
+ '$push' => { positive_voter_ids => options[:voter_id] },
117
+ '$inc' => {
118
+ positive_votes_count => +1,
119
+ negative_votes_count => -1,
120
+ 'votes.point' => point_delta
121
+ }
122
+ }
123
+ end
124
+
125
+
126
+ def unvote_query_and_update(options)
127
+ if options[:value] == :up
128
+ positive_voter_ids = 'votes.up'
129
+ negative_voter_ids = 'votes.down'
130
+ positive_votes_count = 'votes.up_count'
131
+ else
132
+ positive_voter_ids = 'votes.down'
133
+ negative_voter_ids = 'votes.up'
134
+ positive_votes_count = 'votes.down_count'
135
+ end
136
+
137
+ return {
138
+ :_id => options[:votee_id],
139
+ # Validate if voter_id did a vote with value for votee_id
140
+ # Can skip $ne validation since creating a new vote
141
+ # already warranty that a voter can vote one only
142
+ # negative_voter_ids => { '$ne' => options[:voter_id] },
143
+ positive_voter_ids => options[:voter_id]
144
+ }, {
145
+ # then update
146
+ '$pull' => { positive_voter_ids => options[:voter_id] },
147
+ '$inc' => {
148
+ positive_votes_count => -1,
149
+ 'votes.count' => -1,
150
+ 'votes.point' => -options[:voteable][options[:value]]
151
+ }
152
+ }
153
+ end
154
+
155
+
156
+ def update_parent_votes(doc, options)
157
+ VOTEABLE[name].each do |class_name, voteable|
158
+ # For other class in VOTEABLE options, if has relationship with current class
159
+ relation_metadata = relations.find{ |x, r| r.class_name == class_name }.try(:last)
160
+ next unless relation_metadata.present?
161
+
162
+ # If cannot find current votee foreign_key value for that class
163
+ foreign_key_value = doc[relation_metadata.foreign_key.to_s]
164
+ next unless foreign_key_value.present?
165
+
166
+ if relation_metadata.relation == ::Mongoid::Relations::Referenced::In
167
+ class_name.constantize.collection.update(
168
+ { '_id' => foreign_key_value },
169
+ { '$inc' => parent_inc_options(voteable, options) }
170
+ )
171
+ elsif relation_metadata.relation == ::Mongoid::Relations::Referenced::ManyToMany
172
+ class_name.constantize.collection.update(
173
+ { '_id' => { '$in' => foreign_key_value } },
174
+ { '$inc' => parent_inc_options(voteable, options) },
175
+ { :multi => true }
176
+ )
177
+ end
178
+ end
179
+ end
180
+
181
+
182
+ def parent_inc_options(voteable, options)
183
+ inc_options = {}
184
+
185
+ if options[:revote]
186
+ if options[:value] == :up
187
+ inc_options['votes.point'] = voteable[:up] - voteable[:down]
188
+ unless voteable[:update_counters] == false
189
+ inc_options['votes.up_count'] = +1
190
+ inc_options['votes.down_count'] = -1
191
+ end
192
+ else
193
+ inc_options['votes.point'] = -voteable[:up] + voteable[:down]
194
+ unless voteable[:update_counters] == false
195
+ inc_options['votes.up_count'] = -1
196
+ inc_options['votes.down_count'] = +1
197
+ end
198
+ end
199
+
200
+ elsif options[:unvote]
201
+ inc_options['votes.point'] = -voteable[options[:value]]
202
+ unless voteable[:update_counters] == false
203
+ inc_options['votes.count'] = -1
204
+ if options[:value] == :up
205
+ inc_options['votes.up_count'] = -1
206
+ else
207
+ inc_options['votes.down_count'] = -1
208
+ end
209
+ end
210
+
211
+ else # new vote
212
+ inc_options['votes.point'] = voteable[options[:value]]
213
+ unless voteable[:update_counters] == false
214
+ inc_options['votes.count'] = +1
215
+ if options[:value] == :up
216
+ inc_options['votes.up_count'] = +1
217
+ else
218
+ inc_options['votes.down_count'] = +1
219
+ end
220
+ end
221
+ end
222
+
223
+ inc_options
224
+ end
225
+ end
226
+
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,97 @@
1
+ module Mongo
2
+ module Voter
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include ::Mongoid::Document
7
+
8
+ scope :up_voted_for, lambda { |votee| where(:_id => { '$in' => votee.up_voter_ids }) }
9
+ scope :down_voted_for, lambda { |votee| where(:_id => { '$in' => votee.down_voter_ids }) }
10
+ scope :voted_for, lambda { |votee| where(:_id => { '$in' => votee.voter_ids }) }
11
+ end
12
+
13
+ module InstanceMethods
14
+ # Check to see if this voter voted on the votee or not
15
+ #
16
+ # @param [Hash, Object] options the hash containing the votee, or the votee itself
17
+ # @return [true, false] true if voted, false otherwise
18
+ def voted?(options)
19
+ unless options.is_a?(Hash)
20
+ votee_class = options.class
21
+ votee_id = options.id
22
+ else
23
+ votee = options[:votee]
24
+ if votee
25
+ votee_class = votee.class
26
+ votee_id = votee.id
27
+ else
28
+ votee_class = options[:votee_type].classify.constantize
29
+ votee_id = options[:votee_id]
30
+ end
31
+ end
32
+
33
+ votee_class.voted?(:voter_id => id, :votee_id => votee_id)
34
+ end
35
+
36
+ # Get the voted value on a votee
37
+ #
38
+ # @param (see #voted?)
39
+ # @return [Symbol, nil] :up or :down or nil if not voted
40
+ def vote_value(options)
41
+ votee = unless options.is_a?(Hash)
42
+ options
43
+ else
44
+ options[:votee] || options[:votee_type].classify.constantize.only(:votes).where(
45
+ :_id => options[:votee_id]
46
+ ).first
47
+ end
48
+ votee.vote_value(_id)
49
+ end
50
+
51
+ # Cancel the vote on a votee
52
+ #
53
+ # @param [Object] votee the votee to be unvoted
54
+ def unvote(options)
55
+ unless options.is_a?(Hash)
56
+ options = { :votee => options }
57
+ end
58
+ options[:unvote] = true
59
+ options[:revote] = false
60
+ vote(options)
61
+ end
62
+
63
+ # Vote on a votee
64
+ #
65
+ # @param (see #voted?)
66
+ # @param [:up, :down] vote_value vote up or vote down, nil to unvote
67
+ def vote(options, value = nil)
68
+ if options.is_a?(Hash)
69
+ votee = options[:votee]
70
+ else
71
+ votee = options
72
+ options = { :votee => votee, :value => value }
73
+ end
74
+
75
+ if votee
76
+ options[:votee_id] = votee.id
77
+ votee_class = votee.class
78
+ else
79
+ votee_class = options[:votee_type].classify.constantize
80
+ end
81
+
82
+ if options[:value].nil?
83
+ options[:unvote] = true
84
+ options[:value] = vote_value(options)
85
+ else
86
+ options[:revote] = options.has_key?(:revote) ? !options[:revote].blank? : voted?(options)
87
+ end
88
+
89
+ options[:voter] = self
90
+ options[:voter_id] = id
91
+
92
+ ( votee || votee_class ).vote(options)
93
+ end
94
+ end
95
+
96
+ end
97
+ end