bitmask_attributes 0.2.4 → 0.3.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.
- data/.gitignore +2 -1
- data/.travis.yml +4 -0
- data/CHANGELOG.rdoc +59 -6
- data/Gemfile +2 -6
- data/Gemfile.lock +17 -17
- data/README.rdoc +62 -2
- data/lib/bitmask_attributes.rb +1 -1
- data/lib/bitmask_attributes/definition.rb +40 -26
- data/lib/bitmask_attributes/version.rb +1 -1
- data/test/bitmask_attributes_test.rb +231 -175
- data/test/support/models.rb +32 -8
- metadata +11 -7
- data/test/support/helpers.rb +0 -17
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/CHANGELOG.rdoc
CHANGED
@@ -1,14 +1,67 @@
|
|
1
|
+
=== Version 0.3.0 - 2012-04-30 21:36:18 +0100
|
2
|
+
|
3
|
+
Aaron Hamid (1):
|
4
|
+
* wrap two scopes in procs to fix migration/startup issues
|
5
|
+
|
6
|
+
Andre Duffeck (1):
|
7
|
+
* fix values_for
|
8
|
+
|
9
|
+
Ivan Buiko (1):
|
10
|
+
* Adding retrieval by exact value
|
11
|
+
|
12
|
+
Joel Moss (9):
|
13
|
+
* Merge pull request #6 from incandescent/fix-scope-proc
|
14
|
+
* Merge pull request #4 from SUSE/fix_values_for
|
15
|
+
* Bundle update
|
16
|
+
* Adding retrieval by exact value
|
17
|
+
* Merge branch 'retrieval-by-exact-value'
|
18
|
+
* Resolved merge conflict
|
19
|
+
* Merge pull request #9 from numerex/master
|
20
|
+
* Merge pull request #13 from spemmons/master
|
21
|
+
* Merge pull request #16 from spemmons/master
|
22
|
+
|
23
|
+
Sebastian Borrazas (1):
|
24
|
+
* Not raising exception when the bitmask column is not found, since its adding it on a migration
|
25
|
+
|
26
|
+
spemmons (22):
|
27
|
+
* detect whether or not nulls are possible with an attribute and remove OR conditions checking if it is null or not
|
28
|
+
* ignore RubyMine info
|
29
|
+
* merge changes with joel moss for scopes
|
30
|
+
* check for model not being ready when creating scopes
|
31
|
+
* support nulls with an explicit attribute, not by inspecting the model due to preloading issues
|
32
|
+
* add documentation for the :null option
|
33
|
+
* support multiple values in the "without_..." scope that excludes match for of any of the bits; previously 2nd and following values were ignored
|
34
|
+
* update documentation to reflect support for multiple arguments to "without_..."
|
35
|
+
* update documentation to reflect support for multiple arguments to "without_..."
|
36
|
+
* update documentation to reflect support for multiple arguments to "without_..."
|
37
|
+
* added tests to verify the validity of zykadelic's complaint that "with_" scopes were improperly including "OR IS NOT NULL" and fixed the code by removing the condition in this case
|
38
|
+
* DRY-up scope expressions, standardizing on "no_" and "bitmask_for_...(*values)" instead of repeating more complex calculations; also found another example of zykadelic's complaint with "with_any_" that is fixed and has tests
|
39
|
+
* add travis configuration file
|
40
|
+
* follow buffpojken in forcing string-interpolated model to work in global scope
|
41
|
+
* Merge branch 'master' of github.com:spemmons/bitmask_attributes
|
42
|
+
* remove out-dated helpers that, although no longer used, were being loaded by Travis-CI and failing
|
43
|
+
* clarify travis config to fix failing CI build that works locally
|
44
|
+
* apparently 1.9.2 doesn't work... will figure out why later
|
45
|
+
* add :zero option and tests
|
46
|
+
* check for a string match instead of a symbol to allow values arriving from web forms and services to match properly
|
47
|
+
* added documentation for the :zero attribute
|
48
|
+
* rename :zero to :zero_value for clarity
|
49
|
+
|
50
|
+
steve emmons (3):
|
51
|
+
* Merge pull request #1 from numerex/master
|
52
|
+
* Merge pull request #2 from numerex/master
|
53
|
+
* Merge pull request #15 from spemmons/master
|
54
|
+
|
55
|
+
|
1
56
|
=== Version 0.2.4 - 2011-11-23 09:37:50 +0000
|
2
57
|
|
3
58
|
Ivan Buiko (3):
|
4
|
-
*
|
5
|
-
* latest versions in Gemfile.lock
|
6
|
-
* don't validate attribute during creating table
|
59
|
+
* No longer validates attribute during migrations
|
7
60
|
|
8
61
|
Joel Moss (3):
|
9
|
-
*
|
10
|
-
*
|
11
|
-
|
62
|
+
* Added changeling tasks for help with gem releases
|
63
|
+
* Created docs using sdoc
|
64
|
+
|
12
65
|
|
13
66
|
|
14
67
|
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,40 +1,40 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
bitmask_attributes (0.2.
|
4
|
+
bitmask_attributes (0.2.4)
|
5
5
|
activerecord (~> 3.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: http://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activemodel (3.
|
11
|
-
activesupport (= 3.
|
10
|
+
activemodel (3.2.2)
|
11
|
+
activesupport (= 3.2.2)
|
12
12
|
builder (~> 3.0.0)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
arel (~> 2.2.1)
|
13
|
+
activerecord (3.2.2)
|
14
|
+
activemodel (= 3.2.2)
|
15
|
+
activesupport (= 3.2.2)
|
16
|
+
arel (~> 3.0.2)
|
18
17
|
tzinfo (~> 0.3.29)
|
19
|
-
activesupport (3.
|
18
|
+
activesupport (3.2.2)
|
19
|
+
i18n (~> 0.6)
|
20
20
|
multi_json (~> 1.0)
|
21
|
-
ansi (1.4.
|
22
|
-
arel (
|
21
|
+
ansi (1.4.2)
|
22
|
+
arel (3.0.2)
|
23
23
|
builder (3.0.0)
|
24
24
|
i18n (0.6.0)
|
25
|
-
json (1.6.
|
26
|
-
multi_json (1.0
|
25
|
+
json (1.6.5)
|
26
|
+
multi_json (1.1.0)
|
27
27
|
rake (0.9.2.2)
|
28
|
-
rdoc (3.
|
28
|
+
rdoc (3.12)
|
29
29
|
json (~> 1.4)
|
30
30
|
sdoc (0.3.16)
|
31
31
|
json (>= 1.1.3)
|
32
32
|
rdoc (~> 3.10)
|
33
33
|
shoulda (2.11.3)
|
34
|
-
sqlite3 (1.3.
|
34
|
+
sqlite3 (1.3.5)
|
35
35
|
turn (0.8.3)
|
36
36
|
ansi
|
37
|
-
tzinfo (0.3.
|
37
|
+
tzinfo (0.3.32)
|
38
38
|
|
39
39
|
PLATFORMS
|
40
40
|
ruby
|
@@ -44,5 +44,5 @@ DEPENDENCIES
|
|
44
44
|
rake
|
45
45
|
sdoc
|
46
46
|
shoulda
|
47
|
-
sqlite3 (>= 1.3.
|
47
|
+
sqlite3 (>= 1.3.5)
|
48
48
|
turn
|
data/README.rdoc
CHANGED
@@ -71,6 +71,10 @@ A couple useful named scopes are also generated when you use
|
|
71
71
|
# => (all users who are BOTH editors and writers)
|
72
72
|
User.with_any_roles(:editor, :writer)
|
73
73
|
# => (all users who are editors OR writers)
|
74
|
+
User.with_exact_roles(:writer)
|
75
|
+
# => (all users who are ONLY writers)
|
76
|
+
User.with_exact_roles(:writer, :editor)
|
77
|
+
# => (all users who are BOTH editors and writers and nothing else)
|
74
78
|
|
75
79
|
Find records without any bitmask set:
|
76
80
|
|
@@ -79,12 +83,16 @@ Find records without any bitmask set:
|
|
79
83
|
User.no_roles
|
80
84
|
# => (all users without a role)
|
81
85
|
|
82
|
-
Find records without
|
86
|
+
Find records without specific attributes:
|
83
87
|
|
84
88
|
User.without_roles(:editor)
|
85
89
|
# => (all users who are not editors)
|
86
90
|
|
87
|
-
|
91
|
+
User.without_roles(:writer, :editor)
|
92
|
+
# => (all users who are NEITHER writers nor editors)
|
93
|
+
|
94
|
+
Note that "without_" supports one or more attribute arguments, and the "no_" method does not support arguments.
|
95
|
+
And "with_exact_" without arguments is alias for "no_"
|
88
96
|
|
89
97
|
|
90
98
|
=== Adding Methods
|
@@ -102,6 +110,58 @@ named scopes):
|
|
102
110
|
user.other_attribute.worked?
|
103
111
|
# => true
|
104
112
|
|
113
|
+
=== Handling null values
|
114
|
+
|
115
|
+
By default, bitmasks support the potential for the underlying integer value to be null. However, if you have created
|
116
|
+
a field that is guaranteed never to be null, you can simplify the SQL query conditions by declaring ":null => false"
|
117
|
+
in the definition:
|
118
|
+
|
119
|
+
bitmask :never_null_attributes,:as => [:value1, :value2], :null => false
|
120
|
+
|
121
|
+
=== Allowing for a "zero" value
|
122
|
+
|
123
|
+
It is common to use web forms to set bitmask bits using checkboxes. If the various bits each are represented by a
|
124
|
+
checkbox and the user unchecks them all, the resulting "params" posted to the controller will be missing. When this
|
125
|
+
happens, a controller will need to ensure that a "params" hash entry has an empty array or a call to "update_attributes"
|
126
|
+
will not change the attribute. For example:
|
127
|
+
|
128
|
+
In model...
|
129
|
+
class SomeModel < ActiveRecord::Base
|
130
|
+
bitmask :some_attribute, :as => [:value1, :value2]
|
131
|
+
end
|
132
|
+
|
133
|
+
In view...
|
134
|
+
<input type="checkbox" name="some_model[some_attribute][]" value="value1"/>
|
135
|
+
<input type="checkbox" name="some_model[some_attribute][]" value="value2"/>
|
136
|
+
|
137
|
+
In controller...
|
138
|
+
def update
|
139
|
+
@some_model = SomeModel.find(params[:id])
|
140
|
+
params[:some_attribute] ||= []
|
141
|
+
@some_model.update_attributes(params)
|
142
|
+
end
|
143
|
+
|
144
|
+
As an alternative, you may provide a special symbol representing "zero":
|
145
|
+
|
146
|
+
In model...
|
147
|
+
class SomeModel < ActiveRecord::Base
|
148
|
+
bitmask :some_attribute, :as => [:value1, :value2], :zero_value => :none
|
149
|
+
end
|
150
|
+
|
151
|
+
In view...
|
152
|
+
<input type="checkbox" name="some_model[some_attribute][]" value="value1"/>
|
153
|
+
<input type="checkbox" name="some_model[some_attribute][]" value="value2"/>
|
154
|
+
<input type="hidden" name="some_model[some_attribute][]" value="none"/>
|
155
|
+
|
156
|
+
In controller...
|
157
|
+
def update
|
158
|
+
@some_model = SomeModel.find(params[:id])
|
159
|
+
@some_model.update_attributes(params)
|
160
|
+
end
|
161
|
+
|
162
|
+
This technique can be particularly useful for both forms and web services where setting the attribute in question may
|
163
|
+
be optionally included or not such that the controller setting of an empty array in the first example would not be
|
164
|
+
correct.
|
105
165
|
|
106
166
|
=== Warning: Modifying possible values
|
107
167
|
|
data/lib/bitmask_attributes.rb
CHANGED
@@ -9,7 +9,7 @@ module BitmaskAttributes
|
|
9
9
|
unless options[:as] && options[:as].kind_of?(Array)
|
10
10
|
raise ArgumentError, "Must provide an Array :as option"
|
11
11
|
end
|
12
|
-
bitmask_definitions[attribute] = Definition.new(attribute, options[:as].to_a,
|
12
|
+
bitmask_definitions[attribute] = Definition.new(attribute, options[:as].to_a,options[:null].nil? || options[:null],options[:zero_value],&extension)
|
13
13
|
bitmask_definitions[attribute].install_on(self)
|
14
14
|
end
|
15
15
|
|
@@ -1,11 +1,13 @@
|
|
1
1
|
module BitmaskAttributes
|
2
2
|
class Definition
|
3
|
-
attr_reader :attribute, :values, :extension
|
3
|
+
attr_reader :attribute, :values, :allow_null, :zero_value, :extension
|
4
4
|
|
5
|
-
def initialize(attribute, values=[], &extension)
|
5
|
+
def initialize(attribute, values=[],allow_null = true,zero_value = nil, &extension)
|
6
6
|
@attribute = attribute
|
7
7
|
@values = values
|
8
8
|
@extension = extension
|
9
|
+
@allow_null = allow_null
|
10
|
+
@zero_value = zero_value
|
9
11
|
end
|
10
12
|
|
11
13
|
def install_on(model)
|
@@ -17,7 +19,7 @@ module BitmaskAttributes
|
|
17
19
|
create_scopes_on model
|
18
20
|
create_attribute_methods_on model
|
19
21
|
end
|
20
|
-
|
22
|
+
|
21
23
|
private
|
22
24
|
|
23
25
|
def validate_for(model)
|
@@ -25,9 +27,9 @@ module BitmaskAttributes
|
|
25
27
|
# database (the migration has not been run) or table doesn't exist. This usually
|
26
28
|
# occurs in the 'test' and 'production' environment or during migration.
|
27
29
|
return if defined?(Rails) && Rails.configuration.cache_classes || !model.table_exists?
|
28
|
-
|
30
|
+
|
29
31
|
unless model.columns.detect { |col| col.name == attribute.to_s }
|
30
|
-
|
32
|
+
Rails.logger.warn "WARNING: `#{attribute}' is not an attribute of `#{model}'. But, it's ok if it happens during migrations and your \"bitmasked\" attribute is still not created."
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -56,7 +58,7 @@ module BitmaskAttributes
|
|
56
58
|
model.class_eval %(
|
57
59
|
def #{attribute}=(raw_value)
|
58
60
|
values = raw_value.kind_of?(Array) ? raw_value : [raw_value]
|
59
|
-
self.#{attribute}.replace(values.reject(
|
61
|
+
self.#{attribute}.replace(values.reject{|value| #{eval_string_for_zero('value')}})
|
60
62
|
end
|
61
63
|
)
|
62
64
|
end
|
@@ -74,7 +76,9 @@ module BitmaskAttributes
|
|
74
76
|
model.class_eval %(
|
75
77
|
def self.bitmask_for_#{attribute}(*values)
|
76
78
|
values.inject(0) do |bitmask, value|
|
77
|
-
|
79
|
+
if #{eval_string_for_zero('value')}
|
80
|
+
bit = 0
|
81
|
+
elsif (bit = bitmasks[:#{attribute}][value]).nil?
|
78
82
|
raise ArgumentError, "Unsupported value for #{attribute}: \#{value.inspect}"
|
79
83
|
end
|
80
84
|
bitmask | bit
|
@@ -105,50 +109,60 @@ module BitmaskAttributes
|
|
105
109
|
end
|
106
110
|
|
107
111
|
def create_scopes_on(model)
|
112
|
+
or_is_null_condition = " OR #{attribute} IS NULL" if allow_null
|
113
|
+
|
108
114
|
model.class_eval %(
|
109
115
|
scope :with_#{attribute},
|
110
116
|
proc { |*values|
|
111
117
|
if values.blank?
|
112
|
-
where('#{attribute} > 0
|
118
|
+
where('#{attribute} > 0')
|
113
119
|
else
|
114
120
|
sets = values.map do |value|
|
115
|
-
mask =
|
121
|
+
mask = ::#{model}.bitmask_for_#{attribute}(value)
|
116
122
|
"#{attribute} & \#{mask} <> 0"
|
117
123
|
end
|
118
124
|
where(sets.join(' AND '))
|
119
125
|
end
|
120
126
|
}
|
121
127
|
scope :without_#{attribute},
|
122
|
-
proc { |
|
123
|
-
if
|
124
|
-
|
125
|
-
where("#{attribute} IS NULL OR #{attribute} & ? = 0", mask)
|
128
|
+
proc { |*values|
|
129
|
+
if values.blank?
|
130
|
+
no_#{attribute}
|
126
131
|
else
|
127
|
-
where("#{attribute}
|
128
|
-
end
|
129
|
-
|
130
|
-
|
131
|
-
scope :
|
132
|
+
where("#{attribute} & ? = 0#{or_is_null_condition}", ::#{model}.bitmask_for_#{attribute}(*values))
|
133
|
+
end
|
134
|
+
}
|
135
|
+
|
136
|
+
scope :with_exact_#{attribute},
|
137
|
+
proc { | *values|
|
138
|
+
if values.blank?
|
139
|
+
no_#{attribute}
|
140
|
+
else
|
141
|
+
where("#{attribute} = ?", ::#{model}.bitmask_for_#{attribute}(*values))
|
142
|
+
end
|
143
|
+
}
|
132
144
|
|
145
|
+
scope :no_#{attribute}, proc { where("#{attribute} = 0#{or_is_null_condition}") }
|
146
|
+
|
133
147
|
scope :with_any_#{attribute},
|
134
148
|
proc { |*values|
|
135
149
|
if values.blank?
|
136
|
-
where('#{attribute} > 0
|
150
|
+
where('#{attribute} > 0')
|
137
151
|
else
|
138
|
-
|
139
|
-
mask = #{model}.bitmask_for_#{attribute}(value)
|
140
|
-
"#{attribute} & \#{mask} <> 0"
|
141
|
-
end
|
142
|
-
where(sets.join(' OR '))
|
152
|
+
where("#{attribute} & ? <> 0", ::#{model}.bitmask_for_#{attribute}(*values))
|
143
153
|
end
|
144
154
|
}
|
145
155
|
)
|
146
156
|
values.each do |value|
|
147
157
|
model.class_eval %(
|
148
158
|
scope :#{attribute}_for_#{value},
|
149
|
-
where('#{attribute} & ? <> 0',
|
159
|
+
proc { where('#{attribute} & ? <> 0', ::#{model}.bitmask_for_#{attribute}(:#{value})) }
|
150
160
|
)
|
151
161
|
end
|
152
162
|
end
|
163
|
+
|
164
|
+
def eval_string_for_zero(value_string)
|
165
|
+
zero_value ? "#{value_string}.blank? || #{value_string}.to_s == '#{zero_value}'" : "#{value_string}.blank?"
|
166
|
+
end
|
153
167
|
end
|
154
|
-
end
|
168
|
+
end
|
@@ -1,219 +1,275 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class BitmaskAttributesTest < ActiveSupport::TestCase
|
4
|
-
|
5
|
-
context "Campaign" do
|
6
|
-
teardown do
|
7
|
-
Company.destroy_all
|
8
|
-
Campaign.destroy_all
|
9
|
-
end
|
10
|
-
|
11
|
-
should "return all defined values of a given bitmask attribute" do
|
12
|
-
assert_equal Campaign.values_for_medium, [:web, :print, :email, :phone]
|
13
|
-
end
|
14
|
-
|
15
|
-
should "can assign single value to bitmask" do
|
16
|
-
assert_stored Campaign.new(:medium => :web), :web
|
17
|
-
end
|
18
4
|
|
19
|
-
|
20
|
-
|
21
|
-
|
5
|
+
def self.context_with_classes(label,campaign_class,company_class)
|
6
|
+
context label do
|
7
|
+
setup do
|
8
|
+
@campaign_class = campaign_class
|
9
|
+
@company_class = company_class
|
10
|
+
end
|
22
11
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
assert_stored campaign, :web, :print, :phone
|
28
|
-
end
|
12
|
+
teardown do
|
13
|
+
@company_class.destroy_all
|
14
|
+
@campaign_class.destroy_all
|
15
|
+
end
|
29
16
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
campaign.medium << :phone
|
34
|
-
assert_stored campaign, :web, :print, :phone
|
35
|
-
campaign.medium << :phone
|
36
|
-
assert_stored campaign, :web, :print, :phone
|
37
|
-
campaign.medium << "phone"
|
38
|
-
assert_stored campaign, :web, :print, :phone
|
39
|
-
assert_equal 1, campaign.medium.select { |value| value == :phone }.size
|
40
|
-
assert_equal 0, campaign.medium.select { |value| value == "phone" }.size
|
41
|
-
end
|
17
|
+
should "return all defined values of a given bitmask attribute" do
|
18
|
+
assert_equal @campaign_class.values_for_medium, [:web, :print, :email, :phone]
|
19
|
+
end
|
42
20
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
campaign.medium = [:phone, :email]
|
47
|
-
assert_stored campaign, :phone, :email
|
48
|
-
end
|
21
|
+
should "can assign single value to bitmask" do
|
22
|
+
assert_stored @campaign_class.new(:medium => :web), :web
|
23
|
+
end
|
49
24
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
assert campaign.save
|
54
|
-
assert_stored Campaign.find(campaign.id), :web, :print
|
55
|
-
end
|
25
|
+
should "can assign multiple values to bitmask" do
|
26
|
+
assert_stored @campaign_class.new(:medium => [:web, :print]), :web, :print
|
27
|
+
end
|
56
28
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
campaign.medium
|
29
|
+
should "can add single value to bitmask" do
|
30
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
31
|
+
assert_stored campaign, :web, :print
|
32
|
+
campaign.medium << :phone
|
33
|
+
assert_stored campaign, :web, :print, :phone
|
61
34
|
end
|
62
|
-
|
63
|
-
|
35
|
+
|
36
|
+
should "ignores duplicate values added to bitmask" do
|
37
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
38
|
+
assert_stored campaign, :web, :print
|
39
|
+
campaign.medium << :phone
|
40
|
+
assert_stored campaign, :web, :print, :phone
|
41
|
+
campaign.medium << :phone
|
42
|
+
assert_stored campaign, :web, :print, :phone
|
43
|
+
campaign.medium << "phone"
|
44
|
+
assert_stored campaign, :web, :print, :phone
|
45
|
+
assert_equal 1, campaign.medium.select { |value| value == :phone }.size
|
46
|
+
assert_equal 0, campaign.medium.select { |value| value == "phone" }.size
|
64
47
|
end
|
65
|
-
assert campaign.misc.worked?
|
66
|
-
end
|
67
48
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
49
|
+
should "can assign new values at once to bitmask" do
|
50
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
51
|
+
assert_stored campaign, :web, :print
|
52
|
+
campaign.medium = [:phone, :email]
|
53
|
+
assert_stored campaign, :phone, :email
|
54
|
+
end
|
74
55
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
end
|
82
|
-
|
83
|
-
should "assert use of unknown value in convenience method will result in exception" do
|
84
|
-
assert_unsupported { Campaign.bitmask_for_medium(:web, :and_this_isnt_valid) }
|
85
|
-
end
|
56
|
+
should "can save bitmask to db and retrieve values transparently" do
|
57
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
58
|
+
assert_stored campaign, :web, :print
|
59
|
+
assert campaign.save
|
60
|
+
assert_stored @campaign_class.find(campaign.id), :web, :print
|
61
|
+
end
|
86
62
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
63
|
+
should "can add custom behavor to value proxies during bitmask definition" do
|
64
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
65
|
+
assert_raises NoMethodError do
|
66
|
+
campaign.medium.worked?
|
67
|
+
end
|
68
|
+
assert_nothing_raised do
|
69
|
+
campaign.misc.worked?
|
70
|
+
end
|
71
|
+
assert campaign.misc.worked?
|
91
72
|
end
|
92
|
-
assert_equal Campaign.bitmask_for_medium(:web, :print), string_bit
|
93
|
-
end
|
94
73
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
74
|
+
should "cannot use unsupported values" do
|
75
|
+
assert_unsupported { @campaign_class.new(:medium => [:web, :print, :this_will_fail]) }
|
76
|
+
campaign = @campaign_class.new(:medium => :web)
|
77
|
+
assert_unsupported { campaign.medium << :this_will_fail_also }
|
78
|
+
assert_unsupported { campaign.medium = [:so_will_this] }
|
79
|
+
end
|
100
80
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
81
|
+
should "can determine bitmasks using convenience method" do
|
82
|
+
assert @campaign_class.bitmask_for_medium(:web, :print)
|
83
|
+
assert_equal(
|
84
|
+
@campaign_class.bitmasks[:medium][:web] | @campaign_class.bitmasks[:medium][:print],
|
85
|
+
@campaign_class.bitmask_for_medium(:web, :print)
|
86
|
+
)
|
87
|
+
end
|
108
88
|
|
109
|
-
|
110
|
-
|
111
|
-
assert @campaign.medium_for_web?
|
112
|
-
assert @campaign.medium_for_print?
|
113
|
-
assert !@campaign.medium_for_email?
|
114
|
-
end
|
115
|
-
|
116
|
-
should "be supported by the simple predicate method" do
|
117
|
-
assert @campaign.medium?(:web)
|
118
|
-
assert @campaign.medium?(:print)
|
119
|
-
assert !@campaign.medium?(:email)
|
120
|
-
end
|
89
|
+
should "assert use of unknown value in convenience method will result in exception" do
|
90
|
+
assert_unsupported { @campaign_class.bitmask_for_medium(:web, :and_this_isnt_valid) }
|
121
91
|
end
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
assert
|
92
|
+
|
93
|
+
should "hash of values is with indifferent access" do
|
94
|
+
string_bit = nil
|
95
|
+
assert_nothing_raised do
|
96
|
+
assert (string_bit = @campaign_class.bitmask_for_medium('web', 'print'))
|
127
97
|
end
|
98
|
+
assert_equal @campaign_class.bitmask_for_medium(:web, :print), string_bit
|
128
99
|
end
|
129
|
-
end
|
130
100
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@campaign2 = @company.campaigns.create
|
136
|
-
@campaign3 = @company.campaigns.create :medium => [:web, :email]
|
101
|
+
should "save bitmask with non-standard attribute names" do
|
102
|
+
campaign = @campaign_class.new(:Legacy => [:upper, :case])
|
103
|
+
assert campaign.save
|
104
|
+
assert_equal [:upper, :case], @campaign_class.find(campaign.id).Legacy
|
137
105
|
end
|
138
106
|
|
139
|
-
should "
|
140
|
-
assert_equal
|
107
|
+
should "ignore blanks fed as values" do
|
108
|
+
assert_equal 0b11,@campaign_class.bitmask_for_medium(:web, :print, '')
|
109
|
+
campaign = @campaign_class.new(:medium => [:web, :print, ''])
|
110
|
+
assert_stored campaign, :web, :print
|
141
111
|
end
|
142
112
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
113
|
+
context "checking" do
|
114
|
+
setup { @campaign = @campaign_class.new(:medium => [:web, :print]) }
|
115
|
+
|
116
|
+
context "for a single value" do
|
117
|
+
should "be supported by an attribute_for_value convenience method" do
|
118
|
+
assert @campaign.medium_for_web?
|
119
|
+
assert @campaign.medium_for_print?
|
120
|
+
assert !@campaign.medium_for_email?
|
121
|
+
end
|
122
|
+
|
123
|
+
should "be supported by the simple predicate method" do
|
124
|
+
assert @campaign.medium?(:web)
|
125
|
+
assert @campaign.medium?(:print)
|
126
|
+
assert !@campaign.medium?(:email)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "for multiple values" do
|
131
|
+
should "be supported by the simple predicate method" do
|
132
|
+
assert @campaign.medium?(:web, :print)
|
133
|
+
assert !@campaign.medium?(:web, :email)
|
134
|
+
end
|
135
|
+
end
|
149
136
|
end
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
137
|
+
|
138
|
+
context "named scopes" do
|
139
|
+
setup do
|
140
|
+
@company = @company_class.create(:name => "Test Co, Intl.")
|
141
|
+
@campaign1 = @company.campaigns.create :medium => [:web, :print]
|
142
|
+
@campaign2 = @company.campaigns.create
|
143
|
+
@campaign3 = @company.campaigns.create :medium => [:web, :email]
|
144
|
+
@campaign4 = @company.campaigns.create :medium => [:web]
|
145
|
+
@campaign5 = @company.campaigns.create :medium => [:web, :print, :email]
|
146
|
+
@campaign6 = @company.campaigns.create :medium => [:web, :print, :email, :phone]
|
147
|
+
@campaign7 = @company.campaigns.create :medium => [:email, :phone]
|
148
|
+
end
|
149
|
+
|
150
|
+
should "support retrieval by any value" do
|
151
|
+
assert_equal [@campaign1, @campaign3, @campaign4, @campaign5, @campaign6, @campaign7], @company.campaigns.with_medium
|
152
|
+
end
|
153
|
+
|
154
|
+
should "support retrieval by one matching value" do
|
155
|
+
assert_equal [@campaign1, @campaign5, @campaign6], @company.campaigns.with_medium(:print)
|
156
|
+
end
|
157
|
+
|
158
|
+
should "support retrieval by any matching value (OR)" do
|
159
|
+
assert_equal [@campaign1, @campaign3, @campaign5, @campaign6, @campaign7], @company.campaigns.with_any_medium(:print, :email)
|
160
|
+
end
|
161
|
+
|
162
|
+
should "support retrieval by all matching values" do
|
163
|
+
assert_equal [@campaign1, @campaign5, @campaign6], @company.campaigns.with_medium(:web, :print)
|
164
|
+
assert_equal [@campaign3, @campaign5, @campaign6], @company.campaigns.with_medium(:web, :email)
|
165
|
+
end
|
166
|
+
|
167
|
+
should "support retrieval for no values" do
|
168
|
+
assert_equal [@campaign2], @company.campaigns.without_medium
|
169
|
+
assert_equal [@campaign2], @company.campaigns.no_medium
|
170
|
+
end
|
171
|
+
|
172
|
+
should "support retrieval without a specific value" do
|
173
|
+
assert_equal [@campaign2, @campaign3, @campaign4, @campaign7], @company.campaigns.without_medium(:print)
|
174
|
+
assert_equal [@campaign2, @campaign7], @company.campaigns.without_medium(:web, :print)
|
175
|
+
assert_equal [@campaign2, @campaign3, @campaign4], @company.campaigns.without_medium(:print, :phone)
|
176
|
+
end
|
177
|
+
|
178
|
+
should "support retrieval by exact value" do
|
179
|
+
assert_equal [@campaign4], @company.campaigns.with_exact_medium(:web)
|
180
|
+
assert_equal [@campaign1], @company.campaigns.with_exact_medium(:web, :print)
|
181
|
+
assert_equal [@campaign2], @company.campaigns.with_exact_medium
|
182
|
+
end
|
183
|
+
|
184
|
+
should "not retrieve retrieve a subsequent zero value for an unqualified with scope " do
|
185
|
+
assert_equal [@campaign1, @campaign3, @campaign4, @campaign5, @campaign6, @campaign7], @company.campaigns.with_medium
|
186
|
+
@campaign4.medium = []
|
187
|
+
@campaign4.save
|
188
|
+
assert_equal [@campaign1, @campaign3, @campaign5, @campaign6, @campaign7], @company.campaigns.with_medium
|
189
|
+
assert_equal [@campaign1, @campaign3, @campaign5, @campaign6, @campaign7], @company.campaigns.with_any_medium
|
190
|
+
end
|
191
|
+
|
192
|
+
should "not retrieve retrieve a subsequent zero value for a qualified with scope " do
|
193
|
+
assert_equal [@campaign1, @campaign3, @campaign4, @campaign5, @campaign6], @company.campaigns.with_medium(:web)
|
194
|
+
@campaign4.medium = []
|
195
|
+
@campaign4.save
|
196
|
+
assert_equal [@campaign1, @campaign3, @campaign5, @campaign6], @company.campaigns.with_medium(:web)
|
197
|
+
assert_equal [@campaign1, @campaign3, @campaign5, @campaign6], @company.campaigns.with_any_medium(:web)
|
198
|
+
end
|
154
199
|
end
|
155
200
|
|
156
|
-
should "
|
157
|
-
|
158
|
-
|
201
|
+
should "can check if at least one value is set" do
|
202
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
203
|
+
assert campaign.medium?
|
204
|
+
|
205
|
+
campaign = @campaign_class.new
|
206
|
+
assert !campaign.medium?
|
159
207
|
end
|
160
|
-
|
161
|
-
should "
|
162
|
-
|
208
|
+
|
209
|
+
should "find by bitmask values" do
|
210
|
+
campaign = @campaign_class.new(:medium => [:web, :print])
|
211
|
+
assert campaign.save
|
212
|
+
|
213
|
+
assert_equal(
|
214
|
+
@campaign_class.find(:all, :conditions => ['medium & ? <> 0', @campaign_class.bitmask_for_medium(:print)]),
|
215
|
+
@campaign_class.medium_for_print
|
216
|
+
)
|
217
|
+
|
218
|
+
assert_equal @campaign_class.medium_for_print.first, @campaign_class.medium_for_print.medium_for_web.first
|
219
|
+
|
220
|
+
assert_equal [], @campaign_class.medium_for_email
|
221
|
+
assert_equal [], @campaign_class.medium_for_web.medium_for_email
|
163
222
|
end
|
164
|
-
end
|
165
223
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
campaign = Campaign.new
|
171
|
-
assert !campaign.medium?
|
172
|
-
end
|
224
|
+
should "find no values" do
|
225
|
+
campaign = @campaign_class.create(:medium => [:web, :print])
|
226
|
+
assert campaign.save
|
173
227
|
|
174
|
-
|
175
|
-
campaign = Campaign.new(:medium => [:web, :print])
|
176
|
-
assert campaign.save
|
177
|
-
|
178
|
-
assert_equal(
|
179
|
-
Campaign.find(:all, :conditions => ['medium & ? <> 0', Campaign.bitmask_for_medium(:print)]),
|
180
|
-
Campaign.medium_for_print
|
181
|
-
)
|
182
|
-
|
183
|
-
assert_equal Campaign.medium_for_print.first, Campaign.medium_for_print.medium_for_web.first
|
184
|
-
|
185
|
-
assert_equal [], Campaign.medium_for_email
|
186
|
-
assert_equal [], Campaign.medium_for_web.medium_for_email
|
187
|
-
end
|
228
|
+
assert_equal [], @campaign_class.no_medium
|
188
229
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
campaign.medium = []
|
196
|
-
assert campaign.save
|
197
|
-
|
198
|
-
assert_equal [campaign], Campaign.no_medium
|
199
|
-
end
|
230
|
+
campaign.medium = []
|
231
|
+
assert campaign.save
|
232
|
+
|
233
|
+
assert_equal [campaign], @campaign_class.no_medium
|
234
|
+
end
|
200
235
|
|
236
|
+
should "allow zero in values without changing result" do
|
237
|
+
assert_equal 0,@campaign_class.bitmask_for_allow_zero(:none)
|
238
|
+
assert_equal 0b111,@campaign_class.bitmask_for_allow_zero(:one,:two,:three,:none)
|
201
239
|
|
202
|
-
|
240
|
+
campaign = @campaign_class.new(:allow_zero => :none)
|
241
|
+
assert campaign.save
|
242
|
+
assert_equal [],campaign.allow_zero
|
203
243
|
|
204
|
-
|
205
|
-
|
244
|
+
campaign.allow_zero = :none
|
245
|
+
assert campaign.save
|
246
|
+
assert_equal [],campaign.allow_zero
|
247
|
+
|
248
|
+
campaign.allow_zero = [:one,:none]
|
249
|
+
assert campaign.save
|
250
|
+
assert_equal [:one],campaign.allow_zero
|
206
251
|
end
|
207
252
|
|
208
|
-
|
209
|
-
|
210
|
-
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def assert_unsupported(&block)
|
257
|
+
assert_raises(ArgumentError, &block)
|
211
258
|
end
|
212
|
-
|
213
|
-
|
259
|
+
|
260
|
+
def assert_stored(record, *values)
|
261
|
+
values.each do |value|
|
262
|
+
assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
|
263
|
+
end
|
264
|
+
full_mask = values.inject(0) do |mask, value|
|
265
|
+
mask | @campaign_class.bitmasks[:medium][value]
|
266
|
+
end
|
267
|
+
assert_equal full_mask, record.medium.to_i
|
214
268
|
end
|
215
|
-
assert_equal full_mask, record.medium.to_i
|
216
|
-
end
|
217
269
|
|
270
|
+
end
|
218
271
|
end
|
272
|
+
|
273
|
+
context_with_classes 'Campaign with null attributes',CampaignWithNull,CompanyWithNull
|
274
|
+
context_with_classes 'Campaign without null attributes',CampaignWithoutNull,CompanyWithoutNull
|
219
275
|
end
|
data/test/support/models.rb
CHANGED
@@ -1,26 +1,50 @@
|
|
1
1
|
ActiveRecord::Schema.define do
|
2
|
-
create_table :
|
2
|
+
create_table :campaign_with_nulls do |t|
|
3
3
|
t.integer :company_id
|
4
|
-
t.integer :medium, :misc, :Legacy
|
4
|
+
t.integer :medium, :allow_zero, :misc, :Legacy
|
5
5
|
end
|
6
|
-
create_table :
|
6
|
+
create_table :company_with_nulls do |t|
|
7
|
+
t.string :name
|
8
|
+
end
|
9
|
+
create_table :campaign_without_nulls do |t|
|
10
|
+
t.integer :company_id
|
11
|
+
t.integer :medium, :allow_zero, :misc, :Legacy, :null => false, :default => 0
|
12
|
+
end
|
13
|
+
create_table :company_without_nulls do |t|
|
7
14
|
t.string :name
|
8
15
|
end
|
9
16
|
end
|
10
17
|
|
18
|
+
# Pseudo models for testing purposes
|
11
19
|
|
12
|
-
class
|
13
|
-
has_many :campaigns
|
20
|
+
class CompanyWithNull < ActiveRecord::Base
|
21
|
+
has_many :campaigns,:class_name => 'CampaignWithNull',:foreign_key => 'company_id'
|
14
22
|
end
|
15
23
|
|
16
|
-
|
17
|
-
|
18
|
-
belongs_to :company
|
24
|
+
class CampaignWithNull < ActiveRecord::Base
|
25
|
+
belongs_to :company,:class_name => 'CompanyWithNull'
|
19
26
|
bitmask :medium, :as => [:web, :print, :email, :phone]
|
27
|
+
bitmask :allow_zero, :as => [:one, :two, :three], :zero_value => :none
|
20
28
|
bitmask :misc, :as => %w(some useless values) do
|
21
29
|
def worked?
|
22
30
|
true
|
23
31
|
end
|
24
32
|
end
|
25
33
|
bitmask :Legacy, :as => [:upper, :case]
|
34
|
+
end
|
35
|
+
|
36
|
+
class CompanyWithoutNull < ActiveRecord::Base
|
37
|
+
has_many :campaigns,:class_name => 'CampaignWithoutNull',:foreign_key => 'company_id'
|
38
|
+
end
|
39
|
+
|
40
|
+
class CampaignWithoutNull < ActiveRecord::Base
|
41
|
+
belongs_to :company,:class_name => 'CompanyWithoutNull'
|
42
|
+
bitmask :medium, :as => [:web, :print, :email, :phone], :null => false
|
43
|
+
bitmask :allow_zero, :as => [:one, :two, :three], :zero_value => :none, :null => false
|
44
|
+
bitmask :misc, :as => %w(some useless values), :null => false do
|
45
|
+
def worked?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
bitmask :Legacy, :as => [:upper, :case], :null => false
|
26
50
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bitmask_attributes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-04-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,7 +21,12 @@ dependencies:
|
|
21
21
|
version: '3.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
25
30
|
description:
|
26
31
|
email: joel@developwithstyle.com
|
27
32
|
executables: []
|
@@ -31,6 +36,7 @@ files:
|
|
31
36
|
- .document
|
32
37
|
- .gitignore
|
33
38
|
- .rvmrc
|
39
|
+
- .travis.yml
|
34
40
|
- CHANGELOG.rdoc
|
35
41
|
- Gemfile
|
36
42
|
- Gemfile.lock
|
@@ -75,7 +81,6 @@ files:
|
|
75
81
|
- lib/bitmask_attributes/value_proxy.rb
|
76
82
|
- lib/bitmask_attributes/version.rb
|
77
83
|
- test/bitmask_attributes_test.rb
|
78
|
-
- test/support/helpers.rb
|
79
84
|
- test/support/models.rb
|
80
85
|
- test/test_helper.rb
|
81
86
|
homepage: http://github.com/joelmoss/bitmask_attributes
|
@@ -98,12 +103,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
103
|
version: '0'
|
99
104
|
requirements: []
|
100
105
|
rubyforge_project:
|
101
|
-
rubygems_version: 1.8.
|
106
|
+
rubygems_version: 1.8.23
|
102
107
|
signing_key:
|
103
108
|
specification_version: 3
|
104
109
|
summary: Simple bitmask attribute support for ActiveRecord
|
105
110
|
test_files:
|
106
111
|
- test/bitmask_attributes_test.rb
|
107
|
-
- test/support/helpers.rb
|
108
112
|
- test/support/models.rb
|
109
113
|
- test/test_helper.rb
|
data/test/support/helpers.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
class ActiveSupport::TestCase
|
2
|
-
|
3
|
-
def assert_unsupported(&block)
|
4
|
-
assert_raises(ArgumentError, &block)
|
5
|
-
end
|
6
|
-
|
7
|
-
def assert_stored(record, *values)
|
8
|
-
values.each do |value|
|
9
|
-
assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
|
10
|
-
end
|
11
|
-
full_mask = values.inject(0) do |mask, value|
|
12
|
-
mask | Campaign.bitmasks[:medium][value]
|
13
|
-
end
|
14
|
-
assert_equal full_mask, record.medium.to_i
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|