has_many_booleans 0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +74 -0
- data/Rakefile +46 -0
- data/install.rb +4 -0
- data/lib/has_many_booleans.rb +335 -0
- data/lib/has_many_booleans/simple_bitset.rb +64 -0
- data/rails/init.rb +1 -0
- data/test/database.yml +6 -0
- data/test/debug.log +710 -0
- data/test/has_many_booleans_plugin.sqlite3 +0 -0
- data/test/has_many_booleans_test.rb +396 -0
- data/test/schema.rb +36 -0
- data/test/simple_bitset_test.rb +22 -0
- data/test/test_helper.rb +43 -0
- data/uninstall.rb +1 -0
- metadata +75 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Jan Lelis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
== ActiveRecord plugin: has_many_booleans
|
2
|
+
<em>has_many_booleans creates virtual boolean attributes for a model.
|
3
|
+
When the object gets saved, the plugin transforms all attributes into a single integer, using a {bitset}[http://en.wikipedia.org/wiki/Bitset]. So you can easily add new attributes without changing the database structure.</em>
|
4
|
+
|
5
|
+
|
6
|
+
=== Setup
|
7
|
+
Install the plugin with
|
8
|
+
* Rails 2: <tt>script/plugin install {http://http://github.com/janlelis/has_many_booleans}[http://github.com/janlelis/has_many_booleans]</tt>
|
9
|
+
* Rails 3: <tt>rails p install {http://http://github.com/janlelis/has_many_booleans}[http://github.com/janlelis/has_many_booleans]</tt>
|
10
|
+
* Or as a gem: <tt>gem install has_many_booleans</tt>
|
11
|
+
|
12
|
+
Add an integer field with the name +booleans+ to your the model's database table.
|
13
|
+
|
14
|
+
=== Example usage
|
15
|
+
|
16
|
+
You simply list names for the desired booleans in the model.rb file...
|
17
|
+
|
18
|
+
class Model < ActiveRecord::Base
|
19
|
+
has_many_booleans :name, :password
|
20
|
+
end
|
21
|
+
|
22
|
+
...to get the following methods:
|
23
|
+
[<tt>name_activated</tt>, <tt>name_activated?</tt>] get the value of the boolean
|
24
|
+
[<tt>name_activated!</tt>] set the value to true
|
25
|
+
[<tt>name_activated=</tt> value] set the value to false or true
|
26
|
+
[<tt>password_activated</tt>, ...] same methods for <tt>:password</tt>
|
27
|
+
|
28
|
+
|
29
|
+
When saving the object, all "virtual" booleans get converted to a single integer that is
|
30
|
+
saved in the database. Vice versa, when loading an model from the database, its boolean integer sets the value of the above methods.
|
31
|
+
|
32
|
+
==== Example 2: basic options
|
33
|
+
|
34
|
+
class Model < ActiveRecord::Base
|
35
|
+
has_many_booleans :name, :password,
|
36
|
+
:true => [:name],
|
37
|
+
:append => 'set',
|
38
|
+
end
|
39
|
+
|
40
|
+
The default values of all booleans is +false+. However, with the <tt>:true</tt> option, you can list those booleans, which should default to +true+.
|
41
|
+
|
42
|
+
The <tt>:append</tt> option lets you modify the suffix to append to the boolean names.
|
43
|
+
|
44
|
+
==== Example 3: advanced options
|
45
|
+
|
46
|
+
class Model < ActiveRecord::Base
|
47
|
+
has_many_booleans :name, :password,
|
48
|
+
:field => 'some_db_field',
|
49
|
+
:lazy => false,
|
50
|
+
:self => 'model_available',
|
51
|
+
:self_value => true,
|
52
|
+
end
|
53
|
+
|
54
|
+
The <tt>:field</tt> option lets you change the database field in which the integer gets stored (default is +booleans+).
|
55
|
+
|
56
|
+
When the <tt>:lazy</tt> option is set to +false+, the bitset integer gets changed every time you assign a new value for a boolean. The default setting is +true+, which means, the integer does not get updated until the object is saved in the database.
|
57
|
+
|
58
|
+
The <tt>:self</tt> option is just another virtual boolean, which's method name you can freely assign.
|
59
|
+
|
60
|
+
=== Scopes
|
61
|
+
The plugin also generates a <tt>.true</tt> and a <tt>.false</tt> scope for the model. You have to pass a boolean name as parameter to filter for this value. If you pass multiple boolean names, they get connected with 'or'. To get an 'and' condition, chain multiple scopes. If you don't pass any boolean names (or +nil+), the special <tt>:self</tt> boolean is meant.
|
62
|
+
|
63
|
+
==== Example queries
|
64
|
+
Model.true(:name) # scopes to all models, where :name is true
|
65
|
+
Model.false # scopes to all models, where the :self boolean is false
|
66
|
+
Model.true(:name, :password) # :name or :password must be true
|
67
|
+
Model.true(:name).true(:password) # :name and :password must be true
|
68
|
+
|
69
|
+
=== Further reading
|
70
|
+
For a more detailed description of the options, see the rdoc for the {has_many_booleans}[link:classes/HasManyBooleans/ClassMethods.html#M000003] method.
|
71
|
+
|
72
|
+
Copyright (c) 2010 Jan Lelis, http://rbjl.net, released under the MIT license
|
73
|
+
available at http://github.com/janlelis/has_many_booleans
|
74
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Test the has_many_booleans plugin.'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.libs << 'test'
|
13
|
+
t.pattern = 'test/**/*_test.rb'
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generate documentation for the has_many_booleans plugin.'
|
18
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
19
|
+
rdoc.rdoc_dir = 'doc'
|
20
|
+
rdoc.title = 'has_many_booleans Rails plugin'
|
21
|
+
rdoc.options << '--inline-source'
|
22
|
+
rdoc.rdoc_files.include('README.rdoc')
|
23
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
24
|
+
end
|
25
|
+
|
26
|
+
# gem
|
27
|
+
PKG_FILES = FileList[ '[a-zA-Z]*', 'lib/**/*', 'rails/**/*', 'test/**/*' ]
|
28
|
+
spec = Gem::Specification.new do |s|
|
29
|
+
s.name = "has_many_booleans"
|
30
|
+
s.version = "0.9"
|
31
|
+
s.author = "Jan Lelis"
|
32
|
+
s.email = "mail@janlelis.de"
|
33
|
+
s.homepage = "http://rbjl.net"
|
34
|
+
s.platform = Gem::Platform::RUBY
|
35
|
+
s.summary = "This Rails plugin/gem allows you to generate virtual boolean attributes, which get saved in the database as a single bitset integer"
|
36
|
+
s.files = PKG_FILES.to_a
|
37
|
+
s.require_path = "lib"
|
38
|
+
s.has_rdoc = true
|
39
|
+
s.extra_rdoc_files = ["README.rdoc"]
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Turn this plugin into a gem.'
|
43
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
44
|
+
pkg.gem_spec = spec
|
45
|
+
end
|
46
|
+
|
data/install.rb
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
# J-_-L
|
2
|
+
|
3
|
+
require 'has_many_booleans/simple_bitset'
|
4
|
+
|
5
|
+
module HasManyBooleans #:nodoc:
|
6
|
+
RAILS2 = if ENV['RAILS_GEM_VERSION']
|
7
|
+
ENV['RAILS_GEM_VERSION'] < '3'
|
8
|
+
else
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
#=== Setup the booleans for a model
|
14
|
+
#The method takes the symbols of the desired booleans as parameters. As last
|
15
|
+
#parameter you can apply an options hash. Each symbol represents an index,
|
16
|
+
#<em>depending on the position</em> in the list, starting with 1.
|
17
|
+
#
|
18
|
+
# class Model < ActiveRecord::Base
|
19
|
+
# has_many_booleans :name, :password,
|
20
|
+
# :true => [ :name ],
|
21
|
+
# :append => 'set',
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
#Another way of setting up the booleans is with an hash. This is useful when
|
25
|
+
#you want to choose the indexes yourself.
|
26
|
+
#
|
27
|
+
# class Model < ActiveRecord::Base
|
28
|
+
# has_many_booleans({:name => 23, :password => 99},
|
29
|
+
# :append => 'set')
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
#==== Available options
|
33
|
+
#
|
34
|
+
#[<tt>:true</tt>] Takes an array of boolean names which shall default to +true+.
|
35
|
+
#[<tt>:append</tt>] The name to append to the listed booleans. The underscore is added automatically. +nil+ is also possible. Default is +activated+.
|
36
|
+
#[<tt>:field</tt>] The database field used. Defaults to +booleans+.
|
37
|
+
#[<tt>:suffixes</tt>] Specifies, which "alias" methods are created. Defaults to <tt>["?", "=", "!"]</tt>. You cannot add new ones, you can only forbid some of them.
|
38
|
+
#[<tt>:false_values</tt>] All the values in the array can be used to set a boolean to +false+ (when used with the <tt>=</tt> method). <b>Example:</b> Set this to <tt>["0"]</tt> and then call <tt>some_boolean_activated = "0"</tt>, it will set the boolean to +false+. Default settings is +false+ (deactivated).
|
39
|
+
#[<tt>:lazy</tt>] When the <tt>:lazy</tt> option is set to +false+, the bitset integer gets changed every time you assign a new value for a boolean. The default setting is +true+, which means, the integer gets only updated when the object is saved.
|
40
|
+
#[<tt>:self</tt>] This is just another virtual boolean. You can freely assign the name. It is always stored as first bit in the bitset integer (so if the bitset integer is odd, this special boolean is set). You can also set this to +true+, which means, the <tt>:append</tt> value is used as method name. Default: +false+.
|
41
|
+
#[<tt>:self_value</tt>] The default value for the special <tt>:self</tt> boolean above.
|
42
|
+
def has_many_booleans(*params)
|
43
|
+
|
44
|
+
# get params
|
45
|
+
if params.last.is_a? Hash # booleans_options
|
46
|
+
parse_booleans_options params.pop
|
47
|
+
|
48
|
+
if params.empty?
|
49
|
+
warn "has_many_booleans: You applied a single hash as parameter, which gets interpreted as option hash. If you wish to use it as boolean hash, give a {} as second parameter!"
|
50
|
+
end
|
51
|
+
else
|
52
|
+
parse_booleans_options Hash.new
|
53
|
+
end
|
54
|
+
|
55
|
+
if params.first.is_a? Hash # alternative usage with a hash instead of array
|
56
|
+
params = params.first
|
57
|
+
iter_method = :each
|
58
|
+
else
|
59
|
+
iter_method = :each_with_index
|
60
|
+
end
|
61
|
+
|
62
|
+
# data structure: { string => [index, boolean_value] }
|
63
|
+
@booleans_default = {}
|
64
|
+
params.send(iter_method){ |key, index|
|
65
|
+
index = index.to_i+1
|
66
|
+
@booleans_default[key.to_s] = [ index,
|
67
|
+
@booleans_options[:true].include?(key.to_sym) ||
|
68
|
+
@booleans_options[:true].include?(key.to_s)
|
69
|
+
] if index > 0
|
70
|
+
}
|
71
|
+
|
72
|
+
# validators
|
73
|
+
@booleans_validators = { true => [], false => [] }
|
74
|
+
|
75
|
+
send :include, InstanceMethods
|
76
|
+
|
77
|
+
# hook in callbacks part 1 (to not overwrite after_initialize)
|
78
|
+
class << self
|
79
|
+
def instantiate_with_booleans(record) #:nodoc:
|
80
|
+
object = instantiate_without_booleans record
|
81
|
+
object.initialize_booleans
|
82
|
+
object
|
83
|
+
end
|
84
|
+
alias_method_chain :instantiate, :booleans
|
85
|
+
end
|
86
|
+
|
87
|
+
before_save :save_booleans
|
88
|
+
|
89
|
+
# register scopes
|
90
|
+
booleans_scope = lambda{ |true_or_false, *args|
|
91
|
+
indexes = if args.blank?
|
92
|
+
[1] # special self boolean
|
93
|
+
else
|
94
|
+
args.map{ |name|
|
95
|
+
if name == nil # allow self in "or" connection
|
96
|
+
1
|
97
|
+
else
|
98
|
+
name = name.to_s
|
99
|
+
if !@booleans_default[name]
|
100
|
+
warn 'has_many_booleans: You are using unknown boolean names in your scope!'
|
101
|
+
else
|
102
|
+
2 ** @booleans_default[name][0]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
}.compact
|
106
|
+
end
|
107
|
+
cond = ["#{@booleans_options[:field]} & ?#{' < 1' if !true_or_false}"]*indexes.size*' or '
|
108
|
+
if RAILS2
|
109
|
+
{:conditions => [cond, *indexes]}
|
110
|
+
else
|
111
|
+
where cond, *indexes
|
112
|
+
end
|
113
|
+
}
|
114
|
+
|
115
|
+
scope_name = RAILS2 ? :named_scope : :scope
|
116
|
+
|
117
|
+
send scope_name, :true, lambda { |*args|
|
118
|
+
booleans_scope[true, *args]
|
119
|
+
}
|
120
|
+
|
121
|
+
send scope_name, :false, lambda { |*args|
|
122
|
+
booleans_scope[false, *args]
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
alias :hmb :has_many_booleans
|
127
|
+
|
128
|
+
# getters
|
129
|
+
def booleans_options #:nodoc:
|
130
|
+
@booleans_options
|
131
|
+
end
|
132
|
+
|
133
|
+
def booleans_default #:nodoc:
|
134
|
+
@booleans_default
|
135
|
+
end
|
136
|
+
|
137
|
+
def booleans_validators #:nodoc:
|
138
|
+
@booleans_validators
|
139
|
+
end
|
140
|
+
|
141
|
+
# List all booleans that are required to be false!
|
142
|
+
# validates_false :description, :password
|
143
|
+
def validates_false(*bools)
|
144
|
+
booleans_validators[false] = bools
|
145
|
+
validate :validator_false
|
146
|
+
end
|
147
|
+
|
148
|
+
# List all booleans that are required to be true!
|
149
|
+
# validates_false :description, :password
|
150
|
+
def validates_true(*bools)
|
151
|
+
booleans_validators[true] = bools
|
152
|
+
validate :validator_true
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def parse_booleans_options(new_booleans_options)
|
158
|
+
# std booleans_options
|
159
|
+
@booleans_options = {
|
160
|
+
:append => '_activated',
|
161
|
+
:field => 'booleans',
|
162
|
+
:suffixes => %w| ! = ? |,
|
163
|
+
:true => [],
|
164
|
+
:false_values => [], # e.g. [0, "0", ""],
|
165
|
+
:self => false,
|
166
|
+
:self_value => false,
|
167
|
+
:lazy => true,
|
168
|
+
}
|
169
|
+
|
170
|
+
# parse params
|
171
|
+
new_booleans_options.each{|opt_name, opt_value|
|
172
|
+
if @booleans_options.has_key? opt_name
|
173
|
+
|
174
|
+
@booleans_options[opt_name] = case opt_name
|
175
|
+
|
176
|
+
when :append # prepend underscore
|
177
|
+
opt_value ? "_#{opt_value}" : ''
|
178
|
+
|
179
|
+
when :suffixes # only allow ! = ?
|
180
|
+
(opt_value||[]) & %w| ? ! = |
|
181
|
+
|
182
|
+
when :false_values
|
183
|
+
opt_value || []
|
184
|
+
|
185
|
+
when :self_value, :lazy # normalize
|
186
|
+
!!opt_value
|
187
|
+
else
|
188
|
+
opt_value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
}
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
module InstanceMethods #:nodoc: callbacks
|
196
|
+
|
197
|
+
# Load boolean integer from database and define the methods.
|
198
|
+
def initialize_booleans
|
199
|
+
booleans_field = self.class.booleans_options[:field]
|
200
|
+
booleans_activated = self[booleans_field] ? self[booleans_field].to_bra : []
|
201
|
+
|
202
|
+
@booleans_data = { nil => [0, false] } # self@nil
|
203
|
+
|
204
|
+
self.class.booleans_default.each{ |name, (index, value)|
|
205
|
+
name = name.to_s
|
206
|
+
|
207
|
+
each_suffix{ |suffix|
|
208
|
+
# get values
|
209
|
+
init_value = if self.new_record?
|
210
|
+
self.class.booleans_options[:true].member?(name.to_sym)
|
211
|
+
else
|
212
|
+
booleans_activated.member?(index)
|
213
|
+
end
|
214
|
+
@booleans_data[name] = [index, init_value]
|
215
|
+
|
216
|
+
# define the methods
|
217
|
+
method_name = name + self.class.booleans_options[:append] + suffix
|
218
|
+
define_boolean_method method_name, suffix, name
|
219
|
+
|
220
|
+
# define the self method
|
221
|
+
if self.class.booleans_options[:self]
|
222
|
+
# get value
|
223
|
+
if self.new_record?
|
224
|
+
cond = self.class.booleans_options[:self_value]
|
225
|
+
else
|
226
|
+
cond = booleans_activated.member? 0
|
227
|
+
end
|
228
|
+
@booleans_data[nil][1] = cond ? true : false
|
229
|
+
|
230
|
+
# get self method name
|
231
|
+
self_method = if self.class.booleans_options[:self] === true
|
232
|
+
if !self.class.booleans_options[:append] || self.class.booleans_options[:append].empty?
|
233
|
+
raise "has_many_booleans: self method activated with true, but :append is nil! Please use :self => 'method_name'"
|
234
|
+
else
|
235
|
+
self.class.booleans_options[:append][1..-1]
|
236
|
+
end
|
237
|
+
else
|
238
|
+
self.class.booleans_options[:self]
|
239
|
+
end
|
240
|
+
|
241
|
+
each_suffix{ |suffix|
|
242
|
+
define_boolean_method self_method + suffix, suffix
|
243
|
+
}
|
244
|
+
end
|
245
|
+
}
|
246
|
+
}
|
247
|
+
end
|
248
|
+
|
249
|
+
# Transform booleans to integer and save in the database.
|
250
|
+
def save_booleans
|
251
|
+
act = []
|
252
|
+
@booleans_data.each{ |_, (index, value)|
|
253
|
+
act << index if value
|
254
|
+
}
|
255
|
+
self[self.class.booleans_options[:field]] = act.to_bri
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def define_boolean_method(method_name, suffix, name=nil)
|
261
|
+
if self.respond_to?(method_name)
|
262
|
+
#warn "has_many_booleans: Could not define #{method_name} for #{self.class} (method already exists)"
|
263
|
+
else
|
264
|
+
self.class.send(:define_method, method_name) do |*new_value|
|
265
|
+
case suffix
|
266
|
+
when '', '?'
|
267
|
+
# nothing's changed
|
268
|
+
when '!'
|
269
|
+
@booleans_data[name][1] = true
|
270
|
+
save_booleans if !self.class.booleans_options[:lazy]
|
271
|
+
when '='
|
272
|
+
@booleans_data[name][1] =
|
273
|
+
!!( new_value[0] && !self.class.booleans_options[:false_values].member?(new_value[0]) )
|
274
|
+
save_booleans if !self.class.booleans_options[:lazy]
|
275
|
+
end
|
276
|
+
@booleans_data[name][1]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def each_suffix(&block)
|
282
|
+
( [''] + self.class.booleans_options[:suffixes] ).each{ |suffix|
|
283
|
+
yield suffix
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
|
289
|
+
# hook in callbacks part 2 (to not overwrite after_initialize)
|
290
|
+
#TODO run callback after booleans have been fetched
|
291
|
+
def initialize(*args, &block) # :nodoc:
|
292
|
+
super *args, &block
|
293
|
+
initialize_booleans
|
294
|
+
end
|
295
|
+
|
296
|
+
if respond_to? :initialize_copy
|
297
|
+
def initialize_copy(record)
|
298
|
+
object = super record
|
299
|
+
initialize_booleans
|
300
|
+
object
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
|
305
|
+
# validators
|
306
|
+
def validator_true
|
307
|
+
self.class.booleans_validators[true].each { |bool|
|
308
|
+
bd = @booleans_data[bool.to_s]
|
309
|
+
errors.add("Boolean #{bool}", "must be true") if bd && !(bd[1])
|
310
|
+
}
|
311
|
+
end
|
312
|
+
|
313
|
+
def validator_false
|
314
|
+
self.class.booleans_validators[false].each { |bool|
|
315
|
+
bd = @booleans_data[bool.to_s]
|
316
|
+
errors.add("Boolean #{bool}", "must be false") if bd && bd[1]
|
317
|
+
}
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
# activate class methods
|
323
|
+
def self.included(base) #:nodoc:
|
324
|
+
base.class_eval do
|
325
|
+
extend ClassMethods
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# activate instance methods
|
331
|
+
ActiveRecord::Base.send :include, HasManyBooleans
|
332
|
+
|
333
|
+
# Copyright (c) 2010 Jan Lelis http://rbjl.net, released under the MIT license
|
334
|
+
# available at http://github.com/janlelis/has_many_booleans
|
335
|
+
|