assignable_values 0.13.2 → 0.14.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.
- checksums.yaml +5 -5
- data/.travis.yml +6 -1
- data/CHANGELOG.md +56 -0
- data/README.md +36 -18
- data/gemfiles/Gemfile.2.3 +1 -1
- data/gemfiles/Gemfile.2.3.lock +4 -4
- data/gemfiles/Gemfile.3.2.lock +2 -2
- data/gemfiles/Gemfile.4.2.lock +2 -2
- data/gemfiles/Gemfile.5.0.lock +2 -2
- data/gemfiles/Gemfile.5.1.lock +2 -2
- data/gemfiles/Gemfile.5.1.pg +16 -0
- data/gemfiles/Gemfile.5.1.pg.lock +68 -0
- data/lib/assignable_values/active_record/restriction/base.rb +54 -33
- data/lib/assignable_values/active_record/restriction/belongs_to_association.rb +7 -2
- data/lib/assignable_values/version.rb +1 -1
- data/spec/assignable_values/active_record_spec.rb +161 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/database.rb +6 -2
- data/spec/support/models.rb +6 -2
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dd2475ca27683328810e810b40276043bf60b442c621be57f29885e58647183a
|
4
|
+
data.tar.gz: f3e17c9e48ce5e73faf2dfe3b3529b8275c603beef2551c6b3d83cac01a3343e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2f513338f30247aba1d2b8f436266e3f8865c3c367a0367268131f4f61df7ee80a21e049a4a96b7ce65e90e38068ee759ca6829e23e620635d4595cfa6cc825
|
7
|
+
data.tar.gz: be46b3706a7fc5fece9c43b4ac47164ae9b1cd67a43227707c6182d8ae5755eed601aed0f79d25a5fbfdca88519df64f1323104999265725f0992fb6662417f5
|
data/.travis.yml
CHANGED
@@ -12,6 +12,7 @@ gemfile:
|
|
12
12
|
- gemfiles/Gemfile.4.2
|
13
13
|
- gemfiles/Gemfile.5.0
|
14
14
|
- gemfiles/Gemfile.5.1
|
15
|
+
- gemfiles/Gemfile.5.1.pg
|
15
16
|
|
16
17
|
matrix:
|
17
18
|
exclude:
|
@@ -37,6 +38,10 @@ matrix:
|
|
37
38
|
rvm: 2.1.8
|
38
39
|
- gemfile: gemfiles/Gemfile.5.1
|
39
40
|
rvm: 1.8.7
|
41
|
+
- gemfile: gemfiles/Gemfile.5.1.pg
|
42
|
+
rvm: 2.1.8
|
43
|
+
- gemfile: gemfiles/Gemfile.5.1.pg
|
44
|
+
rvm: 1.8.7
|
40
45
|
|
41
46
|
sudo: false
|
42
47
|
|
@@ -47,8 +52,8 @@ notifications:
|
|
47
52
|
- fail@makandra.de
|
48
53
|
|
49
54
|
before_script:
|
55
|
+
- psql -c 'create database assignable_values_test;' -U postgres
|
50
56
|
- mysql -e 'create database IF NOT EXISTS assignable_values_test;'
|
51
|
-
- mysql -e "GRANT ALL PRIVILEGES ON assignable_values_test.* TO 'travis'@'%';"
|
52
57
|
|
53
58
|
install:
|
54
59
|
# Old Travis CI bundler explodes when lockfile version doesn't match recently bumped version
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
All notable changes to this project will be documented in this file.
|
2
|
+
|
3
|
+
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
4
|
+
|
5
|
+
## Unreleased
|
6
|
+
|
7
|
+
### Breaking changes
|
8
|
+
|
9
|
+
-
|
10
|
+
|
11
|
+
### Compatible changes
|
12
|
+
|
13
|
+
-
|
14
|
+
|
15
|
+
|
16
|
+
## 0.14.0 - 2018-09-17
|
17
|
+
|
18
|
+
### Compatible changes
|
19
|
+
|
20
|
+
- Add support for Array columns using `multiple: true`.
|
21
|
+
|
22
|
+
|
23
|
+
## 0.13.2 - 2018-01-23
|
24
|
+
|
25
|
+
### Compatible changes
|
26
|
+
|
27
|
+
- Get rid of deprecation warnings on Rails 5.1+.
|
28
|
+
|
29
|
+
Thanks to irmela.
|
30
|
+
|
31
|
+
|
32
|
+
## 0.13.1 - 2017-10-24
|
33
|
+
|
34
|
+
### Compatible changes
|
35
|
+
|
36
|
+
- Add Rails 5.1 compatibility.
|
37
|
+
|
38
|
+
Thanks to GuidoSchweizer.
|
39
|
+
|
40
|
+
|
41
|
+
## 0.13.0 - 2017-09-08
|
42
|
+
|
43
|
+
### Breaking changes
|
44
|
+
|
45
|
+
- No longer support providing humanized values as a hash in favour of always using I18n.
|
46
|
+
|
47
|
+
### Compatible changes
|
48
|
+
|
49
|
+
- Fix a bug with a `has_many :through` when return a nil object.
|
50
|
+
|
51
|
+
Thanks to foobear.
|
52
|
+
|
53
|
+
|
54
|
+
## Older releases
|
55
|
+
|
56
|
+
Please check commits.
|
data/README.md
CHANGED
@@ -21,8 +21,8 @@ The basic usage to restrict the values assignable to strings, integers, etc. is
|
|
21
21
|
|
22
22
|
The assigned value is checked during validation:
|
23
23
|
|
24
|
-
Song.new(:
|
25
|
-
Song.new(:
|
24
|
+
Song.new(genre: 'rock').valid? # => true
|
25
|
+
Song.new(genre: 'elephant').valid? # => false
|
26
26
|
|
27
27
|
The validation error message is the same as the one from `validates_inclusion_of` (`errors.messages.inclusion` in your I18n dictionary).
|
28
28
|
You can also set a custom error message with the `:message` option.
|
@@ -79,7 +79,7 @@ A good way to populate a `<select>` tag with pairs of internal values and human
|
|
79
79
|
You can define a default value by using the `:default` option:
|
80
80
|
|
81
81
|
class Song < ActiveRecord::Base
|
82
|
-
assignable_values_for :genre, :
|
82
|
+
assignable_values_for :genre, default: 'rock' do
|
83
83
|
['pop', 'rock', 'electronic']
|
84
84
|
end
|
85
85
|
end
|
@@ -91,7 +91,7 @@ The default is applied to new records:
|
|
91
91
|
Defaults can be procs:
|
92
92
|
|
93
93
|
class Song < ActiveRecord::Base
|
94
|
-
assignable_values_for :genre, :
|
94
|
+
assignable_values_for :genre, default: proc { Date.today.year } do
|
95
95
|
1980 .. 2011
|
96
96
|
end
|
97
97
|
end
|
@@ -101,7 +101,7 @@ The proc will be evaluated in the context of the record instance.
|
|
101
101
|
You can also default a secondary default that is only set if the primary default value is not assignable:
|
102
102
|
|
103
103
|
class Song < ActiveRecord::Base
|
104
|
-
assignable_values_for :year, :
|
104
|
+
assignable_values_for :year, default: 1999, secondary_default: proc { Date.today.year } do
|
105
105
|
(Date.today.year - 2) .. Date.today.year
|
106
106
|
end
|
107
107
|
end
|
@@ -119,14 +119,14 @@ will get a validation error.
|
|
119
119
|
If you would like to change this behavior and allow blank values to be valid, use the `:allow_blank` option:
|
120
120
|
|
121
121
|
class Song < ActiveRecord::Base
|
122
|
-
assignable_values_for :genre, :
|
122
|
+
assignable_values_for :genre, default: 'rock', allow_blank: true do
|
123
123
|
['pop', 'rock', 'electronic']
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
127
127
|
The `:allow_blank` option can be a symbol, in which case a method of that name will be called on the record.
|
128
128
|
|
129
|
-
The `:allow_blank` option can also be a
|
129
|
+
The `:allow_blank` option can also be a proc, in which case the proc will be called in the context of the record.
|
130
130
|
|
131
131
|
|
132
132
|
### Values are only validated when they change
|
@@ -141,7 +141,7 @@ Values are only validated when they change. This is useful when the list of assi
|
|
141
141
|
|
142
142
|
If a value has been saved before, it will remain valid, even if it is no longer assignable:
|
143
143
|
|
144
|
-
Song.update_all(:
|
144
|
+
Song.update_all(year: 1985) # update all records with a value that is no longer valid
|
145
145
|
song = Song.last
|
146
146
|
song.year # => 1985
|
147
147
|
song.valid? # => true
|
@@ -164,6 +164,24 @@ Once a changed value has been saved, the previous value disappears from the list
|
|
164
164
|
|
165
165
|
This is to prevent records from becoming invalid as the list of assignable values evolves. This also prevents `<select>` menus with blank selections when opening an old record in a web form.
|
166
166
|
|
167
|
+
### Array values
|
168
|
+
|
169
|
+
Assignable values can also be used for array values. This works when you use Rails 5+ and PostgreSQL with an array column, or with ActiveRecord's `serialize`.
|
170
|
+
|
171
|
+
To validate array values, pass `multiple: true`:
|
172
|
+
|
173
|
+
```
|
174
|
+
class Song < ActiveRecord::Base
|
175
|
+
serialize :genres # skip this when you use PostgreSQL and an array type column
|
176
|
+
|
177
|
+
assignable_values_for :genres, multiple: true do
|
178
|
+
['pop', 'rock', 'electronic']
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
In this case, every *subset* of the given values is valid, for example `['pop', 'electronic']`.
|
184
|
+
|
167
185
|
|
168
186
|
Restricting belongs_to associations
|
169
187
|
-----------------------------------
|
@@ -175,15 +193,15 @@ You can restrict `belongs_to` associations in the same manner as scalar attribut
|
|
175
193
|
belongs_to :artist
|
176
194
|
|
177
195
|
assignable_values_for :artist do
|
178
|
-
Artist.where(:
|
196
|
+
Artist.where(signed: true)
|
179
197
|
end
|
180
198
|
|
181
199
|
end
|
182
200
|
|
183
201
|
Listing and validating als works the same:
|
184
202
|
|
185
|
-
chicane = Artist.create!(:
|
186
|
-
lt2 = Artist.create!(:
|
203
|
+
chicane = Artist.create!(name: 'Chicane', signed: true)
|
204
|
+
lt2 = Artist.create!(name: 'LT2', signed: false)
|
187
205
|
|
188
206
|
song = Song.new
|
189
207
|
|
@@ -225,10 +243,10 @@ Obtaining assignable values from another source
|
|
225
243
|
|
226
244
|
The list of assignable values can be provided by any object that is accessible from your model. This is useful for authorization scenarios like [Consul](https://github.com/makandra/consul) or [CanCan](https://github.com/ryanb/cancan), where permissions are defined in a single class.
|
227
245
|
|
228
|
-
You can define the source of assignable values by setting the `:through` option to a
|
246
|
+
You can define the source of assignable values by setting the `:through` option to a proc:
|
229
247
|
|
230
248
|
class Story < ActiveRecord::Base
|
231
|
-
assignable_values_for :state, :
|
249
|
+
assignable_values_for :state, through: proc { Power.current }
|
232
250
|
end
|
233
251
|
|
234
252
|
`Power.current` must now respond to a method `assignable_story_states` or `assignable_story_states(story)` which returns an `Enumerable` of state strings:
|
@@ -251,7 +269,7 @@ You can define the source of assignable values by setting the `:through` option
|
|
251
269
|
|
252
270
|
Listing and validating works the same with delegation:
|
253
271
|
|
254
|
-
story = Story.new(:
|
272
|
+
story = Story.new(state: 'accepted')
|
255
273
|
|
256
274
|
Power.current = Power.new(:guest)
|
257
275
|
story.assignable_states # => ['draft', 'pending']
|
@@ -263,17 +281,17 @@ Listing and validating works the same with delegation:
|
|
263
281
|
|
264
282
|
Note that delegated validation is skipped when the delegate is `nil`. This way your model remains usable when there is no authorization context, like in batch processes or the console:
|
265
283
|
|
266
|
-
story = Story.new(:
|
284
|
+
story = Story.new(state: 'foo')
|
267
285
|
Power.current = nil
|
268
286
|
story.valid? # => true
|
269
287
|
|
270
288
|
Think of this as enabling an optional authorization layer on top of your model validations, which can be switched on or off depending on the current context.
|
271
289
|
|
272
|
-
Instead of a
|
290
|
+
Instead of a proc you can also use the `:through` option to name an instance method:
|
273
291
|
|
274
292
|
class Story < ActiveRecord::Base
|
275
293
|
attr_accessor :power
|
276
|
-
assignable_values_for :state, :
|
294
|
+
assignable_values_for :state, through: :power
|
277
295
|
end
|
278
296
|
|
279
297
|
|
@@ -303,7 +321,7 @@ You can now delegate validation of assignable values to the current power by say
|
|
303
321
|
This is a shortcut for saying:
|
304
322
|
|
305
323
|
class Story < ActiveRecord::Base
|
306
|
-
assignable_values_for :state, :
|
324
|
+
assignable_values_for :state, through: proc { Power.current }
|
307
325
|
end
|
308
326
|
|
309
327
|
Head over to the [Consul README](https://github.com/makandra/consul) for details.
|
data/gemfiles/Gemfile.2.3
CHANGED
data/gemfiles/Gemfile.2.3.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
assignable_values (0.
|
4
|
+
assignable_values (0.14.0)
|
5
5
|
activerecord (>= 2.3)
|
6
6
|
|
7
7
|
GEM
|
@@ -13,7 +13,7 @@ GEM
|
|
13
13
|
database_cleaner (1.0.1)
|
14
14
|
gemika (0.3.2)
|
15
15
|
i18n (0.6.11)
|
16
|
-
mysql2 (0.2.
|
16
|
+
mysql2 (0.2.24)
|
17
17
|
rake (10.0.4)
|
18
18
|
rspec (1.3.2)
|
19
19
|
rspec_candy (0.2.8)
|
@@ -31,10 +31,10 @@ DEPENDENCIES
|
|
31
31
|
database_cleaner (~> 1.0.0)
|
32
32
|
gemika
|
33
33
|
i18n (< 0.7)
|
34
|
-
mysql2 (= 0.2.
|
34
|
+
mysql2 (= 0.2.24)
|
35
35
|
rake (= 10.0.4)
|
36
36
|
rspec (~> 1.3.0)
|
37
37
|
rspec_candy
|
38
38
|
|
39
39
|
BUNDLED WITH
|
40
|
-
1.
|
40
|
+
1.16.1
|
data/gemfiles/Gemfile.3.2.lock
CHANGED
data/gemfiles/Gemfile.4.2.lock
CHANGED
data/gemfiles/Gemfile.5.0.lock
CHANGED
data/gemfiles/Gemfile.5.1.lock
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Runtime dependencies
|
4
|
+
gem 'activerecord', '~>5.1.0'
|
5
|
+
gem 'i18n'
|
6
|
+
gem 'pg', '<1'
|
7
|
+
|
8
|
+
# Development dependencies
|
9
|
+
gem 'rake'
|
10
|
+
gem 'database_cleaner'
|
11
|
+
gem 'rspec'
|
12
|
+
gem 'rspec_candy'
|
13
|
+
gem 'gemika'
|
14
|
+
|
15
|
+
# Gem under test
|
16
|
+
gem 'assignable_values', :path => '..'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
assignable_values (0.14.0)
|
5
|
+
activerecord (>= 2.3)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (5.1.4)
|
11
|
+
activesupport (= 5.1.4)
|
12
|
+
activerecord (5.1.4)
|
13
|
+
activemodel (= 5.1.4)
|
14
|
+
activesupport (= 5.1.4)
|
15
|
+
arel (~> 8.0)
|
16
|
+
activesupport (5.1.4)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (~> 0.7)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
arel (8.0.0)
|
22
|
+
concurrent-ruby (1.0.5)
|
23
|
+
database_cleaner (1.6.1)
|
24
|
+
diff-lcs (1.3)
|
25
|
+
gemika (0.3.2)
|
26
|
+
i18n (0.9.0)
|
27
|
+
concurrent-ruby (~> 1.0)
|
28
|
+
minitest (5.10.3)
|
29
|
+
pg (0.21.0)
|
30
|
+
rake (12.1.0)
|
31
|
+
rspec (3.7.0)
|
32
|
+
rspec-core (~> 3.7.0)
|
33
|
+
rspec-expectations (~> 3.7.0)
|
34
|
+
rspec-mocks (~> 3.7.0)
|
35
|
+
rspec-core (3.7.0)
|
36
|
+
rspec-support (~> 3.7.0)
|
37
|
+
rspec-expectations (3.7.0)
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
39
|
+
rspec-support (~> 3.7.0)
|
40
|
+
rspec-mocks (3.7.0)
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
+
rspec-support (~> 3.7.0)
|
43
|
+
rspec-support (3.7.0)
|
44
|
+
rspec_candy (0.4.1)
|
45
|
+
rspec
|
46
|
+
sneaky-save
|
47
|
+
sneaky-save (0.1.2)
|
48
|
+
activerecord (>= 3.2.0)
|
49
|
+
thread_safe (0.3.6)
|
50
|
+
tzinfo (1.2.3)
|
51
|
+
thread_safe (~> 0.1)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
activerecord (~> 5.1.0)
|
58
|
+
assignable_values!
|
59
|
+
database_cleaner
|
60
|
+
gemika
|
61
|
+
i18n
|
62
|
+
pg (< 1)
|
63
|
+
rake
|
64
|
+
rspec
|
65
|
+
rspec_candy
|
66
|
+
|
67
|
+
BUNDLED WITH
|
68
|
+
1.16.4
|
@@ -29,22 +29,22 @@ module AssignableValues
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def
|
33
|
-
property
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
32
|
+
def set_default(record)
|
33
|
+
if record.new_record? && record.send(property).nil?
|
34
|
+
default_value = evaluate_default(record, default)
|
35
|
+
begin
|
36
|
+
if secondary_default? && !assignable_value?(record, default_value)
|
37
|
+
secondary_default_value = evaluate_default(record, secondary_default)
|
38
|
+
if assignable_value?(record, secondary_default_value)
|
39
|
+
default_value = secondary_default_value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
rescue AssignableValues::DelegateUnavailable
|
43
|
+
# skip secondary defaults if querying assignable values from a nil delegate
|
44
|
+
end
|
45
|
+
record.send("#{property}=", default_value)
|
41
46
|
end
|
42
|
-
|
43
|
-
|
44
|
-
def assignable_value?(record, value)
|
45
|
-
(has_previously_saved_value?(record) && value == previously_saved_value(record)) ||
|
46
|
-
(value.blank? && allow_blank?(record)) ||
|
47
|
-
assignable_values(record).include?(value)
|
47
|
+
true
|
48
48
|
end
|
49
49
|
|
50
50
|
def assignable_values(record, options = {})
|
@@ -53,7 +53,11 @@ module AssignableValues
|
|
53
53
|
|
54
54
|
if options.fetch(:include_old_value, true) && has_previously_saved_value?(record)
|
55
55
|
old_value = previously_saved_value(record)
|
56
|
-
|
56
|
+
if @options[:multiple]
|
57
|
+
if old_value.is_a?(Array)
|
58
|
+
assignable_values |= old_value
|
59
|
+
end
|
60
|
+
elsif !old_value.blank? && !current_values.include?(old_value)
|
57
61
|
assignable_values << old_value
|
58
62
|
end
|
59
63
|
end
|
@@ -65,25 +69,42 @@ module AssignableValues
|
|
65
69
|
assignable_values
|
66
70
|
end
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
# skip secondary defaults if querying assignable values from a nil delegate
|
80
|
-
end
|
81
|
-
record.send("#{property}=", default_value)
|
72
|
+
private
|
73
|
+
|
74
|
+
def error_property
|
75
|
+
property
|
76
|
+
end
|
77
|
+
|
78
|
+
def not_included_error_message
|
79
|
+
if @options[:message]
|
80
|
+
@options[:message]
|
81
|
+
else
|
82
|
+
I18n.t('errors.messages.inclusion', :default => 'is not included in the list')
|
82
83
|
end
|
83
|
-
true
|
84
84
|
end
|
85
85
|
|
86
|
-
|
86
|
+
def assignable_value?(record, value)
|
87
|
+
if @options[:multiple]
|
88
|
+
assignable_multi_value?(record, value)
|
89
|
+
else
|
90
|
+
assignable_single_value?(record, value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def assignable_single_value?(record, value)
|
95
|
+
(has_previously_saved_value?(record) && value == previously_saved_value(record)) ||
|
96
|
+
(value.blank? && allow_blank?(record)) ||
|
97
|
+
assignable_values(record, :include_old_value => false).include?(value)
|
98
|
+
end
|
99
|
+
|
100
|
+
def assignable_multi_value?(record, value)
|
101
|
+
(has_previously_saved_value?(record) && value == previously_saved_value(record)) ||
|
102
|
+
(value.blank? ? allow_blank?(record) : subset?(value, assignable_values(record)))
|
103
|
+
end
|
104
|
+
|
105
|
+
def subset?(array1, array2)
|
106
|
+
array1.is_a?(Array) && array2.is_a?(Array) && (array1 - array2).empty?
|
107
|
+
end
|
87
108
|
|
88
109
|
def evaluate_default(record, value_or_proc)
|
89
110
|
if value_or_proc.is_a?(Proc)
|
@@ -177,7 +198,7 @@ module AssignableValues
|
|
177
198
|
define_method validate_method do
|
178
199
|
restriction.validate_record(self)
|
179
200
|
end
|
180
|
-
validate validate_method
|
201
|
+
validate validate_method.to_sym
|
181
202
|
end
|
182
203
|
end
|
183
204
|
|
@@ -3,6 +3,13 @@ module AssignableValues
|
|
3
3
|
module Restriction
|
4
4
|
class BelongsToAssociation < Base
|
5
5
|
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
if @options[:multiple]
|
9
|
+
raise "Option :multiple is not allowed for restricting belongs_to associations."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
6
13
|
private
|
7
14
|
|
8
15
|
def association_class
|
@@ -48,8 +55,6 @@ module AssignableValues
|
|
48
55
|
record.send(property)
|
49
56
|
end
|
50
57
|
|
51
|
-
private
|
52
|
-
|
53
58
|
def association_id_was(record)
|
54
59
|
if record.respond_to?(:attribute_in_database)
|
55
60
|
record.attribute_in_database(:"#{association_id_method}").presence # Rails >= 5.1
|
@@ -39,6 +39,50 @@ describe AssignableValues::ActiveRecord do
|
|
39
39
|
|
40
40
|
end
|
41
41
|
|
42
|
+
context 'when validating virtual attributes with multiple: true' do
|
43
|
+
|
44
|
+
context 'with allow_blank: false' do
|
45
|
+
|
46
|
+
before :each do
|
47
|
+
@klass = Song.disposable_copy do
|
48
|
+
assignable_values_for :sub_genres,:multiple => true do
|
49
|
+
%w[pop rock]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should validate that the attribute is a subset' do
|
55
|
+
@klass.new(:sub_genres => ['pop']).should be_valid
|
56
|
+
@klass.new(:sub_genres => ['pop', 'rock']).should be_valid
|
57
|
+
@klass.new(:sub_genres => ['pop', 'disallowed value']).should_not be_valid
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should not allow nil or [] for allow_blank: false' do
|
61
|
+
@klass.new(:sub_genres => nil).should_not be_valid
|
62
|
+
@klass.new(:sub_genres => []).should_not be_valid
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with allow_blank: true' do
|
68
|
+
|
69
|
+
before :each do
|
70
|
+
@klass = Song.disposable_copy do
|
71
|
+
assignable_values_for :sub_genres, :multiple => true, :allow_blank => true do
|
72
|
+
%w[pop rock]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should allow nil or [] for allow_blank: false' do
|
78
|
+
@klass.new(:sub_genres => nil).should be_valid
|
79
|
+
@klass.new(:sub_genres => []).should be_valid
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
42
86
|
context 'when validating scalar attributes' do
|
43
87
|
|
44
88
|
context 'without options' do
|
@@ -195,6 +239,76 @@ describe AssignableValues::ActiveRecord do
|
|
195
239
|
|
196
240
|
end
|
197
241
|
|
242
|
+
context 'when validating scalar attributes with multiple: true' do
|
243
|
+
|
244
|
+
context 'without options' do
|
245
|
+
|
246
|
+
before :each do
|
247
|
+
@klass = Song.disposable_copy do
|
248
|
+
assignable_values_for :genres, :multiple => true do
|
249
|
+
%w[pop rock]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'should validate that the attribute is allowed' do
|
255
|
+
@klass.new(:genres => ['pop']).should be_valid
|
256
|
+
@klass.new(:genres => ['pop', 'rock']).should be_valid
|
257
|
+
@klass.new(:genres => ['pop', 'invalid value']).should_not be_valid
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should not allow a scalar attribute' do
|
261
|
+
@klass.new(:genres => 'pop').should_not be_valid
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should not allow nil or [] for the attribute value' do
|
265
|
+
@klass.new(:genres => nil).should_not be_valid
|
266
|
+
@klass.new(:genres => []).should_not be_valid
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'should allow a subset of previously saved values even if that value is no longer allowed' do
|
270
|
+
record = @klass.create!(:genres => ['pop'])
|
271
|
+
record.genres = ['pretend', 'previously', 'valid', 'value']
|
272
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
273
|
+
record.save(false)
|
274
|
+
else
|
275
|
+
record.save(:validate => false) # update without validations for the sake of this test
|
276
|
+
end
|
277
|
+
record.reload.should be_valid
|
278
|
+
record.genres = ['valid', 'previously', 'pop']
|
279
|
+
record.should be_valid
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'should allow a previously saved, blank value even if that value is no longer allowed' do
|
283
|
+
record = @klass.create!(:genres => ['pop'])
|
284
|
+
@klass.update_all(:genres => []) # update without validations for the sake of this test
|
285
|
+
record.reload.should be_valid
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
context 'if the :allow_blank option is set to true' do
|
291
|
+
|
292
|
+
before :each do
|
293
|
+
@klass = Song.disposable_copy do
|
294
|
+
assignable_values_for :genres, :multiple => true, :allow_blank => true do
|
295
|
+
%w[pop rock]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'should allow nil for the attribute value' do
|
301
|
+
@klass.new(:genres => nil).should be_valid
|
302
|
+
end
|
303
|
+
|
304
|
+
it 'should allow an empty array as value' do
|
305
|
+
@klass.new(:genres => []).should be_valid
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
311
|
+
|
198
312
|
context 'when validating belongs_to associations' do
|
199
313
|
|
200
314
|
it 'should validate that the association is allowed' do
|
@@ -262,6 +376,27 @@ describe AssignableValues::ActiveRecord do
|
|
262
376
|
record.should be_valid
|
263
377
|
end
|
264
378
|
|
379
|
+
it 'should not request the list of assignable values during validation if the association has not changed' do
|
380
|
+
allowed_association = Artist.create!
|
381
|
+
klass = Song.disposable_copy
|
382
|
+
record = klass.create!(:artist => allowed_association)
|
383
|
+
|
384
|
+
request_count = 0
|
385
|
+
|
386
|
+
klass.class_eval do
|
387
|
+
assignable_values_for :artist do
|
388
|
+
request_count += 1
|
389
|
+
[allowed_association]
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
record.reload
|
394
|
+
request_count.should == 0
|
395
|
+
record.year = 1975 # change any other attribute to make the record dirty
|
396
|
+
record.valid?
|
397
|
+
request_count.should == 0
|
398
|
+
end
|
399
|
+
|
265
400
|
it 'should allow nil for an association if the record was saved before with a nil association' do
|
266
401
|
allowed_association = Artist.create!
|
267
402
|
klass = Song.disposable_copy
|
@@ -372,6 +507,32 @@ describe AssignableValues::ActiveRecord do
|
|
372
507
|
klass.new.assignable_years.should == [1977, 1980, 1983]
|
373
508
|
end
|
374
509
|
|
510
|
+
it 'should not request the list of assignable values during validation if the association has not changed' do
|
511
|
+
allowed_association = Artist.create!
|
512
|
+
klass = Song.disposable_copy
|
513
|
+
request_count = 0
|
514
|
+
|
515
|
+
delegate = Class.new do
|
516
|
+
define_method :assignable_song_artists do
|
517
|
+
request_count += 1
|
518
|
+
[allowed_association]
|
519
|
+
end
|
520
|
+
end.new
|
521
|
+
|
522
|
+
record = klass.create!(:artist => allowed_association)
|
523
|
+
|
524
|
+
klass.class_eval do
|
525
|
+
assignable_values_for :artist, :through => lambda { delegate }
|
526
|
+
end
|
527
|
+
|
528
|
+
request_count.should == 0
|
529
|
+
|
530
|
+
record.reload
|
531
|
+
record.year = 1975 # change any other attribute to make the record dirty
|
532
|
+
record.valid?
|
533
|
+
request_count.should == 0
|
534
|
+
end
|
535
|
+
|
375
536
|
context 'when the delegation method returns nil' do
|
376
537
|
let(:klass) do
|
377
538
|
Song.disposable_copy do
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/database.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
Gemika::Database.new
|
1
|
+
database = Gemika::Database.new
|
2
|
+
database.connect
|
3
|
+
|
4
|
+
database.rewrite_schema! do
|
2
5
|
|
3
6
|
create_table :artists
|
4
7
|
|
@@ -7,10 +10,11 @@ Gemika::Database.new.rewrite_schema! do
|
|
7
10
|
t.string :genre
|
8
11
|
t.integer :year
|
9
12
|
t.integer :duration
|
13
|
+
t.string :genres, :array => true
|
10
14
|
end
|
11
15
|
|
12
16
|
create_table :vinyl_recordings do |t|
|
13
17
|
t.integer :year
|
14
18
|
end
|
15
19
|
|
16
|
-
end
|
20
|
+
end
|
data/spec/support/models.rb
CHANGED
@@ -4,12 +4,16 @@ class Artist < ActiveRecord::Base
|
|
4
4
|
|
5
5
|
end
|
6
6
|
|
7
|
-
|
8
7
|
class Song < ActiveRecord::Base
|
9
8
|
|
10
9
|
belongs_to :artist
|
11
10
|
|
12
|
-
attr_accessor :sub_genre
|
11
|
+
attr_accessor :sub_genre, :sub_genres
|
12
|
+
|
13
|
+
if ActiveRecord::VERSION::MAJOR < 4 || !Song.new(:genres => ['test']).genres.is_a?(Array)
|
14
|
+
# Rails 4 or not postgres
|
15
|
+
serialize :genres
|
16
|
+
end
|
13
17
|
|
14
18
|
end
|
15
19
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: assignable_values
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Henning Koch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-09-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -34,6 +34,7 @@ files:
|
|
34
34
|
- ".rspec"
|
35
35
|
- ".ruby-version"
|
36
36
|
- ".travis.yml"
|
37
|
+
- CHANGELOG.md
|
37
38
|
- Gemfile
|
38
39
|
- Gemfile.lock
|
39
40
|
- LICENSE
|
@@ -50,6 +51,8 @@ files:
|
|
50
51
|
- gemfiles/Gemfile.5.0.lock
|
51
52
|
- gemfiles/Gemfile.5.1
|
52
53
|
- gemfiles/Gemfile.5.1.lock
|
54
|
+
- gemfiles/Gemfile.5.1.pg
|
55
|
+
- gemfiles/Gemfile.5.1.pg.lock
|
53
56
|
- lib/assignable_values.rb
|
54
57
|
- lib/assignable_values/active_record.rb
|
55
58
|
- lib/assignable_values/active_record/restriction/base.rb
|
@@ -87,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
90
|
version: '0'
|
88
91
|
requirements: []
|
89
92
|
rubyforge_project:
|
90
|
-
rubygems_version: 2.
|
93
|
+
rubygems_version: 2.7.6
|
91
94
|
signing_key:
|
92
95
|
specification_version: 4
|
93
96
|
summary: Restrict the values assignable to ActiveRecord attributes or associations
|