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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 80416120490eb1fa174ee1af4318c93ce18fd432
4
- data.tar.gz: e4379495924e83d42bb9b610b9adef3ba4ca3b01
2
+ SHA256:
3
+ metadata.gz: dd2475ca27683328810e810b40276043bf60b442c621be57f29885e58647183a
4
+ data.tar.gz: f3e17c9e48ce5e73faf2dfe3b3529b8275c603beef2551c6b3d83cac01a3343e
5
5
  SHA512:
6
- metadata.gz: efb7a94db723785ea7e3eab7c2a8919aa32cd16441c3f4b60d63d7463f63720a35d2f46c97fb81dd7c876d55337f7982f6a9fc525905401393abbf56fe94dedf
7
- data.tar.gz: d7094fe9d1419a44aa156e3b315ffcb981b449b1c419a8e5d52500389d2be0bbf14ab7cc8db79cca895931ac3a3027556de7a8d58957ac98001b822074f9209e
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(:genre => 'rock').valid? # => true
25
- Song.new(:genre => 'elephant').valid? # => false
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, :default => 'rock' do
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, :default => proc { Date.today.year } do
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, :default => 1999, :secondary_default => lambda { Date.today.year } do
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, :default => 'rock', :allow_blank => true do
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 lambda, in which case the lambda will be called in the context of the record.
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(:year => 1985) # update all records with a value that is no longer valid
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(:signed => true)
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!(:name => 'Chicane', :signed => true)
186
- lt2 = Artist.create!(:name => 'LT2', :signed => false)
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 lambda:
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, :through => lambda { Power.current }
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(:state => 'accepted')
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(:state => 'foo')
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 lambda you can also use the `:through` option to name an instance method:
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, :through => :power
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, :through => lambda { Power.current }
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
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  # Runtime dependencies
4
4
  gem 'activerecord', '~>2.3.0'
5
5
  gem 'i18n', '<0.7' # 0.7 no longer builds for Ruby 1.8.7
6
- gem 'mysql2', '= 0.2.20'
6
+ gem 'mysql2', '= 0.2.24'
7
7
 
8
8
  # Development dependencies
9
9
  gem 'rake', '=10.0.4'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- assignable_values (0.13.1)
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.20)
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.20)
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.15.4
40
+ 1.16.1
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- assignable_values (0.13.2)
4
+ assignable_values (0.14.0)
5
5
  activerecord (>= 2.3)
6
6
 
7
7
  GEM
@@ -62,4 +62,4 @@ DEPENDENCIES
62
62
  rspec_candy
63
63
 
64
64
  BUNDLED WITH
65
- 1.15.4
65
+ 1.16.1
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- assignable_values (0.13.2)
4
+ assignable_values (0.14.0)
5
5
  activerecord (>= 2.3)
6
6
 
7
7
  GEM
@@ -67,4 +67,4 @@ DEPENDENCIES
67
67
  rspec_candy
68
68
 
69
69
  BUNDLED WITH
70
- 1.16.1
70
+ 1.16.4
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- assignable_values (0.13.2)
4
+ assignable_values (0.14.0)
5
5
  activerecord (>= 2.3)
6
6
 
7
7
  GEM
@@ -64,4 +64,4 @@ DEPENDENCIES
64
64
  rspec_candy
65
65
 
66
66
  BUNDLED WITH
67
- 1.16.1
67
+ 1.16.4
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- assignable_values (0.13.2)
4
+ assignable_values (0.14.0)
5
5
  activerecord (>= 2.3)
6
6
 
7
7
  GEM
@@ -65,4 +65,4 @@ DEPENDENCIES
65
65
  rspec_candy
66
66
 
67
67
  BUNDLED WITH
68
- 1.16.1
68
+ 1.16.4
@@ -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 error_property
33
- property
34
- end
35
-
36
- def not_included_error_message
37
- if @options[:message]
38
- @options[:message]
39
- else
40
- I18n.t('errors.messages.inclusion', :default => 'is not included in the list')
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
- end
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
- unless old_value.blank? || current_values.include?(old_value)
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
- def set_default(record)
69
- if record.new_record? && record.send(property).nil?
70
- default_value = evaluate_default(record, default)
71
- begin
72
- if secondary_default? && !assignable_value?(record, default_value)
73
- secondary_default_value = evaluate_default(record, secondary_default)
74
- if assignable_value?(record, secondary_default_value)
75
- default_value = secondary_default_value
76
- end
77
- end
78
- rescue AssignableValues::DelegateUnavailable
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
- private
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
@@ -1,3 +1,3 @@
1
1
  module AssignableValues
2
- VERSION = '0.13.2'
2
+ VERSION = '0.14.0'
3
3
  end
@@ -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
@@ -1,7 +1,6 @@
1
1
  $: << File.join(File.dirname(__FILE__), "/../../lib" )
2
2
 
3
3
  require 'i18n'
4
- require 'mysql2'
5
4
  require 'active_record'
6
5
  require 'assignable_values'
7
6
  require 'rspec_candy/all'
@@ -1,4 +1,7 @@
1
- Gemika::Database.new.rewrite_schema! do
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
@@ -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.13.2
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-01-23 00:00:00.000000000 Z
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.6.6
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