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.
- data/.gitignore +12 -0
- data/.rvmrc +2 -0
- data/.watchr +23 -0
- data/CHANGELOG.rdoc +71 -0
- data/Gemfile +4 -0
- data/README.rdoc +159 -0
- data/Rakefile +10 -0
- data/TODO +3 -0
- data/lib/voteable_mongo.rb +10 -0
- data/lib/voteable_mongo/railtie.rb +17 -0
- data/lib/voteable_mongo/railties/database.rake +18 -0
- data/lib/voteable_mongo/version.rb +3 -0
- data/lib/voteable_mongo/voteable.rb +199 -0
- data/lib/voteable_mongo/voteable/tasks.rb +162 -0
- data/lib/voteable_mongo/voteable/votes.rb +19 -0
- data/lib/voteable_mongo/voteable/voting.rb +229 -0
- data/lib/voteable_mongo/voter.rb +97 -0
- data/spec/.rspec +1 -0
- data/spec/models/category.rb +10 -0
- data/spec/models/comment.rb +13 -0
- data/spec/models/post.rb +13 -0
- data/spec/models/user.rb +4 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/voteable_mongo/tasks_spec.rb +33 -0
- data/spec/voteable_mongo/voteable_spec.rb +426 -0
- data/spec/voteable_mongo/voter_spec.rb +148 -0
- data/voteable_mongo.gemspec +26 -0
- metadata +131 -0
@@ -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
|