activemodel 5.2.5 → 6.0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +160 -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 = "1"
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