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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -71
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -3
  5. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  6. data/lib/active_model/attribute.rb +3 -4
  7. data/lib/active_model/attribute_assignment.rb +1 -2
  8. data/lib/active_model/attribute_methods.rb +56 -15
  9. data/lib/active_model/attribute_mutation_tracker.rb +88 -34
  10. data/lib/active_model/attribute_set/builder.rb +1 -3
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  12. data/lib/active_model/attribute_set.rb +2 -12
  13. data/lib/active_model/attributes.rb +60 -35
  14. data/lib/active_model/callbacks.rb +10 -8
  15. data/lib/active_model/conversion.rb +1 -1
  16. data/lib/active_model/dirty.rb +36 -99
  17. data/lib/active_model/errors.rb +105 -21
  18. data/lib/active_model/gem_version.rb +4 -4
  19. data/lib/active_model/naming.rb +20 -5
  20. data/lib/active_model/railtie.rb +6 -0
  21. data/lib/active_model/secure_password.rb +47 -48
  22. data/lib/active_model/serialization.rb +0 -1
  23. data/lib/active_model/serializers/json.rb +10 -9
  24. data/lib/active_model/translation.rb +1 -1
  25. data/lib/active_model/type/big_integer.rb +0 -1
  26. data/lib/active_model/type/binary.rb +1 -1
  27. data/lib/active_model/type/boolean.rb +0 -1
  28. data/lib/active_model/type/date.rb +0 -5
  29. data/lib/active_model/type/date_time.rb +3 -8
  30. data/lib/active_model/type/decimal.rb +0 -1
  31. data/lib/active_model/type/float.rb +0 -3
  32. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +4 -0
  33. data/lib/active_model/type/helpers/numeric.rb +9 -3
  34. data/lib/active_model/type/helpers/time_value.rb +17 -5
  35. data/lib/active_model/type/immutable_string.rb +0 -1
  36. data/lib/active_model/type/integer.rb +8 -20
  37. data/lib/active_model/type/registry.rb +11 -16
  38. data/lib/active_model/type/string.rb +2 -3
  39. data/lib/active_model/type/time.rb +1 -6
  40. data/lib/active_model/type/value.rb +0 -1
  41. data/lib/active_model/validations/absence.rb +1 -1
  42. data/lib/active_model/validations/acceptance.rb +33 -26
  43. data/lib/active_model/validations/callbacks.rb +0 -1
  44. data/lib/active_model/validations/clusivity.rb +1 -2
  45. data/lib/active_model/validations/confirmation.rb +2 -2
  46. data/lib/active_model/validations/format.rb +1 -2
  47. data/lib/active_model/validations/inclusion.rb +1 -1
  48. data/lib/active_model/validations/length.rb +1 -1
  49. data/lib/active_model/validations/numericality.rb +5 -4
  50. data/lib/active_model/validations/validates.rb +2 -3
  51. data/lib/active_model/validations.rb +0 -3
  52. data/lib/active_model/validator.rb +1 -2
  53. data/lib/active_model.rb +1 -1
  54. metadata +15 -12
@@ -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
- @previously_changed = changes
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 have unsaved changes, +false+ otherwise.
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
- changed_attributes.present?
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
- changed_attributes.keys
165
+ mutations_from_database.changed_attribute_names
172
166
  end
173
167
 
174
- # Handles <tt>*_changed?</tt> for +method_missing+.
175
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
176
- !!changes_include?(attr) &&
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
- # Handles <tt>*_was</tt> for +method_missing+.
182
- def attribute_was(attr) # :nodoc:
183
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
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
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
187
- def attribute_previously_changed?(attr) #:nodoc:
188
- previous_changes_include?(attr)
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(attributes = changed)
193
- attributes.each { |attr| restore_attribute! attr }
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
- # This should only be set by methods which will call changed_attributes
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
- cache_changed_attributes do
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
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
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
- NullMutationTracker.instance
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
- def cache_changed_attributes
280
- @cached_changed_attributes = changed_attributes
281
- yield
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
- # Handles <tt>*_previous_change</tt> for +method_missing+.
308
- def attribute_previous_change(attr)
309
- previous_changes[attr] if attribute_previously_changed?(attr)
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
- # Handles <tt>*_will_change!</tt> for +method_missing+.
313
- def attribute_will_change!(attr)
314
- unless attribute_changed?(attr)
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
- # Handles <tt>restore_*!</tt> for +method_missing+.
327
- def restore_attribute!(attr)
328
- if attribute_changed?(attr)
329
- __send__("#{attr}=", changed_attributes[attr])
330
- clear_attribute_changes([attr])
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
@@ -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 an option, then it returns +true+ with
316
- # the correct option, or +false+ with an incorrect or missing option.
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
- # person.errors.add :name, :too_long, { count: 25 }
319
- # person.errors.added? :name, :too_long, count: 25 # => true
320
- # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
321
- # person.errors.added? :name, :too_long, count: 24 # => false
322
- # person.errors.added? :name, :too_long # => false
323
- # person.errors.added? :name, "is too long" # => false
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
- attr_name = attribute.to_s.tr(".", "_").humanize
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
- I18n.t(:"errors.format",
372
- default: "%{attribute} %{message}",
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
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
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:
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 5
13
- PRE = nil
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 4
13
+ PRE = "6"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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("/".freeze, "_".freeze)
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 = parents.detect do |n|
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)
@@ -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 +password_digest+ attribute.
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 +password_confirmation+ attribute)
25
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
25
26
  #
26
- # If password confirmation validation is not needed, simply leave out the
27
- # value for +password_confirmation+ (i.e. don't provide a form field 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(options = {})
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 options.fetch(:validations, true)
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(:password, :blank) unless record.password_digest.present?
82
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
77
83
  end
78
84
 
79
- validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
80
- validates_confirmation_of :password, allow_blank: true
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
- module InstanceMethodsOnActivation
86
- # Returns +self+ if the password is correct, otherwise +false+.
87
- #
88
- # class User < ActiveRecord::Base
89
- # has_secure_password validations: false
90
- # end
91
- #
92
- # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
93
- # user.save
94
- # user.authenticate('notright') # => false
95
- # user.authenticate('mUc3m00RsqyRe') # => user
96
- def authenticate(unencrypted_password)
97
- BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
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
- attr_reader :password
105
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
106
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
107
+ end
101
108
 
102
- # Encrypts the password into the +password_digest+ attribute, only if the
103
- # new password is not empty.
104
- #
105
- # class User < ActiveRecord::Base
106
- # has_secure_password validations: false
107
- # end
108
- #
109
- # user = User.new
110
- # user.password = nil
111
- # user.password_digest # => nil
112
- # user.password = 'mUc3m00RsqyRe'
113
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
114
- def password=(unencrypted_password)
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
- def password_confirmation=(unencrypted_password)
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