assignable_values 0.13.2 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|