activemodel 5.2.5 → 6.0.4.6
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 +185 -71
- data/MIT-LICENSE +1 -1
- data/README.rdoc +5 -3
- data/lib/active_model/attribute/user_provided_default.rb +1 -2
- data/lib/active_model/attribute.rb +3 -4
- data/lib/active_model/attribute_assignment.rb +1 -2
- data/lib/active_model/attribute_methods.rb +56 -15
- data/lib/active_model/attribute_mutation_tracker.rb +88 -34
- data/lib/active_model/attribute_set/builder.rb +1 -3
- data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
- data/lib/active_model/attribute_set.rb +2 -12
- data/lib/active_model/attributes.rb +60 -35
- data/lib/active_model/callbacks.rb +10 -8
- data/lib/active_model/conversion.rb +1 -1
- data/lib/active_model/dirty.rb +36 -99
- data/lib/active_model/errors.rb +105 -21
- data/lib/active_model/gem_version.rb +4 -4
- data/lib/active_model/naming.rb +20 -5
- data/lib/active_model/railtie.rb +6 -0
- data/lib/active_model/secure_password.rb +47 -48
- data/lib/active_model/serialization.rb +0 -1
- data/lib/active_model/serializers/json.rb +10 -9
- data/lib/active_model/translation.rb +1 -1
- data/lib/active_model/type/big_integer.rb +0 -1
- data/lib/active_model/type/binary.rb +1 -1
- data/lib/active_model/type/boolean.rb +0 -1
- data/lib/active_model/type/date.rb +0 -5
- data/lib/active_model/type/date_time.rb +3 -8
- data/lib/active_model/type/decimal.rb +0 -1
- data/lib/active_model/type/float.rb +0 -3
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +4 -0
- data/lib/active_model/type/helpers/numeric.rb +9 -3
- data/lib/active_model/type/helpers/time_value.rb +17 -5
- data/lib/active_model/type/immutable_string.rb +0 -1
- data/lib/active_model/type/integer.rb +8 -20
- data/lib/active_model/type/registry.rb +11 -16
- data/lib/active_model/type/string.rb +2 -3
- data/lib/active_model/type/time.rb +1 -6
- data/lib/active_model/type/value.rb +0 -1
- data/lib/active_model/validations/absence.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +33 -26
- data/lib/active_model/validations/callbacks.rb +0 -1
- data/lib/active_model/validations/clusivity.rb +1 -2
- data/lib/active_model/validations/confirmation.rb +2 -2
- data/lib/active_model/validations/format.rb +1 -2
- data/lib/active_model/validations/inclusion.rb +1 -1
- data/lib/active_model/validations/length.rb +1 -1
- data/lib/active_model/validations/numericality.rb +5 -4
- data/lib/active_model/validations/validates.rb +2 -3
- data/lib/active_model/validations.rb +0 -3
- data/lib/active_model/validator.rb +1 -2
- data/lib/active_model.rb +1 -1
- metadata +15 -12
data/lib/active_model/dirty.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/hash_with_indifferent_access"
|
4
|
-
require "active_support/core_ext/object/duplicable"
|
5
3
|
require "active_model/attribute_mutation_tracker"
|
6
4
|
|
7
5
|
module ActiveModel
|
@@ -122,9 +120,6 @@ module ActiveModel
|
|
122
120
|
extend ActiveSupport::Concern
|
123
121
|
include ActiveModel::AttributeMethods
|
124
122
|
|
125
|
-
OPTION_NOT_GIVEN = Object.new # :nodoc:
|
126
|
-
private_constant :OPTION_NOT_GIVEN
|
127
|
-
|
128
123
|
included do
|
129
124
|
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
|
130
125
|
attribute_method_suffix "_previously_changed?", "_previous_change"
|
@@ -145,21 +140,20 @@ module ActiveModel
|
|
145
140
|
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
|
146
141
|
def changes_applied
|
147
142
|
unless defined?(@attributes)
|
148
|
-
|
143
|
+
mutations_from_database.finalize_changes
|
149
144
|
end
|
150
145
|
@mutations_before_last_save = mutations_from_database
|
151
|
-
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
152
146
|
forget_attribute_assignments
|
153
147
|
@mutations_from_database = nil
|
154
148
|
end
|
155
149
|
|
156
|
-
# Returns +true+ if any of the attributes
|
150
|
+
# Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
|
157
151
|
#
|
158
152
|
# person.changed? # => false
|
159
153
|
# person.name = 'bob'
|
160
154
|
# person.changed? # => true
|
161
155
|
def changed?
|
162
|
-
|
156
|
+
mutations_from_database.any_changes?
|
163
157
|
end
|
164
158
|
|
165
159
|
# Returns an array with the name of the attributes with unsaved changes.
|
@@ -168,42 +162,37 @@ module ActiveModel
|
|
168
162
|
# person.name = 'bob'
|
169
163
|
# person.changed # => ["name"]
|
170
164
|
def changed
|
171
|
-
|
165
|
+
mutations_from_database.changed_attribute_names
|
172
166
|
end
|
173
167
|
|
174
|
-
#
|
175
|
-
def attribute_changed?(
|
176
|
-
|
177
|
-
(to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
|
178
|
-
(from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
|
168
|
+
# Dispatch target for <tt>*_changed?</tt> attribute methods.
|
169
|
+
def attribute_changed?(attr_name, **options) # :nodoc:
|
170
|
+
mutations_from_database.changed?(attr_name.to_s, **options)
|
179
171
|
end
|
180
172
|
|
181
|
-
#
|
182
|
-
def attribute_was(
|
183
|
-
|
173
|
+
# Dispatch target for <tt>*_was</tt> attribute methods.
|
174
|
+
def attribute_was(attr_name) # :nodoc:
|
175
|
+
mutations_from_database.original_value(attr_name.to_s)
|
184
176
|
end
|
185
177
|
|
186
|
-
#
|
187
|
-
def attribute_previously_changed?(
|
188
|
-
|
178
|
+
# Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
|
179
|
+
def attribute_previously_changed?(attr_name) # :nodoc:
|
180
|
+
mutations_before_last_save.changed?(attr_name.to_s)
|
189
181
|
end
|
190
182
|
|
191
183
|
# Restore all previous data of the provided attributes.
|
192
|
-
def restore_attributes(
|
193
|
-
|
184
|
+
def restore_attributes(attr_names = changed)
|
185
|
+
attr_names.each { |attr_name| restore_attribute!(attr_name) }
|
194
186
|
end
|
195
187
|
|
196
188
|
# Clears all dirty data: current changes and previous changes.
|
197
189
|
def clear_changes_information
|
198
|
-
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
199
190
|
@mutations_before_last_save = nil
|
200
|
-
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
201
191
|
forget_attribute_assignments
|
202
192
|
@mutations_from_database = nil
|
203
193
|
end
|
204
194
|
|
205
195
|
def clear_attribute_changes(attr_names)
|
206
|
-
attributes_changed_by_setter.except!(*attr_names)
|
207
196
|
attr_names.each do |attr_name|
|
208
197
|
clear_attribute_change(attr_name)
|
209
198
|
end
|
@@ -216,13 +205,7 @@ module ActiveModel
|
|
216
205
|
# person.name = 'robert'
|
217
206
|
# person.changed_attributes # => {"name" => "bob"}
|
218
207
|
def changed_attributes
|
219
|
-
|
220
|
-
# multiple times when it is known that the computed value cannot change.
|
221
|
-
if defined?(@cached_changed_attributes)
|
222
|
-
@cached_changed_attributes
|
223
|
-
else
|
224
|
-
attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
|
225
|
-
end
|
208
|
+
mutations_from_database.changed_values
|
226
209
|
end
|
227
210
|
|
228
211
|
# Returns a hash of changed attributes indicating their original
|
@@ -232,9 +215,7 @@ module ActiveModel
|
|
232
215
|
# person.name = 'bob'
|
233
216
|
# person.changes # => { "name" => ["bill", "bob"] }
|
234
217
|
def changes
|
235
|
-
|
236
|
-
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
237
|
-
end
|
218
|
+
mutations_from_database.changes
|
238
219
|
end
|
239
220
|
|
240
221
|
# Returns a hash of attributes that were changed before the model was saved.
|
@@ -244,27 +225,23 @@ module ActiveModel
|
|
244
225
|
# person.save
|
245
226
|
# person.previous_changes # => {"name" => ["bob", "robert"]}
|
246
227
|
def previous_changes
|
247
|
-
|
248
|
-
@previously_changed.merge(mutations_before_last_save.changes)
|
228
|
+
mutations_before_last_save.changes
|
249
229
|
end
|
250
230
|
|
251
231
|
def attribute_changed_in_place?(attr_name) # :nodoc:
|
252
|
-
mutations_from_database.changed_in_place?(attr_name)
|
232
|
+
mutations_from_database.changed_in_place?(attr_name.to_s)
|
253
233
|
end
|
254
234
|
|
255
235
|
private
|
256
236
|
def clear_attribute_change(attr_name)
|
257
|
-
mutations_from_database.forget_change(attr_name)
|
237
|
+
mutations_from_database.forget_change(attr_name.to_s)
|
258
238
|
end
|
259
239
|
|
260
240
|
def mutations_from_database
|
261
|
-
unless defined?(@mutations_from_database)
|
262
|
-
@mutations_from_database = nil
|
263
|
-
end
|
264
241
|
@mutations_from_database ||= if defined?(@attributes)
|
265
242
|
ActiveModel::AttributeMutationTracker.new(@attributes)
|
266
243
|
else
|
267
|
-
|
244
|
+
ActiveModel::ForcedMutationTracker.new(self)
|
268
245
|
end
|
269
246
|
end
|
270
247
|
|
@@ -276,68 +253,28 @@ module ActiveModel
|
|
276
253
|
@mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
|
277
254
|
end
|
278
255
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
ensure
|
283
|
-
clear_changed_attributes_cache
|
284
|
-
end
|
285
|
-
|
286
|
-
def clear_changed_attributes_cache
|
287
|
-
remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
|
288
|
-
end
|
289
|
-
|
290
|
-
# Returns +true+ if attr_name is changed, +false+ otherwise.
|
291
|
-
def changes_include?(attr_name)
|
292
|
-
attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
|
293
|
-
end
|
294
|
-
alias attribute_changed_by_setter? changes_include?
|
295
|
-
|
296
|
-
# Returns +true+ if attr_name were changed before the model was saved,
|
297
|
-
# +false+ otherwise.
|
298
|
-
def previous_changes_include?(attr_name)
|
299
|
-
previous_changes.include?(attr_name)
|
300
|
-
end
|
301
|
-
|
302
|
-
# Handles <tt>*_change</tt> for +method_missing+.
|
303
|
-
def attribute_change(attr)
|
304
|
-
[changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
|
256
|
+
# Dispatch target for <tt>*_change</tt> attribute methods.
|
257
|
+
def attribute_change(attr_name)
|
258
|
+
mutations_from_database.change_to_attribute(attr_name.to_s)
|
305
259
|
end
|
306
260
|
|
307
|
-
#
|
308
|
-
def attribute_previous_change(
|
309
|
-
|
261
|
+
# Dispatch target for <tt>*_previous_change</tt> attribute methods.
|
262
|
+
def attribute_previous_change(attr_name)
|
263
|
+
mutations_before_last_save.change_to_attribute(attr_name.to_s)
|
310
264
|
end
|
311
265
|
|
312
|
-
#
|
313
|
-
def attribute_will_change!(
|
314
|
-
|
315
|
-
begin
|
316
|
-
value = _read_attribute(attr)
|
317
|
-
value = value.duplicable? ? value.clone : value
|
318
|
-
rescue TypeError, NoMethodError
|
319
|
-
end
|
320
|
-
|
321
|
-
set_attribute_was(attr, value)
|
322
|
-
end
|
323
|
-
mutations_from_database.force_change(attr)
|
266
|
+
# Dispatch target for <tt>*_will_change!</tt> attribute methods.
|
267
|
+
def attribute_will_change!(attr_name)
|
268
|
+
mutations_from_database.force_change(attr_name.to_s)
|
324
269
|
end
|
325
270
|
|
326
|
-
#
|
327
|
-
def restore_attribute!(
|
328
|
-
|
329
|
-
|
330
|
-
|
271
|
+
# Dispatch target for <tt>restore_*!</tt> attribute methods.
|
272
|
+
def restore_attribute!(attr_name)
|
273
|
+
attr_name = attr_name.to_s
|
274
|
+
if attribute_changed?(attr_name)
|
275
|
+
__send__("#{attr_name}=", attribute_was(attr_name))
|
276
|
+
clear_attribute_change(attr_name)
|
331
277
|
end
|
332
278
|
end
|
333
|
-
|
334
|
-
def attributes_changed_by_setter
|
335
|
-
@attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
|
336
|
-
end
|
337
|
-
|
338
|
-
# Force an attribute to have a particular "before" value
|
339
|
-
def set_attribute_was(attr, old_value)
|
340
|
-
attributes_changed_by_setter[attr] = old_value
|
341
|
-
end
|
342
279
|
end
|
343
280
|
end
|
data/lib/active_model/errors.rb
CHANGED
@@ -62,6 +62,11 @@ module ActiveModel
|
|
62
62
|
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
|
63
63
|
MESSAGE_OPTIONS = [:message]
|
64
64
|
|
65
|
+
class << self
|
66
|
+
attr_accessor :i18n_customize_full_message # :nodoc:
|
67
|
+
end
|
68
|
+
self.i18n_customize_full_message = false
|
69
|
+
|
65
70
|
attr_reader :messages, :details
|
66
71
|
|
67
72
|
# Pass in the instance of the object that is using the errors object.
|
@@ -107,6 +112,17 @@ module ActiveModel
|
|
107
112
|
@details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
|
108
113
|
end
|
109
114
|
|
115
|
+
# Removes all errors except the given keys. Returns a hash containing the removed errors.
|
116
|
+
#
|
117
|
+
# person.errors.keys # => [:name, :age, :gender, :city]
|
118
|
+
# person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
|
119
|
+
# person.errors.keys # => [:age, :gender]
|
120
|
+
def slice!(*keys)
|
121
|
+
keys = keys.map(&:to_sym)
|
122
|
+
@details.slice!(*keys)
|
123
|
+
@messages.slice!(*keys)
|
124
|
+
end
|
125
|
+
|
110
126
|
# Clear the error messages.
|
111
127
|
#
|
112
128
|
# person.errors.full_messages # => ["name cannot be nil"]
|
@@ -312,15 +328,15 @@ module ActiveModel
|
|
312
328
|
# person.errors.added? :name, :blank # => true
|
313
329
|
# person.errors.added? :name, "can't be blank" # => true
|
314
330
|
#
|
315
|
-
# If the error message requires
|
316
|
-
# the correct
|
331
|
+
# If the error message requires options, then it returns +true+ with
|
332
|
+
# the correct options, or +false+ with incorrect or missing options.
|
317
333
|
#
|
318
|
-
#
|
319
|
-
#
|
320
|
-
#
|
321
|
-
#
|
322
|
-
#
|
323
|
-
#
|
334
|
+
# person.errors.add :name, :too_long, { count: 25 }
|
335
|
+
# person.errors.added? :name, :too_long, count: 25 # => true
|
336
|
+
# person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
|
337
|
+
# person.errors.added? :name, :too_long, count: 24 # => false
|
338
|
+
# person.errors.added? :name, :too_long # => false
|
339
|
+
# person.errors.added? :name, "is too long" # => false
|
324
340
|
def added?(attribute, message = :invalid, options = {})
|
325
341
|
message = message.call if message.respond_to?(:call)
|
326
342
|
|
@@ -331,6 +347,27 @@ module ActiveModel
|
|
331
347
|
end
|
332
348
|
end
|
333
349
|
|
350
|
+
# Returns +true+ if an error on the attribute with the given message is
|
351
|
+
# present, or +false+ otherwise. +message+ is treated the same as for +add+.
|
352
|
+
#
|
353
|
+
# person.errors.add :age
|
354
|
+
# person.errors.add :name, :too_long, { count: 25 }
|
355
|
+
# person.errors.of_kind? :age # => true
|
356
|
+
# person.errors.of_kind? :name # => false
|
357
|
+
# person.errors.of_kind? :name, :too_long # => true
|
358
|
+
# person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
|
359
|
+
# person.errors.of_kind? :name, :not_too_long # => false
|
360
|
+
# person.errors.of_kind? :name, "is too long" # => false
|
361
|
+
def of_kind?(attribute, message = :invalid)
|
362
|
+
message = message.call if message.respond_to?(:call)
|
363
|
+
|
364
|
+
if message.is_a? Symbol
|
365
|
+
details[attribute.to_sym].map { |e| e[:error] }.include? message
|
366
|
+
else
|
367
|
+
self[attribute].include? message
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
334
371
|
# Returns all the full error messages in an array.
|
335
372
|
#
|
336
373
|
# class Person
|
@@ -364,12 +401,54 @@ module ActiveModel
|
|
364
401
|
# Returns a full message for a given attribute.
|
365
402
|
#
|
366
403
|
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
|
404
|
+
#
|
405
|
+
# The `"%{attribute} %{message}"` error format can be overridden with either
|
406
|
+
#
|
407
|
+
# * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt>
|
408
|
+
# * <tt>activemodel.errors.models.person/contacts/addresses.format</tt>
|
409
|
+
# * <tt>activemodel.errors.models.person.attributes.name.format</tt>
|
410
|
+
# * <tt>activemodel.errors.models.person.format</tt>
|
411
|
+
# * <tt>errors.format</tt>
|
367
412
|
def full_message(attribute, message)
|
368
413
|
return message if attribute == :base
|
369
|
-
|
414
|
+
attribute = attribute.to_s
|
415
|
+
|
416
|
+
if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
|
417
|
+
attribute = attribute.remove(/\[\d\]/)
|
418
|
+
parts = attribute.split(".")
|
419
|
+
attribute_name = parts.pop
|
420
|
+
namespace = parts.join("/") unless parts.empty?
|
421
|
+
attributes_scope = "#{@base.class.i18n_scope}.errors.models"
|
422
|
+
|
423
|
+
if namespace
|
424
|
+
defaults = @base.class.lookup_ancestors.map do |klass|
|
425
|
+
[
|
426
|
+
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
|
427
|
+
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
|
428
|
+
]
|
429
|
+
end
|
430
|
+
else
|
431
|
+
defaults = @base.class.lookup_ancestors.map do |klass|
|
432
|
+
[
|
433
|
+
:"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
|
434
|
+
:"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
|
435
|
+
]
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
defaults.flatten!
|
440
|
+
else
|
441
|
+
defaults = []
|
442
|
+
end
|
443
|
+
|
444
|
+
defaults << :"errors.format"
|
445
|
+
defaults << "%{attribute} %{message}"
|
446
|
+
|
447
|
+
attr_name = attribute.tr(".", "_").humanize
|
370
448
|
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
|
371
|
-
|
372
|
-
|
449
|
+
|
450
|
+
I18n.t(defaults.shift,
|
451
|
+
default: defaults,
|
373
452
|
attribute: attr_name,
|
374
453
|
message: message)
|
375
454
|
end
|
@@ -400,6 +479,14 @@ module ActiveModel
|
|
400
479
|
# * <tt>errors.messages.blank</tt>
|
401
480
|
def generate_message(attribute, type = :invalid, options = {})
|
402
481
|
type = options.delete(:message) if options[:message].is_a?(Symbol)
|
482
|
+
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
|
483
|
+
|
484
|
+
options = {
|
485
|
+
model: @base.model_name.human,
|
486
|
+
attribute: @base.class.human_attribute_name(attribute),
|
487
|
+
value: value,
|
488
|
+
object: @base
|
489
|
+
}.merge!(options)
|
403
490
|
|
404
491
|
if @base.class.respond_to?(:i18n_scope)
|
405
492
|
i18n_scope = @base.class.i18n_scope.to_s
|
@@ -408,6 +495,11 @@ module ActiveModel
|
|
408
495
|
:"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
|
409
496
|
end
|
410
497
|
defaults << :"#{i18n_scope}.errors.messages.#{type}"
|
498
|
+
|
499
|
+
catch(:exception) do
|
500
|
+
translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
|
501
|
+
return translation unless translation.nil?
|
502
|
+
end unless options[:message]
|
411
503
|
else
|
412
504
|
defaults = []
|
413
505
|
end
|
@@ -417,17 +509,9 @@ module ActiveModel
|
|
417
509
|
|
418
510
|
key = defaults.shift
|
419
511
|
defaults = options.delete(:message) if options[:message]
|
420
|
-
|
421
|
-
|
422
|
-
options = {
|
423
|
-
default: defaults,
|
424
|
-
model: @base.model_name.human,
|
425
|
-
attribute: @base.class.human_attribute_name(attribute),
|
426
|
-
value: value,
|
427
|
-
object: @base
|
428
|
-
}.merge!(options)
|
512
|
+
options[:default] = defaults
|
429
513
|
|
430
|
-
I18n.translate(key, options)
|
514
|
+
I18n.translate(key, **options)
|
431
515
|
end
|
432
516
|
|
433
517
|
def marshal_dump # :nodoc:
|
data/lib/active_model/naming.rb
CHANGED
@@ -110,6 +110,22 @@ module ActiveModel
|
|
110
110
|
# BlogPost.model_name.eql?('BlogPost') # => true
|
111
111
|
# BlogPost.model_name.eql?('Blog Post') # => false
|
112
112
|
|
113
|
+
##
|
114
|
+
# :method: match?
|
115
|
+
#
|
116
|
+
# :call-seq:
|
117
|
+
# match?(regexp)
|
118
|
+
#
|
119
|
+
# Equivalent to <tt>String#match?</tt>. Match the class name against the
|
120
|
+
# given regexp. Returns +true+ if there is a match, otherwise +false+.
|
121
|
+
#
|
122
|
+
# class BlogPost
|
123
|
+
# extend ActiveModel::Naming
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# BlogPost.model_name.match?(/Post/) # => true
|
127
|
+
# BlogPost.model_name.match?(/\d/) # => false
|
128
|
+
|
113
129
|
##
|
114
130
|
# :method: to_s
|
115
131
|
#
|
@@ -131,7 +147,7 @@ module ActiveModel
|
|
131
147
|
# to_str()
|
132
148
|
#
|
133
149
|
# Equivalent to +to_s+.
|
134
|
-
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
|
150
|
+
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
|
135
151
|
:to_str, :as_json, to: :name
|
136
152
|
|
137
153
|
# Returns a new ActiveModel::Name instance. By default, the +namespace+
|
@@ -187,13 +203,12 @@ module ActiveModel
|
|
187
203
|
defaults << @human
|
188
204
|
|
189
205
|
options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
|
190
|
-
I18n.translate(defaults.shift, options)
|
206
|
+
I18n.translate(defaults.shift, **options)
|
191
207
|
end
|
192
208
|
|
193
209
|
private
|
194
|
-
|
195
210
|
def _singularize(string)
|
196
|
-
ActiveSupport::Inflector.underscore(string).tr("/"
|
211
|
+
ActiveSupport::Inflector.underscore(string).tr("/", "_")
|
197
212
|
end
|
198
213
|
end
|
199
214
|
|
@@ -236,7 +251,7 @@ module ActiveModel
|
|
236
251
|
# Person.model_name.plural # => "people"
|
237
252
|
def model_name
|
238
253
|
@_model_name ||= begin
|
239
|
-
namespace =
|
254
|
+
namespace = module_parents.detect do |n|
|
240
255
|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
241
256
|
end
|
242
257
|
ActiveModel::Name.new(self, namespace)
|
data/lib/active_model/railtie.rb
CHANGED
@@ -7,8 +7,14 @@ module ActiveModel
|
|
7
7
|
class Railtie < Rails::Railtie # :nodoc:
|
8
8
|
config.eager_load_namespaces << ActiveModel
|
9
9
|
|
10
|
+
config.active_model = ActiveSupport::OrderedOptions.new
|
11
|
+
|
10
12
|
initializer "active_model.secure_password" do
|
11
13
|
ActiveModel::SecurePassword.min_cost = Rails.env.test?
|
12
14
|
end
|
15
|
+
|
16
|
+
initializer "active_model.i18n_customize_full_message" do
|
17
|
+
ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
|
18
|
+
end
|
13
19
|
end
|
14
20
|
end
|
@@ -16,15 +16,16 @@ module ActiveModel
|
|
16
16
|
|
17
17
|
module ClassMethods
|
18
18
|
# Adds methods to set and authenticate against a BCrypt password.
|
19
|
-
# This mechanism requires you to have a +
|
19
|
+
# This mechanism requires you to have a +XXX_digest+ attribute.
|
20
|
+
# Where +XXX+ is the attribute name of your desired password.
|
20
21
|
#
|
21
22
|
# The following validations are added automatically:
|
22
23
|
# * Password must be present on creation
|
23
24
|
# * Password length should be less than or equal to 72 bytes
|
24
|
-
# * Confirmation of password (using a +
|
25
|
+
# * Confirmation of password (using a +XXX_confirmation+ attribute)
|
25
26
|
#
|
26
|
-
# If
|
27
|
-
# value for +
|
27
|
+
# If confirmation validation is not needed, simply leave out the
|
28
|
+
# value for +XXX_confirmation+ (i.e. don't provide a form field for
|
28
29
|
# it). When this attribute has a +nil+ value, the validation will not be
|
29
30
|
# triggered.
|
30
31
|
#
|
@@ -37,9 +38,10 @@ module ActiveModel
|
|
37
38
|
#
|
38
39
|
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
39
40
|
#
|
40
|
-
# # Schema: User(name:string, password_digest:string)
|
41
|
+
# # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
|
41
42
|
# class User < ActiveRecord::Base
|
42
43
|
# has_secure_password
|
44
|
+
# has_secure_password :recovery_password, validations: false
|
43
45
|
# end
|
44
46
|
#
|
45
47
|
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
|
@@ -48,11 +50,15 @@ module ActiveModel
|
|
48
50
|
# user.save # => false, confirmation doesn't match
|
49
51
|
# user.password_confirmation = 'mUc3m00RsqyRe'
|
50
52
|
# user.save # => true
|
53
|
+
# user.recovery_password = "42password"
|
54
|
+
# user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
|
55
|
+
# user.save # => true
|
51
56
|
# user.authenticate('notright') # => false
|
52
57
|
# user.authenticate('mUc3m00RsqyRe') # => user
|
58
|
+
# user.authenticate_recovery_password('42password') # => user
|
53
59
|
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
|
54
60
|
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
|
55
|
-
def has_secure_password(
|
61
|
+
def has_secure_password(attribute = :password, validations: true)
|
56
62
|
# Load bcrypt gem only when has_secure_password is used.
|
57
63
|
# This is to avoid ActiveModel (and by extension the entire framework)
|
58
64
|
# being dependent on a binary library.
|
@@ -63,9 +69,9 @@ module ActiveModel
|
|
63
69
|
raise
|
64
70
|
end
|
65
71
|
|
66
|
-
include InstanceMethodsOnActivation
|
72
|
+
include InstanceMethodsOnActivation.new(attribute)
|
67
73
|
|
68
|
-
if
|
74
|
+
if validations
|
69
75
|
include ActiveModel::Validations
|
70
76
|
|
71
77
|
# This ensures the model has a password by checking whether the password_digest
|
@@ -73,56 +79,49 @@ module ActiveModel
|
|
73
79
|
# when there is an error, the message is added to the password attribute instead
|
74
80
|
# so that the error message will make sense to the end-user.
|
75
81
|
validate do |record|
|
76
|
-
record.errors.add(
|
82
|
+
record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
|
77
83
|
end
|
78
84
|
|
79
|
-
validates_length_of
|
80
|
-
validates_confirmation_of
|
85
|
+
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
86
|
+
validates_confirmation_of attribute, allow_blank: true
|
81
87
|
end
|
82
88
|
end
|
83
89
|
end
|
84
90
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
end
|
91
|
+
class InstanceMethodsOnActivation < Module
|
92
|
+
def initialize(attribute)
|
93
|
+
attr_reader attribute
|
94
|
+
|
95
|
+
define_method("#{attribute}=") do |unencrypted_password|
|
96
|
+
if unencrypted_password.nil?
|
97
|
+
self.send("#{attribute}_digest=", nil)
|
98
|
+
elsif !unencrypted_password.empty?
|
99
|
+
instance_variable_set("@#{attribute}", unencrypted_password)
|
100
|
+
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
101
|
+
self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
|
102
|
+
end
|
103
|
+
end
|
99
104
|
|
100
|
-
|
105
|
+
define_method("#{attribute}_confirmation=") do |unencrypted_password|
|
106
|
+
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
|
107
|
+
end
|
101
108
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
if unencrypted_password.nil?
|
116
|
-
self.password_digest = nil
|
117
|
-
elsif !unencrypted_password.empty?
|
118
|
-
@password = unencrypted_password
|
119
|
-
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
120
|
-
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
|
109
|
+
# Returns +self+ if the password is correct, otherwise +false+.
|
110
|
+
#
|
111
|
+
# class User < ActiveRecord::Base
|
112
|
+
# has_secure_password validations: false
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
116
|
+
# user.save
|
117
|
+
# user.authenticate_password('notright') # => false
|
118
|
+
# user.authenticate_password('mUc3m00RsqyRe') # => user
|
119
|
+
define_method("authenticate_#{attribute}") do |unencrypted_password|
|
120
|
+
attribute_digest = send("#{attribute}_digest")
|
121
|
+
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
|
121
122
|
end
|
122
|
-
end
|
123
123
|
|
124
|
-
|
125
|
-
@password_confirmation = unencrypted_password
|
124
|
+
alias_method :authenticate, :authenticate_password if attribute == :password
|
126
125
|
end
|
127
126
|
end
|
128
127
|
end
|
@@ -150,7 +150,6 @@ module ActiveModel
|
|
150
150
|
end
|
151
151
|
|
152
152
|
private
|
153
|
-
|
154
153
|
# Hook method defining how an attribute value should be retrieved for
|
155
154
|
# serialization. By default this is assumed to be an instance named after
|
156
155
|
# the attribute. Override this method in subclasses should you need to
|