assignable_values 0.18.1 → 1.0.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 +4 -4
- data/CHANGELOG.md +2 -0
- data/Gemfile.5.0.lock +1 -1
- data/Gemfile.5.1.lock +1 -1
- data/Gemfile.5.1.pg.lock +1 -1
- data/Gemfile.6.1.pg.lock +1 -1
- data/Gemfile.7.0.pg.lock +1 -1
- data/README.md +206 -146
- data/lib/assignable_values/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 974063fe5e304e09553d556f1eae415a7908f16bc026e713e811cb8180bc7510
|
4
|
+
data.tar.gz: 5754483e8d63baace26522a6e7885c750e0010a21ee87ce795421a917e0823d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13628f9ab3a4fbe96db3a2accfd2be3d00ac711994ece3694c3e3e3d15cf46c94def5e6f53270bd1b9242eb6bae40381b545c70ca68f62bc6d032415107d3918
|
7
|
+
data.tar.gz: 30833906903fb6291aad2157615631e76880a3df3d6342a057fc9993b021caa4d49173c13910aec5349b98d0af4647087fc4e18b26a7c7caba4a34b236cb9c81
|
data/CHANGELOG.md
CHANGED
data/Gemfile.5.0.lock
CHANGED
data/Gemfile.5.1.lock
CHANGED
data/Gemfile.5.1.pg.lock
CHANGED
data/Gemfile.6.1.pg.lock
CHANGED
data/Gemfile.7.0.pg.lock
CHANGED
data/README.md
CHANGED
@@ -13,16 +13,20 @@ Restricting scalar attributes
|
|
13
13
|
|
14
14
|
The basic usage to restrict the values assignable to strings, integers, etc. is this:
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
```rb
|
17
|
+
class Song < ActiveRecord::Base
|
18
|
+
assignable_values_for :genre do
|
19
|
+
['pop', 'rock', 'electronic']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
```
|
21
23
|
|
22
24
|
The assigned value is checked during validation:
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
+
```rb
|
27
|
+
Song.new(genre: 'rock').valid? # => true
|
28
|
+
Song.new(genre: 'elephant').valid? # => false
|
29
|
+
```
|
26
30
|
|
27
31
|
The validation error message is the same as the one from `validates_inclusion_of` (`errors.messages.inclusion` in your I18n dictionary).
|
28
32
|
You can also set a custom error message with the `:message` option.
|
@@ -31,13 +35,15 @@ You can also set a custom error message with the `:message` option.
|
|
31
35
|
### Listing assignable values
|
32
36
|
|
33
37
|
You can ask a record for a list of values that can be assigned to an attribute:
|
34
|
-
|
35
|
-
|
38
|
+
```rb
|
39
|
+
song.assignable_genres # => ['pop', 'rock', 'electronic']
|
40
|
+
```
|
36
41
|
|
37
42
|
This is useful for populating `<select>` tags in web forms:
|
38
43
|
|
39
|
-
|
40
|
-
|
44
|
+
```rb
|
45
|
+
form.select :genre, form.object.assignable_genres
|
46
|
+
```
|
41
47
|
|
42
48
|
### Humanized labels
|
43
49
|
|
@@ -45,49 +51,63 @@ You will often want to present internal values in a humanized form. E.g. `"pop"`
|
|
45
51
|
|
46
52
|
You can define human labels in your I18n dictionary:
|
47
53
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
```yaml
|
55
|
+
en:
|
56
|
+
assignable_values:
|
57
|
+
song:
|
58
|
+
genre:
|
59
|
+
pop: 'Pop music'
|
60
|
+
rock: 'Rock music'
|
61
|
+
electronic: 'Electronic music'
|
62
|
+
```
|
55
63
|
|
56
64
|
You can access the humanized version for the current value like this:
|
57
65
|
|
58
|
-
|
59
|
-
|
66
|
+
```rb
|
67
|
+
song = Song.new(:genre => 'pop')
|
68
|
+
song.humanized_genre # => 'Pop music'
|
69
|
+
```
|
60
70
|
|
61
71
|
Or you can retrieve the humanized version of any given value by passing it as an argument to either instance or class:
|
62
72
|
|
63
|
-
|
64
|
-
|
73
|
+
```rb
|
74
|
+
song.humanized_genre('rock') # => 'Rock music'
|
75
|
+
Song.humanized_genre('rock') # => 'Rock music'
|
76
|
+
```
|
65
77
|
|
66
78
|
You can obtain a list of all assignable values with their humanizations:
|
67
79
|
|
68
|
-
|
69
|
-
|
70
|
-
|
80
|
+
```rb
|
81
|
+
song.humanized_assignable_genres.size # => 3
|
82
|
+
song.humanized_assignable_genres.first.value # => "pop"
|
83
|
+
song.humanized_assignable_genres.first.humanized # => "Pop music"
|
84
|
+
```
|
71
85
|
|
72
86
|
A good way to populate a `<select>` tag with pairs of internal values and human labels is to use the `collection_select` helper from Rails:
|
73
87
|
|
74
|
-
|
88
|
+
```rb
|
89
|
+
form.collection_select :genre, form.object.humanized_assignable_genres, :value, :humanized
|
90
|
+
```
|
75
91
|
|
76
92
|
#### Humanized labels and inheritance
|
77
93
|
|
78
94
|
For models that inherit assignable values you can override the humanized labels:
|
79
95
|
|
80
|
-
|
81
|
-
|
82
|
-
|
96
|
+
```rb
|
97
|
+
class FunnySong < Song
|
98
|
+
...
|
99
|
+
end
|
100
|
+
```
|
83
101
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
102
|
+
```yaml
|
103
|
+
en:
|
104
|
+
assignable_values:
|
105
|
+
funny_song:
|
106
|
+
genre:
|
107
|
+
pop: 'The stuff you hear on mainstream radio all day long'
|
108
|
+
rock: 'A lot of electric guitars and drums'
|
109
|
+
electronic: 'Whatever David Guetta does'
|
110
|
+
```
|
91
111
|
|
92
112
|
If no humanization is provided for the child model (i.e. the `funny_song.genre` key) humanization will fall back to the
|
93
113
|
parent model (`song`).
|
@@ -96,38 +116,45 @@ parent model (`song`).
|
|
96
116
|
|
97
117
|
You can define a default value by using the `:default` option:
|
98
118
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
119
|
+
```rb
|
120
|
+
class Song < ActiveRecord::Base
|
121
|
+
assignable_values_for :genre, default: 'rock' do
|
122
|
+
['pop', 'rock', 'electronic']
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
104
126
|
|
105
127
|
The default is applied to new records:
|
106
|
-
|
107
|
-
|
128
|
+
```rb
|
129
|
+
Song.new.genre # => 'rock'
|
130
|
+
```
|
108
131
|
|
109
132
|
Defaults can be procs:
|
110
133
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
134
|
+
```rb
|
135
|
+
class Song < ActiveRecord::Base
|
136
|
+
assignable_values_for :year, default: proc { Date.today.year } do
|
137
|
+
1980 .. 2011
|
138
|
+
end
|
139
|
+
end
|
140
|
+
```
|
116
141
|
|
117
142
|
The proc will be evaluated in the context of the record instance.
|
118
143
|
|
119
144
|
You can also default a secondary default that is only set if the primary default value is not assignable:
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
145
|
+
```rb
|
146
|
+
class Song < ActiveRecord::Base
|
147
|
+
assignable_values_for :year, default: 1999, secondary_default: proc { Date.today.year } do
|
148
|
+
(Date.today.year - 2) .. Date.today.year
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
126
152
|
|
127
153
|
If called in 2013 the code above will fall back to:
|
128
154
|
|
129
|
-
|
130
|
-
|
155
|
+
```rb
|
156
|
+
Song.new.year # => 2013
|
157
|
+
```
|
131
158
|
|
132
159
|
### Allowing blank values
|
133
160
|
|
@@ -136,11 +163,13 @@ will get a validation error.
|
|
136
163
|
|
137
164
|
If you would like to change this behavior and allow blank values to be valid, use the `:allow_blank` option:
|
138
165
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
166
|
+
```rb
|
167
|
+
class Song < ActiveRecord::Base
|
168
|
+
assignable_values_for :genre, default: 'rock', allow_blank: true do
|
169
|
+
['pop', 'rock', 'electronic']
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
144
173
|
|
145
174
|
The `:allow_blank` option can be a symbol, in which case a method of that name will be called on the record.
|
146
175
|
|
@@ -151,34 +180,44 @@ The `:allow_blank` option can also be a proc, in which case the proc will be cal
|
|
151
180
|
|
152
181
|
Values are only validated when they change. This is useful when the list of assignable values can change during runtime:
|
153
182
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
183
|
+
```rb
|
184
|
+
class Song < ActiveRecord::Base
|
185
|
+
assignable_values_for :year do
|
186
|
+
(Date.today.year - 2) .. Date.today.year
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
159
190
|
|
160
191
|
If a value has been saved before, it will remain valid, even if it is no longer assignable:
|
161
192
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
193
|
+
```rb
|
194
|
+
Song.update_all(year: 1985) # update all records with a value that is no longer valid
|
195
|
+
song = Song.last
|
196
|
+
song.year # => 1985
|
197
|
+
song.valid? # => true
|
198
|
+
```
|
166
199
|
|
167
200
|
It will also be returned when obtaining the list of assignable values:
|
168
201
|
|
169
|
-
|
202
|
+
```rb
|
203
|
+
song.assignable_years # => [2010, 2011, 2012, 1985]
|
204
|
+
```
|
170
205
|
|
171
206
|
However, if you want only those values that are actually intended to be assignable, e.g. when updating a `<select>` via AJAX, pass an option:
|
172
207
|
|
173
|
-
|
208
|
+
```rb
|
209
|
+
song.assignable_years(include_old_value: false) # => [2010, 2011, 2012]
|
210
|
+
```
|
174
211
|
|
175
212
|
Once a changed value has been saved, the previous value disappears from the list of assignable values:
|
176
213
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
214
|
+
```rb
|
215
|
+
song.year = '2010'
|
216
|
+
song.save!
|
217
|
+
song.assignable_years # => [2010, 2011, 2012]
|
218
|
+
song.year = 1985
|
219
|
+
song.valid? # => false
|
220
|
+
```
|
182
221
|
|
183
222
|
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.
|
184
223
|
|
@@ -188,7 +227,7 @@ Assignable values can also be used for array values. This works when you use Rai
|
|
188
227
|
|
189
228
|
To validate array values, pass `multiple: true`:
|
190
229
|
|
191
|
-
```
|
230
|
+
```rb
|
192
231
|
class Song < ActiveRecord::Base
|
193
232
|
serialize :genres # skip this when you use PostgreSQL and an array type column
|
194
233
|
|
@@ -202,7 +241,7 @@ In this case, every *subset* of the given values is valid, for example `['pop',
|
|
202
241
|
|
203
242
|
For humanization, you can still use
|
204
243
|
|
205
|
-
```
|
244
|
+
```rb
|
206
245
|
song.humanized_genre('pop') # => "Pop music"
|
207
246
|
song.humanized_assignable_genres.last.humanized # => "Electronic music"
|
208
247
|
```
|
@@ -213,30 +252,34 @@ Restricting belongs_to associations
|
|
213
252
|
|
214
253
|
You can restrict `belongs_to` associations in the same manner as scalar attributes:
|
215
254
|
|
216
|
-
|
255
|
+
```rb
|
256
|
+
class Song < ActiveRecord::Base
|
217
257
|
|
218
|
-
|
258
|
+
belongs_to :artist
|
219
259
|
|
220
|
-
|
221
|
-
|
222
|
-
|
260
|
+
assignable_values_for :artist do
|
261
|
+
Artist.where(signed: true)
|
262
|
+
end
|
223
263
|
|
224
|
-
|
264
|
+
end
|
265
|
+
```
|
225
266
|
|
226
267
|
Listing and validating also works the same:
|
227
268
|
|
228
|
-
|
229
|
-
|
269
|
+
```rb
|
270
|
+
chicane = Artist.create!(name: 'Chicane', signed: true)
|
271
|
+
lt2 = Artist.create!(name: 'LT2', signed: false)
|
230
272
|
|
231
|
-
|
273
|
+
song = Song.new
|
232
274
|
|
233
|
-
|
275
|
+
song.assignable_artists # => [#<Artist id: 1, name: "Chicane">]
|
234
276
|
|
235
|
-
|
236
|
-
|
277
|
+
song.artist = chicane
|
278
|
+
song.valid? # => true
|
237
279
|
|
238
|
-
|
239
|
-
|
280
|
+
song.artist = lt2
|
281
|
+
song.valid? # => false
|
282
|
+
```
|
240
283
|
|
241
284
|
Similiar to scalar attributes, associations are only validated when the foreign key (`artist_id` in the example above) changes.
|
242
285
|
Values stored in the database will remain assignable until they are changed, and you can query actually assignable values with `song.assignable_artists(include_old_value: false)`.
|
@@ -249,18 +292,20 @@ How assignable values are evaluated
|
|
249
292
|
|
250
293
|
The list of assignable values is generated at runtime. Since the given block is evaluated on the record instance, so you can refer to other methods:
|
251
294
|
|
252
|
-
|
295
|
+
```rb
|
296
|
+
class Song < ActiveRecord::Base
|
253
297
|
|
254
|
-
|
298
|
+
validates_numericality_of :year
|
255
299
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
300
|
+
assignable_values_for :genre do
|
301
|
+
genres = []
|
302
|
+
genres << 'jazz' if year > 1900
|
303
|
+
genres << 'rock' if year > 1960
|
304
|
+
genres
|
305
|
+
end
|
262
306
|
|
263
|
-
|
307
|
+
end
|
308
|
+
```
|
264
309
|
|
265
310
|
|
266
311
|
Obtaining assignable values from another source
|
@@ -270,54 +315,62 @@ The list of assignable values can be provided by any object that is accessible f
|
|
270
315
|
|
271
316
|
You can define the source of assignable values by setting the `:through` option to a proc:
|
272
317
|
|
273
|
-
|
274
|
-
|
275
|
-
|
318
|
+
```rb
|
319
|
+
class Story < ActiveRecord::Base
|
320
|
+
assignable_values_for :state, through: proc { Power.current }
|
321
|
+
end
|
322
|
+
```
|
276
323
|
|
277
324
|
`Power.current` must now respond to a method `assignable_story_states` or `assignable_story_states(story)` which returns an `Enumerable` of state strings:
|
278
325
|
|
279
|
-
|
326
|
+
```rb
|
327
|
+
class Power
|
280
328
|
|
281
|
-
|
329
|
+
cattr_accessor :current
|
282
330
|
|
283
|
-
|
284
|
-
|
285
|
-
|
331
|
+
def initialize(role)
|
332
|
+
@role = role
|
333
|
+
end
|
286
334
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
335
|
+
def assignable_story_states(story)
|
336
|
+
states = ['draft', 'pending']
|
337
|
+
states << 'accepted' if @role == :admin
|
338
|
+
states
|
339
|
+
end
|
292
340
|
|
293
|
-
|
341
|
+
end
|
342
|
+
```
|
294
343
|
|
295
344
|
Listing and validating works the same with delegation:
|
345
|
+
```rb
|
346
|
+
story = Story.new(state: 'accepted')
|
296
347
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
story.assignable_states # => ['draft', 'pending']
|
301
|
-
story.valid? # => false
|
348
|
+
Power.current = Power.new(:guest)
|
349
|
+
story.assignable_states # => ['draft', 'pending']
|
350
|
+
story.valid? # => false
|
302
351
|
|
303
|
-
|
304
|
-
|
305
|
-
|
352
|
+
Power.current = Power.new(:admin)
|
353
|
+
story.assignable_states # => ['draft', 'pending', 'accepted']
|
354
|
+
story.valid? # => true
|
355
|
+
```
|
306
356
|
|
307
357
|
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:
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
358
|
+
```rb
|
359
|
+
story = Story.new(state: 'foo')
|
360
|
+
Power.current = nil
|
361
|
+
story.valid? # => true
|
362
|
+
```
|
312
363
|
|
313
364
|
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.
|
314
365
|
|
315
366
|
Instead of a proc you can also use the `:through` option to name an instance method:
|
316
367
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
368
|
+
```rb
|
369
|
+
class Story < ActiveRecord::Base
|
370
|
+
attr_accessor :power
|
371
|
+
assignable_values_for :state, through: :power
|
372
|
+
end
|
373
|
+
```
|
321
374
|
|
322
375
|
|
323
376
|
### Obtaining assignable values from a Consul power
|
@@ -326,28 +379,34 @@ A common use case for the `:through` option is when there is some globally acces
|
|
326
379
|
|
327
380
|
If you are using [Consul](https://github.com/makandra/consul), you will get a lot of this plumbing for free. Consul gives you a macro `current_power` to instantiate a so called "power", which describes what the current user may access:
|
328
381
|
|
329
|
-
|
330
|
-
|
382
|
+
```rb
|
383
|
+
class ApplicationController < ActionController::Base
|
384
|
+
include Consul::Controller
|
331
385
|
|
332
|
-
|
333
|
-
|
334
|
-
|
386
|
+
current_power do
|
387
|
+
Power.new(current_user)
|
388
|
+
end
|
335
389
|
|
336
|
-
|
390
|
+
end
|
391
|
+
```
|
337
392
|
|
338
393
|
The code above will provide you with a helper method `current_power` for your controller and views. Everywhere else, you can simply access it from `Power.current`.
|
339
394
|
|
340
395
|
You can now delegate validation of assignable values to the current power by saying:
|
341
396
|
|
342
|
-
|
343
|
-
|
344
|
-
|
397
|
+
```rb
|
398
|
+
class Story < ActiveRecord::Base
|
399
|
+
authorize_values_for :state
|
400
|
+
end
|
401
|
+
```
|
345
402
|
|
346
403
|
This is a shortcut for saying:
|
347
404
|
|
348
|
-
|
349
|
-
|
350
|
-
|
405
|
+
```rb
|
406
|
+
class Story < ActiveRecord::Base
|
407
|
+
assignable_values_for :state, through: proc { Power.current }
|
408
|
+
end
|
409
|
+
```
|
351
410
|
|
352
411
|
Head over to the [Consul README](https://github.com/makandra/consul) for details.
|
353
412
|
|
@@ -356,8 +415,9 @@ Installation
|
|
356
415
|
------------
|
357
416
|
|
358
417
|
Put this into your `Gemfile`:
|
359
|
-
|
360
|
-
|
418
|
+
```rb
|
419
|
+
gem 'assignable_values'
|
420
|
+
```
|
361
421
|
|
362
422
|
Now run `bundle install` and restart your server. Done.
|
363
423
|
|
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: 1.0.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:
|
11
|
+
date: 2024-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -90,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
90
|
- !ruby/object:Gem::Version
|
91
91
|
version: '0'
|
92
92
|
requirements: []
|
93
|
-
rubygems_version: 3.4.
|
93
|
+
rubygems_version: 3.4.13
|
94
94
|
signing_key:
|
95
95
|
specification_version: 4
|
96
96
|
summary: Restrict the values assignable to ActiveRecord attributes or associations
|