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