ballot 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/Contributing.md +68 -0
  3. data/History.md +5 -0
  4. data/Licence.md +27 -0
  5. data/Manifest.txt +68 -0
  6. data/README.rdoc +264 -0
  7. data/Rakefile +71 -0
  8. data/bin/ballot_generator +9 -0
  9. data/lib/ballot.rb +25 -0
  10. data/lib/ballot/action_controller.rb +32 -0
  11. data/lib/ballot/active_record.rb +152 -0
  12. data/lib/ballot/active_record/votable.rb +145 -0
  13. data/lib/ballot/active_record/vote.rb +35 -0
  14. data/lib/ballot/active_record/voter.rb +99 -0
  15. data/lib/ballot/railtie.rb +19 -0
  16. data/lib/ballot/sequel.rb +170 -0
  17. data/lib/ballot/sequel/vote.rb +99 -0
  18. data/lib/ballot/votable.rb +445 -0
  19. data/lib/ballot/vote.rb +129 -0
  20. data/lib/ballot/voter.rb +320 -0
  21. data/lib/ballot/words.rb +32 -0
  22. data/lib/generators/ballot.rb +40 -0
  23. data/lib/generators/ballot/install/install_generator.rb +27 -0
  24. data/lib/generators/ballot/install/templates/active_record/migration.rb +19 -0
  25. data/lib/generators/ballot/install/templates/sequel/migration.rb +25 -0
  26. data/lib/generators/ballot/standalone.rb +89 -0
  27. data/lib/generators/ballot/standalone/support.rb +70 -0
  28. data/lib/generators/ballot/summary/summary_generator.rb +27 -0
  29. data/lib/generators/ballot/summary/templates/active_record/migration.rb +15 -0
  30. data/lib/generators/ballot/summary/templates/sequel/migration.rb +20 -0
  31. data/lib/sequel/plugins/ballot_votable.rb +180 -0
  32. data/lib/sequel/plugins/ballot_voter.rb +125 -0
  33. data/test/active_record/ballot_votable_test.rb +16 -0
  34. data/test/active_record/ballot_voter_test.rb +13 -0
  35. data/test/active_record/rails_generator_test.rb +28 -0
  36. data/test/active_record/votable_voter_test.rb +19 -0
  37. data/test/generators/rails-activerecord/Rakefile +2 -0
  38. data/test/generators/rails-activerecord/app/.keep +0 -0
  39. data/test/generators/rails-activerecord/bin/rails +5 -0
  40. data/test/generators/rails-activerecord/config/application.rb +17 -0
  41. data/test/generators/rails-activerecord/config/boot.rb +3 -0
  42. data/test/generators/rails-activerecord/config/database.yml +12 -0
  43. data/test/generators/rails-activerecord/config/environment.rb +3 -0
  44. data/test/generators/rails-activerecord/config/routes.rb +3 -0
  45. data/test/generators/rails-activerecord/config/secrets.yml +5 -0
  46. data/test/generators/rails-activerecord/db/seeds.rb +1 -0
  47. data/test/generators/rails-activerecord/log/.keep +0 -0
  48. data/test/generators/rails-sequel/Rakefile +2 -0
  49. data/test/generators/rails-sequel/app/.keep +0 -0
  50. data/test/generators/rails-sequel/bin/rails +5 -0
  51. data/test/generators/rails-sequel/config/application.rb +14 -0
  52. data/test/generators/rails-sequel/config/boot.rb +3 -0
  53. data/test/generators/rails-sequel/config/database.yml +12 -0
  54. data/test/generators/rails-sequel/config/environment.rb +3 -0
  55. data/test/generators/rails-sequel/config/routes.rb +3 -0
  56. data/test/generators/rails-sequel/config/secrets.yml +5 -0
  57. data/test/generators/rails-sequel/db/seeds.rb +1 -0
  58. data/test/generators/rails-sequel/log/.keep +0 -0
  59. data/test/minitest_config.rb +14 -0
  60. data/test/sequel/ballot_votable_test.rb +45 -0
  61. data/test/sequel/ballot_voter_test.rb +42 -0
  62. data/test/sequel/rails_generator_test.rb +25 -0
  63. data/test/sequel/votable_voter_test.rb +19 -0
  64. data/test/sequel/vote_test.rb +105 -0
  65. data/test/support/active_record_setup.rb +145 -0
  66. data/test/support/generators_setup.rb +129 -0
  67. data/test/support/sequel_setup.rb +164 -0
  68. data/test/support/shared_examples/votable_examples.rb +630 -0
  69. data/test/support/shared_examples/voter_examples.rb +600 -0
  70. metadata +333 -0
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # This file exists for documentation purposes only. Ballot::Vote is an optional
5
+ # constant, and may be one of these values:
6
+ #
7
+ # * Ballot::Sequel::Vote (when using *only* Sequel in an application);
8
+ # * Ballot::ActiveRecord::Vote (when using *only* ActiveRecord in an application)
9
+ #++
10
+ unless defined?(Ballot::Sequel::Vote) || defined?(Ballot::ActiveRecord::Vote)
11
+ fail 'ballot/vote cannot be required directly'
12
+ end
13
+
14
+ ##
15
+ module Ballot
16
+ # A Vote represents the votes stored in the +ballot_votes+ table, holding
17
+ # votes for Votable objects by Voter objects.
18
+ #
19
+ # \ActiveRecord:: This is implemented as the Ballot::ActiveRecord::Vote
20
+ # class.
21
+ # \Sequel:: This is implemented as the Ballot::Sequel::Vote class.
22
+ #
23
+ #
24
+ # NOTE:: Ballot::Vote is implemented as a method returning the primary
25
+ # implementation of the vote class. If _only_ Ballot::Sequel::Vote is
26
+ # defined, it will be returned. If _only_ Ballot::ActiveRecord::Vote
27
+ # is defined, it will be returned. If _both_ are defined, +nil+ will
28
+ # be returned.
29
+ class Vote
30
+ #-----
31
+ # :section: Scope / Dataset Methods
32
+ #-----
33
+
34
+ ##
35
+ # Returns all positive votes.
36
+ #
37
+ # \ActiveRecord:: This is a scope.
38
+ # \Sequel:: This is a dataset module subset.
39
+ def self.up; end
40
+
41
+ ##
42
+ # Returns all negative votes.
43
+ #
44
+ # \ActiveRecord:: This is a scope.
45
+ # \Sequel:: This is a dataset module subset.
46
+ def self.down; end
47
+
48
+ ##
49
+ # Returns all votes for Votable objects of the provided +model_class+. The
50
+ # +model_class+ is resolved to the canonical name for the model, which
51
+ # differs if the model is STI-enabled.
52
+ #
53
+ # \ActiveRecord:: This is a scope.
54
+ # \Sequel:: This is a dataset module method.
55
+ def self.for_type(model_class); end
56
+
57
+ ##
58
+ # Returns all votes for Voter objects of the provided +model_class+. The
59
+ # +model_class+ is resolved to the canonical name for the model, which
60
+ # differs if the model is STI-enabled.
61
+ #
62
+ # \ActiveRecord:: This is a scope.
63
+ # \Sequel:: This is a dataset module method.
64
+ def self.by_type(model_class); end
65
+
66
+ #-----
67
+ # :section:
68
+ #-----
69
+
70
+ ##
71
+ # :attr_accessor: voter_id
72
+ # The id of the Voter record for this Vote.
73
+
74
+ ##
75
+ # :attr_accessor: voter_type
76
+ # The canonical model name for the Voter record for this Vote.
77
+
78
+ ##
79
+ # :attr_accessor: votable_id
80
+ # The id of the Votable record for this Vote.
81
+
82
+ ##
83
+ # :attr_accessor: votable_type
84
+ # The canonical model name for the Votable record for this Vote.
85
+
86
+ ##
87
+ # :attr_accessor: vote
88
+ # The state of the Vote; +true+ if a positive vote, +false+ if a negative
89
+ # vote.
90
+
91
+ ##
92
+ # :attr_accessor: scope
93
+ # The optional scope for this Vote.
94
+
95
+ ##
96
+ # :attr_accessor: weight
97
+ # The optional weight for this Vote. If missing, defaults to 1, but may be
98
+ # any integer value.
99
+
100
+ ##
101
+ # :attr_reader: created_at
102
+ # When this Vote record was created.
103
+
104
+ ##
105
+ # :attr_reader: updated_at
106
+ # When this Vote record was last updated.
107
+
108
+ ##
109
+ # :attr_accessor: voter
110
+ # The associated Voter for this Vote. Determined from #voter_id and
111
+ # #voter_type.
112
+
113
+ ##
114
+ # :attr_accessor: votable
115
+ # The associated Votable for this Vote. Determined from #votable_id and
116
+ # #votable_type.
117
+ end
118
+ remove_const :Vote
119
+
120
+ #--
121
+ def self.Vote # :nodoc:
122
+ if defined?(Ballot::Sequel::Vote) && !defined?(Ballot::ActiveRecord::Vote)
123
+ Ballot::Sequel::Vote
124
+ elsif defined?(Ballot::ActiveRecord::Vote)
125
+ Ballot::ActiveRecord::Vote
126
+ end
127
+ end
128
+ #++
129
+ end
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ module Ballot
5
+ # Methods added to a model that is marked as a Voter.
6
+ module Voter
7
+ #-----
8
+ # :section: Recording Votes
9
+ #-----
10
+
11
+ ##
12
+ # :method: cast_ballot_for
13
+ # :call-seq:
14
+ # cast_ballot_for(votable = nil, kwargs = {})
15
+ # cast_ballot_for(votable)
16
+ # cast_ballot_for(votable_id: id, votable_type: type)
17
+ # cast_ballot_for(votable_gid: gid)
18
+ # cast_ballot_for(votable, scope: scope, vote: false, weight: true)
19
+ #
20
+ # Record a Vote for this Voter on the provided +votable+. The +votable+ may
21
+ # be specified as its own parameter, or through the keyword arguments
22
+ # +votable_id+, +votable_type+, +votable_gid+, or +votable+ (note that the
23
+ # parameter +votable+ will override the keyword argument +votable+, if both
24
+ # are provided).
25
+ #
26
+ # Additional named arguments may be provided through +kwargs+:
27
+ #
28
+ # scope:: The scope of the vote to be recorded. Defaults to +nil+.
29
+ # vote:: The vote to be recorded. Defaults to +true+ and is parsed through
30
+ # Ballot::Words.truthy?.
31
+ # weight:: The weight of the vote to be recorded. Defaults to +1+.
32
+ # duplicate:: Allow a duplicate vote to be recorded. This is not
33
+ # recommended as it has negative performance implications at
34
+ # scale.
35
+ #
36
+ # Other arguments are ignored.
37
+ #
38
+ # \ActiveRecord:: There are no special notes for ActiveRecord.
39
+ # \Sequel:: GlobalID does not currently provide support for Sequel. The use
40
+ # of +votable_gid+ in this case will probably fail.
41
+ #
42
+ # <em>Also aliased as: #ballot_for.</em>
43
+
44
+ ##
45
+ # :method: ballot_for
46
+ # :call-seq:
47
+ # ballot_for(votable = nil, kwargs = {})
48
+ # ballot_for(votable)
49
+ # ballot_for(votable_id: id, votable_type: type)
50
+ # ballot_for(votable_gid: gid)
51
+ # ballot_for(votable, scope: scope, vote: false, weight: true)
52
+ #
53
+ # <em>Alias for: #cast_ballot_for.</em>
54
+
55
+ ##
56
+ # Records a positive vote by this Voter on the provided +votable+ with
57
+ # options provided in +kwargs+. Any value passed to the +vote+ keyword
58
+ # argument will be ignored. See #cast_ballot_for for more details.
59
+ def cast_up_ballot_for(votable = nil, kwargs = {})
60
+ cast_ballot_for(votable, kwargs.merge(vote: true))
61
+ end
62
+ alias up_ballot_for cast_up_ballot_for
63
+
64
+ ##
65
+ # Records a negative vote by this Voter on the provided +votable+ with
66
+ # options provided in +kwargs+. Any value passed to the +vote+ keyword
67
+ # argument will be ignored. See #cast_ballot_for for more details.
68
+ def cast_down_ballot_for(votable = nil, kwargs = {})
69
+ cast_ballot_for(votable, kwargs.merge(vote: false))
70
+ end
71
+ alias down_ballot_for cast_down_ballot_for
72
+
73
+ ##
74
+ # :method: remove_ballot_for
75
+ # :call-seq:
76
+ # remove_ballot_for(votable = nil, kwargs = {})
77
+ # remove_ballot_for(votable)
78
+ # remove_ballot_for(votable_id: id, votable_type: type)
79
+ # remove_ballot_for(votable_gid: gid)
80
+ # remove_ballot_for(votable, scope: scope)
81
+ #
82
+ # Remove any votes by this Voter for the provided +votable+. The +votable+
83
+ # may be specified as its own parameter, or through the keyword arguments
84
+ # +votable_id+, +votable_type+, +votable_gid+, or +votable+ (note that the
85
+ # parameter +votable+ will override the keyword argument +votable+, if both
86
+ # are provided).
87
+ #
88
+ # Only the +scope+ argument is available through +kwargs+:
89
+ #
90
+ # scope:: The scope of the vote to be recorded. Defaults to +nil+.
91
+ #
92
+ # Other arguments are ignored.
93
+ #
94
+ # \ActiveRecord:: There are no special notes for \ActiveRecord.
95
+ # \Sequel:: GlobalID does not currently provide support for \Sequel, so
96
+ # there are many cases where attempting to use +votable_gid+ will
97
+ # fail.
98
+
99
+ #-----
100
+ # :section: Finding Votes
101
+ #-----
102
+
103
+ ##
104
+ # :method: ballots_by
105
+ #
106
+ # The votes attached to this Votable.
107
+ #
108
+ # \ActiveRecord:: This is generated by the polymorphic association
109
+ # <tt>has_many :ballots_by</tt>.
110
+ # \Sequel:: This is generated by the polymorphic association
111
+ # <tt>one_to_many :ballots_by</tt>
112
+
113
+ ##
114
+ # :method: ballots_for_dataset
115
+ #
116
+ # The \Sequel association dataset for votes attached to this Voter.
117
+ #
118
+ # \ActiveRecord:: This does not exist for \ActiveRecord.
119
+ # \Sequel:: This is generated by the polymorphic association
120
+ # <tt>one_to_many :ballots_for</tt>
121
+
122
+ ##
123
+ # Returns ballots by this Voter where the recorded vote is positive.
124
+ #
125
+ # \ActiveRecord:: There are no special notes for ActiveRecord.
126
+ # \Sequel:: This method returns the _dataset_; if vote objects are desired,
127
+ # use <tt>up_ballots_by.all</tt>.
128
+ def up_ballots_by(kwargs = {})
129
+ find_ballots_by(vote: true, scope: kwargs[:scope])
130
+ end
131
+
132
+ ##
133
+ # Returns ballots by this Voter where the recorded vote is negative.
134
+ #
135
+ # \ActiveRecord:: There are no special notes for ActiveRecord.
136
+ # \Sequel:: This method returns the _dataset_; if vote objects are desired,
137
+ # use <tt>down_ballots_by.all</tt>.
138
+ def down_ballots_by(kwargs = {})
139
+ find_ballots_by(vote: false, scope: kwargs[:scope])
140
+ end
141
+
142
+ #-----
143
+ # :section: Votable Inquiries
144
+ #-----
145
+
146
+ ##
147
+ # :method: cast_ballot_for?
148
+ # :call-seq:
149
+ # ballot_for?(votable = nil, kwargs = {})
150
+ # ballot_for?(votable)
151
+ # ballot_for?(votable_id: id, votable_type: type)
152
+ # ballot_for?(votable_gid: gid)
153
+ # ballot_for?(votable, scope: scope, vote: false, weight: true)
154
+ #
155
+ # Returns +true+ if this Voter has voted for the provided +votable+
156
+ # matching the provided criteria. The +votable+ may be specified as its own
157
+ # parameter, or through the keyword arguments +votable_id+, +votable_type+,
158
+ # +votable_gid+, or +votable+ (note that the parameter +votable+ will
159
+ # override the keyword argument +votable+, if both are provided).
160
+ #
161
+ # Additional named arguments may be provided through +kwargs+:
162
+ #
163
+ # scope:: The scope of the vote to be recorded. Defaults to +nil+.
164
+ # vote:: The vote to be queried. If present, is parsed through
165
+ # Ballot::Words.truthy?.
166
+ #
167
+ # Other arguments are ignored.
168
+ #
169
+ # \ActiveRecord:: There are no special notes for ActiveRecord.
170
+ # \Sequel:: GlobalID does not currently provide support for Sequel. The use
171
+ # of +votable_gid+ in this case will probably fail.
172
+ #
173
+ # <em>Also aliased as: #ballot_for?.</em>
174
+
175
+ ##
176
+ # :method: ballot_for?
177
+ # :call-seq:
178
+ # ballot_for?(votable = nil, kwargs = {})
179
+ # ballot_for?(votable)
180
+ # ballot_for?(votable_id: id, votable_type: type)
181
+ # ballot_for?(votable_gid: gid)
182
+ # ballot_for?(votable, scope: scope, vote: false, weight: true)
183
+ #
184
+ # <em>Alias for: #cast_ballot_for?.</em>
185
+
186
+ ##
187
+ # Returns +true+ if this Voter has made positive votes for the provided
188
+ # +votable+. Any value passed to the +vote+ keyword argument will be
189
+ # ignored. See #cast_ballot_for? for more details.
190
+ def cast_up_ballot_for?(votable = nil, kwargs = {})
191
+ cast_ballot_for?(votable, kwargs.merge(vote: true))
192
+ end
193
+ alias up_ballot_for? cast_up_ballot_for?
194
+
195
+ ##
196
+ # Returns +true+ if this Voter has made negative votes for the provided
197
+ # +votable+. Any value passed to the +vote+ keyword argument will be
198
+ # ignored. See #cast_ballot_for? for more details.
199
+ def cast_down_ballot_for?(votable = nil, kwargs = {})
200
+ cast_ballot_for?(votable, kwargs.merge(vote: false))
201
+ end
202
+ alias down_ballot_for? cast_down_ballot_for?
203
+
204
+ ##
205
+ # :method: ballot_as_cast_for
206
+ # :call-seq:
207
+ # ballot_as_cast_for(votable = nil, kwargs = {})
208
+ # ballot_as_cast_for(votable)
209
+ # ballot_as_cast_for(votable_id: id, votable_type: type)
210
+ # ballot_as_cast_for(votable_gid: gid)
211
+ # ballot_as_cast_for(votable, scope: scope, vote: false, weight: true)
212
+ #
213
+ # Returns the Ballot::Vote#vote value of the ballot cast by this Voter
214
+ # against the provided +votable+. The +votable+ may be specified as its own
215
+ # parameter, or through the keyword arguments +votable_id+, +votable_type+,
216
+ # +votable_gid+, or +votable+ (note that the parameter +votable+ will
217
+ # override the keyword argument +votable+, if both are provided).
218
+ #
219
+ # If this Voter has not cast a ballot against the +votable+, returns +nil+.
220
+ #
221
+ # Additional named arguments may be provided through +kwargs+:
222
+ #
223
+ # scope:: The scope of the vote to be query. Defaults to +nil+.
224
+ # vote:: The vote to be queried. If present, is parsed through
225
+ # Ballot::Words.truthy?.
226
+ #
227
+ # Other arguments are ignored.
228
+ #
229
+ # \ActiveRecord:: There are no special notes for ActiveRecord.
230
+ # \Sequel:: GlobalID does not currently provide support for Sequel. The use
231
+ # of +votable_gid+ in this case will probably fail.
232
+
233
+ ##
234
+ # :method: ballots_for_class(model_class, kwargs = {})
235
+ #
236
+ # Find ballots cast by this Voter matching the canonical name of the
237
+ # +model_class+ as the type of Votable.
238
+ #
239
+ # Additional named arguments may be provided through +kwargs+:
240
+ #
241
+ # scope:: The scope of the vote to be recorded. Defaults to +nil+.
242
+ # vote:: The vote to be queried. If present, is parsed through
243
+ # Ballot::Words.truthy?.
244
+ #
245
+ # Other arguments are ignored.
246
+
247
+ ##
248
+ # Find positive ballots cast by this Voter matching the canonical name of
249
+ # the +model_class+ as the type of Votable. Any value passed to the +vote+
250
+ # keyword argument will be ignored. See #ballots_for_class for more
251
+ # details.
252
+ def up_ballots_for_class(model_class, kwargs = {})
253
+ ballots_for_class(model_class, kwargs.merge(vote: true))
254
+ end
255
+
256
+ ##
257
+ # Find negative ballots cast by this Voter matching the canonical name of
258
+ # the +model_class+ as the type of Votable. Any value passed to the +vote+
259
+ # keyword argument will be ignored. See #ballots_for_class for more
260
+ # details.
261
+ def down_ballots_for_class(model_class, kwargs = {})
262
+ ballots_for_class(model_class, kwargs.merge(vote: false))
263
+ end
264
+
265
+ ##
266
+ # Returns the Votable objects that this Voter has voted on. Additional
267
+ # query conditions may be specified in +conds+, or in the +block+ if
268
+ # supported by the ORM. The Voter objects are eager loaded to minimize the
269
+ # number of queries required to satisfy this request.
270
+ #
271
+ # \ActiveRecord:: Polymorphic eager loading is directly supported, using
272
+ # <tt>ballots_for.includes(:votable)</tt>. Normal
273
+ # +where+-clause conditions may be provided in +conds+.
274
+ # \Sequel:: Polymorphic eager loading is not supported by \Sequel, but has
275
+ # been implemented in Ballot for this method. Normal
276
+ # +where+-clause conditions may be provided in +conds+ or in
277
+ # +block+ for \Sequel virtual row support.
278
+ def ballot_votables(*conds, &block)
279
+ __eager_ballot_votables(find_ballots_by(*conds, &block))
280
+ end
281
+
282
+ ##
283
+ # Returns the Votable objects that this Voter has made positive votes on.
284
+ # See #ballot_voters for how +conds+ and +block+ apply.
285
+ def ballot_up_votables(*conds, &block)
286
+ __eager_ballot_votables(
287
+ find_ballots_by(*conds, &block).where(vote: true)
288
+ )
289
+ end
290
+
291
+ ##
292
+ # Returns the Votable objects that this Voter has made negative votes on.
293
+ # See #ballot_voters for how +conds+ and +block+ apply.
294
+ def ballot_down_votables(*conds, &block)
295
+ __eager_ballot_votables(
296
+ find_ballots_by(*conds, &block).where(vote: false)
297
+ )
298
+ end
299
+
300
+ private
301
+
302
+ def __ballot_voter_kwargs(votable, kwargs = {})
303
+ if votable.kind_of?(Hash)
304
+ kwargs.merge(votable)
305
+ elsif votable.nil?
306
+ kwargs
307
+ else
308
+ kwargs.merge(votable: votable)
309
+ end
310
+ end
311
+
312
+ # Methods added to the Voter model class.
313
+ module ClassMethods
314
+ # The class is now a voter record.
315
+ def ballot_voter?
316
+ true
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ ##
6
+ module Ballot
7
+ # Methods to determine whether the value of the vote word in question is
8
+ # positive (#truthy?) or negative (#falsy?).
9
+ module Words
10
+ module_function
11
+
12
+ # The list of 'words' recognized as falsy values for voting purposes. This
13
+ # set can be added to for localization purposes. Any word *not* in this
14
+ # list is considered truthy.
15
+ FALSY = Set.new(
16
+ [
17
+ 'down', 'downvote', 'dislike', 'disliked', 'negative', 'no', 'bad',
18
+ 'false', '0', '-1'
19
+ ]
20
+ )
21
+
22
+ # Returns +true+ if the word supplied is not #falsy?.
23
+ def truthy?(word)
24
+ !falsy?(word)
25
+ end
26
+
27
+ # Returns +true+ if the word supplied is in the FALSY set.
28
+ def falsy?(word)
29
+ FALSY.include?(word.to_s.downcase)
30
+ end
31
+ end
32
+ end