ballot 1.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.
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