activemodel 7.2.2.2 → 8.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7c26617196a4f0ddac0af557544df70b9e31a5c3da316af9b0e3b347687a666
4
- data.tar.gz: fea728acd46ac5fb02e46ea32d298b19ab8eb8de03505c17f6e56574536e8fe4
3
+ metadata.gz: 16b040fd5a62c0392cc172673913fcb87cdb1aa6314bc12a3b54c4b50a20ed94
4
+ data.tar.gz: d7e329b88a5c375b5e65c03e30b5469996b966ec3a6e8f15492c3c2f099205a0
5
5
  SHA512:
6
- metadata.gz: 2d7fe1b0ecddd5b8cf281a66d7061cbf83e69df809c89d6b7df93d4d3413b78cf102ca8e8dc8ae9b6fdccf509acd48cdb877156f2c7e142bf6e31802149d81d4
7
- data.tar.gz: 8ddbd1c97773e65c014aafeb240ec6b4407f2edc55d1773248cf396fdcf75409274f994898aec55057eca91dec015c6e14d938a341258071e8c68c0ca765a21e
6
+ metadata.gz: '04859dbfdb1a4e16f0e4d391ca49a149f588926e19b4175cd0338f047a98b88df2edca6eef049098e268a12f494877294441fb748d52ed576e9ab65f6ef64dc3'
7
+ data.tar.gz: 17a3fd0fd7d386aa3997bfdf247b0a386ec00b879fc0922422b82c25675cf021092f44e8c32bc142fc57db01c97524123d3cca6e7ddd8718a6cc747d7e27c6f9
data/CHANGELOG.md CHANGED
@@ -1,71 +1,129 @@
1
- ## Rails 7.2.2.2 (August 13, 2025) ##
1
+ ## Rails 8.0.3 (September 22, 2025) ##
2
+
3
+ * Fix `has_secure_password` to perform confirmation validation of the password even when blank.
4
+
5
+ The validation was incorrectly skipped when the password only contained whitespace characters.
6
+
7
+ *Fabio Sangiovanni*
8
+
9
+
10
+ ## Rails 8.0.2.1 (August 13, 2025) ##
2
11
 
3
12
  * No changes.
4
13
 
5
14
 
6
- ## Rails 7.2.2.1 (December 10, 2024) ##
15
+ ## Rails 8.0.2 (March 12, 2025) ##
7
16
 
8
17
  * No changes.
9
18
 
10
19
 
11
- ## Rails 7.2.2 (October 30, 2024) ##
20
+ ## Rails 8.0.1 (December 13, 2024) ##
12
21
 
13
- * Fix regression in `alias_attribute` to work with user defined methods.
22
+ * No changes.
14
23
 
15
- `alias_attribute` would wrongly assume the attribute accessor was generated by Active Model.
16
24
 
17
- ```ruby
18
- class Person
19
- include ActiveModel::AttributeMethods
25
+ ## Rails 8.0.0.1 (December 10, 2024) ##
26
+
27
+ * No changes.
28
+
29
+
30
+ ## Rails 8.0.0 (November 07, 2024) ##
31
+
32
+ * No changes.
33
+
34
+
35
+ ## Rails 8.0.0.rc2 (October 30, 2024) ##
36
+
37
+ * No changes.
20
38
 
21
- define_attribute_methods :name
22
- attr_accessor :name
23
39
 
24
- alias_attribute :full_name, :name
40
+ ## Rails 8.0.0.rc1 (October 19, 2024) ##
41
+
42
+ * Add `:except_on` option for validations. Grants the ability to _skip_ validations in specified contexts.
43
+
44
+ ```ruby
45
+ class User < ApplicationRecord
46
+ #...
47
+ validates :birthday, presence: { except_on: :admin }
48
+ #...
25
49
  end
26
50
 
27
- person.full_name # => NoMethodError: undefined method `attribute' for an instance of Person
51
+ user = User.new(attributes except birthday)
52
+ user.save(context: :admin)
28
53
  ```
29
54
 
30
- *Jean Boussier*
55
+ *Drew Bragg*
31
56
 
57
+ ## Rails 8.0.0.beta1 (September 26, 2024) ##
32
58
 
33
- ## Rails 7.2.1.2 (October 23, 2024) ##
59
+ * Make `ActiveModel::Serialization#read_attribute_for_serialization` public
34
60
 
35
- * No changes.
61
+ *Sean Doyle*
36
62
 
63
+ * Add a default token generator for password reset tokens when using `has_secure_password`.
37
64
 
38
- ## Rails 7.2.1.1 (October 15, 2024) ##
65
+ ```ruby
66
+ class User < ApplicationRecord
67
+ has_secure_password
68
+ end
39
69
 
40
- * No changes.
70
+ user = User.create!(name: "david", password: "123", password_confirmation: "123")
71
+ token = user.password_reset_token
72
+ User.find_by_password_reset_token(token) # returns user
41
73
 
74
+ # 16 minutes later...
75
+ User.find_by_password_reset_token(token) # returns nil
42
76
 
43
- ## Rails 7.2.1 (August 22, 2024) ##
77
+ # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
78
+ User.find_by_password_reset_token!(token)
79
+ ```
44
80
 
45
- * No changes.
81
+ *DHH*
46
82
 
83
+ * Add a load hook `active_model_translation` for `ActiveModel::Translation`.
47
84
 
48
- ## Rails 7.2.0 (August 09, 2024) ##
85
+ *Shouichi Kamiya*
49
86
 
50
- * Fix a bug where type casting of string to `Time` and `DateTime` doesn't
51
- calculate minus minute value in TZ offset correctly.
87
+ * Add `raise_on_missing_translations` option to `ActiveModel::Translation`.
88
+ When the option is set, `human_attribute_name` raises an error if a translation of the given attribute is missing.
52
89
 
53
- *Akira Matsuda*
90
+ ```ruby
91
+ # ActiveModel::Translation.raise_on_missing_translations = false
92
+ Post.human_attribute_name("title")
93
+ => "Title"
94
+
95
+ # ActiveModel::Translation.raise_on_missing_translations = true
96
+ Post.human_attribute_name("title")
97
+ => Translation missing. Options considered were: (I18n::MissingTranslationData)
98
+ - en.activerecord.attributes.post.title
99
+ - en.attributes.title
100
+
101
+ raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
102
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
103
+ ```
104
+
105
+ *Shouichi Kamiya*
106
+
107
+ * Introduce `ActiveModel::AttributeAssignment#attribute_writer_missing`
54
108
 
55
- * Port the `type_for_attribute` method to Active Model. Classes that include
56
- `ActiveModel::Attributes` will now provide this method. This method behaves
57
- the same for Active Model as it does for Active Record.
109
+ Provide instances with an opportunity to gracefully handle assigning to an
110
+ unknown attribute:
58
111
 
59
- ```ruby
60
- class MyModel
61
- include ActiveModel::Attributes
112
+ ```ruby
113
+ class Rectangle
114
+ include ActiveModel::AttributeAssignment
115
+
116
+ attr_accessor :length, :width
62
117
 
63
- attribute :my_attribute, :integer
118
+ def attribute_writer_missing(name, value)
119
+ Rails.logger.warn "Tried to assign to unknown attribute #{name}"
64
120
  end
121
+ end
65
122
 
66
- MyModel.type_for_attribute(:my_attribute) # => #<ActiveModel::Type::Integer ...>
67
- ```
123
+ rectangle = Rectangle.new
124
+ rectangle.assign_attributes(height: 10) # => Logs "Tried to assign to unknown attribute 'height'"
125
+ ```
68
126
 
69
- *Jonathan Hefner*
127
+ *Sean Doyle*
70
128
 
71
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activemodel/CHANGELOG.md) for previous changes.
129
+ Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activemodel/CHANGELOG.md) for previous changes.
data/README.rdoc CHANGED
@@ -261,6 +261,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
261
261
 
262
262
  * https://github.com/rails/rails/issues
263
263
 
264
- Feature requests should be discussed on the rails-core mailing list here:
264
+ Feature requests should be discussed on the rubyonrails-core forum here:
265
265
 
266
266
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -38,7 +38,7 @@ module ActiveModel
38
38
  @value = value unless value.nil?
39
39
  end
40
40
 
41
- def value
41
+ def value(&_)
42
42
  # `defined?` is cheaper than `||=` when we get back falsy values
43
43
  @value = type_cast(value_before_type_cast) unless defined?(@value)
44
44
  @value
@@ -36,9 +36,30 @@ module ActiveModel
36
36
 
37
37
  alias attributes= assign_attributes
38
38
 
39
+ # Like `BasicObject#method_missing`, `#attribute_writer_missing` is invoked
40
+ # when `#assign_attributes` is passed an unknown attribute name.
41
+ #
42
+ # By default, `#attribute_writer_missing` raises an UnknownAttributeError.
43
+ #
44
+ # class Rectangle
45
+ # include ActiveModel::AttributeAssignment
46
+ #
47
+ # attr_accessor :length, :width
48
+ #
49
+ # def attribute_writer_missing(name, value)
50
+ # Rails.logger.warn "Tried to assign to unknown attribute #{name}"
51
+ # end
52
+ # end
53
+ #
54
+ # rectangle = Rectangle.new
55
+ # rectangle.assign_attributes(height: 10) # => Logs "Tried to assign to unknown attribute 'height'"
56
+ def attribute_writer_missing(name, value)
57
+ raise UnknownAttributeError.new(self, name)
58
+ end
59
+
39
60
  private
40
61
  def _assign_attributes(attributes)
41
- attributes.each do |k, v|
62
+ attributes.each_pair do |k, v|
42
63
  _assign_attribute(k, v)
43
64
  end
44
65
  end
@@ -50,7 +71,7 @@ module ActiveModel
50
71
  if respond_to?(setter)
51
72
  raise
52
73
  else
53
- raise UnknownAttributeError.new(self, k.to_s)
74
+ attribute_writer_missing(k.to_s, v)
54
75
  end
55
76
  end
56
77
  end
@@ -214,7 +214,7 @@ module ActiveModel
214
214
  end
215
215
  end
216
216
 
217
- def generate_alias_attribute_methods(code_generator, new_name, old_name)
217
+ def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc:
218
218
  ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
219
219
  attribute_method_patterns.each do |pattern|
220
220
  alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
@@ -321,8 +321,8 @@ module ActiveModel
321
321
  canonical_method_name = pattern.method_name(attr_name)
322
322
  public_method_name = pattern.method_name(as)
323
323
 
324
- # If defining a regular attribute method, we don't override methods that are explictly
325
- # defined in parrent classes.
324
+ # If defining a regular attribute method, we don't override methods that are explicitly
325
+ # defined in parent classes.
326
326
  if instance_method_already_implemented?(public_method_name)
327
327
  # However, for `alias_attribute`, we always define the method.
328
328
  # We check for override second because `instance_method_already_implemented?`
@@ -71,7 +71,7 @@ module ActiveModel
71
71
  end
72
72
 
73
73
  def deep_dup
74
- AttributeSet.new(attributes.deep_dup)
74
+ AttributeSet.new(attributes.transform_values(&:deep_dup))
75
75
  end
76
76
 
77
77
  def initialize_dup(_)
@@ -3,7 +3,7 @@
3
3
  module ActiveModel
4
4
  # = Active \Model \Conversion
5
5
  #
6
- # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
6
+ # Handles default conversions: #to_model, #to_key, #to_param, and #to_partial_path.
7
7
  #
8
8
  # Let's take for example this non-persisted object.
9
9
  #
@@ -108,7 +108,7 @@ module ActiveModel
108
108
  # person.changes # => {"name" => ["Bill", "Bob"]}
109
109
  #
110
110
  # If an attribute is modified in-place then make use of
111
- # {*_will_change!}[rdoc-label:method-i-2A_will_change-21] to mark that the attribute is changing.
111
+ # {*_will_change!}[rdoc-ref:#*_will_change!] to mark that the attribute is changing.
112
112
  # Otherwise \Active \Model can't track changes to in-place attributes. Note
113
113
  # that Active Record can detect in-place modifications automatically. You do
114
114
  # not need to call <tt>*_will_change!</tt> on Active Record models.
@@ -247,16 +247,23 @@ module ActiveModel
247
247
 
248
248
  def initialize_dup(other) # :nodoc:
249
249
  super
250
- if self.class.respond_to?(:_default_attributes)
251
- @attributes = self.class._default_attributes.map do |attr|
252
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
250
+ @mutations_from_database = nil
251
+ end
252
+
253
+ def init_attributes(other) # :nodoc:
254
+ attrs = super
255
+ if other.persisted? && self.class.respond_to?(:_default_attributes)
256
+ self.class._default_attributes.map do |attr|
257
+ attr.with_value_from_user(attrs.fetch_value(attr.name))
253
258
  end
259
+ else
260
+ attrs
254
261
  end
255
- @mutations_from_database = nil
256
262
  end
257
263
 
258
264
  def as_json(options = {}) # :nodoc:
259
- options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
265
+ except = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
266
+ options = options.merge except: except
260
267
  super(options)
261
268
  end
262
269
 
@@ -289,22 +296,22 @@ module ActiveModel
289
296
  mutations_from_database.changed_attribute_names
290
297
  end
291
298
 
292
- # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
299
+ # Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
293
300
  def attribute_changed?(attr_name, **options)
294
301
  mutations_from_database.changed?(attr_name.to_s, **options)
295
302
  end
296
303
 
297
- # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
304
+ # Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
298
305
  def attribute_was(attr_name)
299
306
  mutations_from_database.original_value(attr_name.to_s)
300
307
  end
301
308
 
302
- # Dispatch target for {*_previously_changed?}[rdoc-label:method-i-2A_previously_changed-3F] attribute methods.
309
+ # Dispatch target for {*_previously_changed?}[rdoc-ref:#*_previously_changed?] attribute methods.
303
310
  def attribute_previously_changed?(attr_name, **options)
304
311
  mutations_before_last_save.changed?(attr_name.to_s, **options)
305
312
  end
306
313
 
307
- # Dispatch target for {*_previously_was}[rdoc-label:method-i-2A_previously_was] attribute methods.
314
+ # Dispatch target for {*_previously_was}[rdoc-ref:#*_previously_was] attribute methods.
308
315
  def attribute_previously_was(attr_name)
309
316
  mutations_before_last_save.original_value(attr_name.to_s)
310
317
  end
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 2
12
- TINY = 2
13
- PRE = "2"
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 3
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -54,7 +54,7 @@ module ActiveModel
54
54
  #
55
55
  # person = Person.new(id: 1, name: "bob")
56
56
  # person.slice(:id, :name)
57
- # => { "id" => 1, "name" => "bob" }
57
+ # # => { "id" => 1, "name" => "bob" }
58
58
  #
59
59
  #--
60
60
  # Implemented by ActiveModel::Access#slice.
@@ -68,7 +68,7 @@ module ActiveModel
68
68
  #
69
69
  # person = Person.new(id: 1, name: "bob")
70
70
  # person.values_at(:id, :name)
71
- # => [1, "bob"]
71
+ # # => [1, "bob"]
72
72
  #
73
73
  #--
74
74
  # Implemented by ActiveModel::Access#values_at.
@@ -39,6 +39,10 @@ module ActiveModel
39
39
  # <tt>validations: false</tt> as an argument. This allows complete
40
40
  # customizability of validation behavior.
41
41
  #
42
+ # Finally, a password reset token that's valid for 15 minutes after issue
43
+ # is automatically configured when +reset_token+ is set to true (which it is by default)
44
+ # and the object responds to +generates_token_for+ (which Active Records do).
45
+ #
42
46
  # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
43
47
  #
44
48
  # gem "bcrypt", "~> 3.1.7"
@@ -98,7 +102,18 @@ module ActiveModel
98
102
  # account.is_guest = true
99
103
  # account.valid? # => true
100
104
  #
101
- def has_secure_password(attribute = :password, validations: true)
105
+ # ===== Using the password reset token
106
+ #
107
+ # user = User.create!(name: "david", password: "123", password_confirmation: "123")
108
+ # token = user.password_reset_token
109
+ # User.find_by_password_reset_token(token) # returns user
110
+ #
111
+ # # 16 minutes later...
112
+ # User.find_by_password_reset_token(token) # returns nil
113
+ #
114
+ # # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
115
+ # User.find_by_password_reset_token!(token)
116
+ def has_secure_password(attribute = :password, validations: true, reset_token: true)
102
117
  # Load bcrypt gem only when has_secure_password is used.
103
118
  # This is to avoid ActiveModel (and by extension the entire framework)
104
119
  # being dependent on a binary library.
@@ -109,7 +124,7 @@ module ActiveModel
109
124
  raise
110
125
  end
111
126
 
112
- include InstanceMethodsOnActivation.new(attribute)
127
+ include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token)
113
128
 
114
129
  if validations
115
130
  include ActiveModel::Validations
@@ -140,13 +155,32 @@ module ActiveModel
140
155
  end
141
156
  end
142
157
 
143
- validates_confirmation_of attribute, allow_blank: true
158
+ validates_confirmation_of attribute, allow_nil: true
159
+ end
160
+
161
+ # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
162
+ if reset_token && respond_to?(:generates_token_for)
163
+ generates_token_for :"#{attribute}_reset", expires_in: 15.minutes do
164
+ public_send(:"#{attribute}_salt")&.last(10)
165
+ end
166
+
167
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
168
+ silence_redefinition_of_method :find_by_#{attribute}_reset_token
169
+ def self.find_by_#{attribute}_reset_token(token)
170
+ find_by_token_for(:#{attribute}_reset, token)
171
+ end
172
+
173
+ silence_redefinition_of_method :find_by_#{attribute}_reset_token!
174
+ def self.find_by_#{attribute}_reset_token!(token)
175
+ find_by_token_for!(:#{attribute}_reset, token)
176
+ end
177
+ RUBY
144
178
  end
145
179
  end
146
180
  end
147
181
 
148
182
  class InstanceMethodsOnActivation < Module
149
- def initialize(attribute)
183
+ def initialize(attribute, reset_token:)
150
184
  attr_reader attribute
151
185
 
152
186
  define_method("#{attribute}=") do |unencrypted_password|
@@ -184,6 +218,13 @@ module ActiveModel
184
218
  end
185
219
 
186
220
  alias_method :authenticate, :authenticate_password if attribute == :password
221
+
222
+ if reset_token
223
+ # Returns the class-level configured reset token for the password.
224
+ define_method("#{attribute}_reset_token") do
225
+ generate_token_for(:"#{attribute}_reset")
226
+ end
227
+ end
187
228
  end
188
229
  end
189
230
  end
@@ -29,8 +29,8 @@ module ActiveModel
29
29
  # An +attributes+ hash must be defined and should contain any attributes you
30
30
  # need to be serialized. Attributes must be strings, not symbols.
31
31
  # When called, serializable hash will use instance methods that match the name
32
- # of the attributes hash's keys. In order to override this behavior, take a look
33
- # at the private method +read_attribute_for_serialization+.
32
+ # of the attributes hash's keys. In order to override this behavior, override
33
+ # the +read_attribute_for_serialization+ method.
34
34
  #
35
35
  # ActiveModel::Serializers::JSON module automatically includes
36
36
  # the +ActiveModel::Serialization+ module, so there is no need to
@@ -128,7 +128,7 @@ module ActiveModel
128
128
  return serializable_attributes(attribute_names) if options.blank?
129
129
 
130
130
  if only = options[:only]
131
- attribute_names &= Array(only).map(&:to_s)
131
+ attribute_names = Array(only).map(&:to_s) & attribute_names
132
132
  elsif except = options[:except]
133
133
  attribute_names -= Array(except).map(&:to_s)
134
134
  end
@@ -148,29 +148,29 @@ module ActiveModel
148
148
  hash
149
149
  end
150
150
 
151
+ # Hook method defining how an attribute value should be retrieved for
152
+ # serialization. By default this is assumed to be an instance named after
153
+ # the attribute. Override this method in subclasses should you need to
154
+ # retrieve the value for a given attribute differently:
155
+ #
156
+ # class MyClass
157
+ # include ActiveModel::Serialization
158
+ #
159
+ # def initialize(data = {})
160
+ # @data = data
161
+ # end
162
+ #
163
+ # def read_attribute_for_serialization(key)
164
+ # @data[key]
165
+ # end
166
+ # end
167
+ alias :read_attribute_for_serialization :send
168
+
151
169
  private
152
170
  def attribute_names_for_serialization
153
171
  attributes.keys
154
172
  end
155
173
 
156
- # Hook method defining how an attribute value should be retrieved for
157
- # serialization. By default this is assumed to be an instance named after
158
- # the attribute. Override this method in subclasses should you need to
159
- # retrieve the value for a given attribute differently:
160
- #
161
- # class MyClass
162
- # include ActiveModel::Serialization
163
- #
164
- # def initialize(data = {})
165
- # @data = data
166
- # end
167
- #
168
- # def read_attribute_for_serialization(key)
169
- # @data[key]
170
- # end
171
- # end
172
- alias :read_attribute_for_serialization :send
173
-
174
174
  def serializable_attributes(attribute_names)
175
175
  attribute_names.index_with { |n| read_attribute_for_serialization(n) }
176
176
  end
@@ -22,6 +22,8 @@ module ActiveModel
22
22
  module Translation
23
23
  include ActiveModel::Naming
24
24
 
25
+ singleton_class.attr_accessor :raise_on_missing_translations
26
+
25
27
  # Returns the +i18n_scope+ for the class. Override if you want custom lookup.
26
28
  def i18n_scope
27
29
  :activemodel
@@ -50,23 +52,38 @@ module ActiveModel
50
52
  namespace, _, attribute = attribute.rpartition(".")
51
53
  namespace.tr!(".", "/")
52
54
 
55
+ if attribute.present?
56
+ key = "#{namespace}.#{attribute}"
57
+ separator = "/"
58
+ else
59
+ key = namespace
60
+ separator = "."
61
+ end
62
+
53
63
  defaults = lookup_ancestors.map do |klass|
54
- :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
64
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}#{separator}#{key}"
55
65
  end
56
- defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
66
+ defaults << :"#{i18n_scope}.attributes.#{key}"
67
+ defaults << :"attributes.#{key}"
57
68
  else
58
69
  defaults = lookup_ancestors.map do |klass|
59
70
  :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
60
71
  end
61
72
  end
62
73
 
74
+ raise_on_missing = options.fetch(:raise, Translation.raise_on_missing_translations)
75
+
63
76
  defaults << :"attributes.#{attribute}"
64
77
  defaults << options[:default] if options[:default]
65
- defaults << MISSING_TRANSLATION
78
+ defaults << MISSING_TRANSLATION unless raise_on_missing
66
79
 
67
- translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
68
- translation = attribute.humanize if translation == MISSING_TRANSLATION
80
+ translation = I18n.translate(defaults.shift, count: 1, raise: raise_on_missing, **options, default: defaults)
81
+ if translation == MISSING_TRANSLATION
82
+ translation = attribute.present? ? attribute.humanize : namespace.humanize
83
+ end
69
84
  translation
70
85
  end
71
86
  end
87
+
88
+ ActiveSupport.run_load_hooks(:active_model_translation, Translation)
72
89
  end
@@ -15,14 +15,6 @@ module ActiveModel
15
15
  # attribute :weight, :float
16
16
  # end
17
17
  #
18
- # Values are cast using their +to_f+ method, except for the following
19
- # strings:
20
- #
21
- # - Blank strings are cast to +nil+.
22
- # - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
23
- # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
24
- # - <tt>"NaN"</tt> is cast to +Float::NAN+.
25
- #
26
18
  # bag = BagOfCoffee.new
27
19
  #
28
20
  # bag.weight = "0.25"
@@ -33,6 +25,14 @@ module ActiveModel
33
25
  #
34
26
  # bag.weight = "NaN"
35
27
  # bag.weight # => Float::NAN
28
+ #
29
+ # Values are cast using their +to_f+ method, except for the following
30
+ # strings:
31
+ #
32
+ # - Blank strings are cast to +nil+.
33
+ # - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
34
+ # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
35
+ # - <tt>"NaN"</tt> is cast to +Float::NAN+.
36
36
  class Float < Value
37
37
  include Helpers::Numeric
38
38
 
@@ -69,56 +69,32 @@ module ActiveModel
69
69
  \z
70
70
  /x
71
71
 
72
- if RUBY_VERSION >= "3.2"
73
- if Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 # Early 3.2.x had a bug
74
- # BUG: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0
75
- # used to return an invalid Time object
76
- # see: https://bugs.ruby-lang.org/issues/19292
77
- def fast_string_to_time(string)
78
- return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
79
-
80
- if is_utc?
81
- ::Time.at(::Time.new(string, in: "UTC"))
82
- else
83
- ::Time.new(string)
84
- end
85
- rescue ArgumentError
86
- nil
87
- end
88
- else
89
- def fast_string_to_time(string)
90
- return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
91
-
92
- if is_utc?
93
- ::Time.new(string, in: "UTC")
94
- else
95
- ::Time.new(string)
96
- end
97
- rescue ArgumentError
98
- nil
72
+ if Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 # Early 3.2.x had a bug
73
+ # BUG: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0
74
+ # used to return an invalid Time object
75
+ # see: https://bugs.ruby-lang.org/issues/19292
76
+ def fast_string_to_time(string)
77
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
78
+
79
+ if is_utc?
80
+ ::Time.at(::Time.new(string, in: "UTC"))
81
+ else
82
+ ::Time.new(string)
99
83
  end
84
+ rescue ArgumentError
85
+ nil
100
86
  end
101
87
  else
102
88
  def fast_string_to_time(string)
103
- return unless ISO_DATETIME =~ string
89
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
104
90
 
105
- usec = $7.to_i
106
- usec_len = $7&.length
107
- if usec_len&.< 6
108
- usec *= 10**(6 - usec_len)
91
+ if is_utc?
92
+ ::Time.new(string, in: "UTC")
93
+ else
94
+ ::Time.new(string)
109
95
  end
110
-
111
- if $8
112
- offset = \
113
- if $8 == "Z"
114
- 0
115
- else
116
- offset_h, offset_m = $8.to_i, $9.to_i
117
- offset_h.to_i * 3600 + (offset_h.negative? ? -1 : 1) * offset_m * 60
118
- end
119
- end
120
-
121
- new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
96
+ rescue ArgumentError
97
+ nil
122
98
  end
123
99
  end
124
100
  end
@@ -25,7 +25,7 @@ module ActiveModel
25
25
  # by the database. For example a boolean type can return +true+ if the
26
26
  # value parameter is a Ruby boolean, but may return +false+ if the value
27
27
  # parameter is some other object.
28
- def serializable?(value)
28
+ def serializable?(value, &_)
29
29
  true
30
30
  end
31
31
 
@@ -114,8 +114,8 @@ module ActiveModel
114
114
  false
115
115
  end
116
116
 
117
- def map(value) # :nodoc:
118
- yield value
117
+ def map(value, &) # :nodoc:
118
+ value
119
119
  end
120
120
 
121
121
  def ==(other)
@@ -24,7 +24,7 @@ module ActiveModel
24
24
  Array(options[:accept]).include?(value)
25
25
  end
26
26
 
27
- class LazilyDefineAttributes < Module
27
+ class LazilyDefineAttributes < Module # :nodoc:
28
28
  def initialize(attributes)
29
29
  @attributes = attributes.map(&:to_s)
30
30
  end
@@ -78,7 +78,12 @@ module ActiveModel
78
78
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
79
79
  # <tt>on: :custom_validation_context</tt> or
80
80
  # <tt>on: [:create, :custom_validation_context]</tt>)
81
- # * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
81
+ # * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
82
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
83
+ # or an array of symbols. (e.g. <tt>except: :create</tt> or
84
+ # <tt>except_on: :custom_validation_context</tt> or
85
+ # <tt>except_on: [:create, :custom_validation_context]</tt>)
86
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
82
87
  # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
83
88
  # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
84
89
  # proc or string should return or evaluate to a +true+ or +false+ value.
@@ -155,7 +160,7 @@ module ActiveModel
155
160
  # When creating custom validators, it might be useful to be able to specify
156
161
  # additional default keys. This can be done by overwriting this method.
157
162
  def _validates_default_keys
158
- [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
163
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict, :except_on]
159
164
  end
160
165
 
161
166
  def _parse_validates_options(options)
@@ -45,27 +45,6 @@ module ActiveModel
45
45
  extend HelperMethods
46
46
  include HelperMethods
47
47
 
48
- ##
49
- # :method: validation_context
50
- # Returns the context when running validations.
51
- #
52
- # This is useful when running validations except a certain context (opposite to the +on+ option).
53
- #
54
- # class Person
55
- # include ActiveModel::Validations
56
- #
57
- # attr_accessor :name
58
- # validates :name, presence: true, if: -> { validation_context != :custom }
59
- # end
60
- #
61
- # person = Person.new
62
- # person.valid? #=> false
63
- # person.valid?(:new) #=> false
64
- # person.valid?(:custom) #=> true
65
-
66
- ##
67
- attr_accessor :validation_context
68
- private :validation_context=
69
48
  define_callbacks :validate, scope: :name
70
49
 
71
50
  class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
@@ -84,12 +63,18 @@ module ActiveModel
84
63
  # end
85
64
  # end
86
65
  #
87
- # Options:
66
+ # ==== Options
67
+ #
88
68
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
89
69
  # Runs in all validation contexts by default +nil+. You can pass a symbol
90
70
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
91
71
  # <tt>on: :custom_validation_context</tt> or
92
72
  # <tt>on: [:create, :custom_validation_context]</tt>)
73
+ # * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
74
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
75
+ # or an array of symbols. (e.g. <tt>except: :create</tt> or
76
+ # <tt>except_on: :custom_validation_context</tt> or
77
+ # <tt>except_on: [:create, :custom_validation_context]</tt>)
93
78
  # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
94
79
  # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
95
80
  # * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
@@ -105,7 +90,7 @@ module ActiveModel
105
90
  validates_with BlockValidator, _merge_attributes(attr_names), &block
106
91
  end
107
92
 
108
- VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
93
+ VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend, :except_on].freeze # :nodoc:
109
94
 
110
95
  # Adds a validation method or block to the class. This is useful when
111
96
  # overriding the +validate+ instance method becomes too unwieldy and
@@ -150,20 +135,26 @@ module ActiveModel
150
135
  # Note that the return value of validation methods is not relevant.
151
136
  # It's not possible to halt the validate callback chain.
152
137
  #
153
- # Options:
138
+ # ==== Options
139
+ #
154
140
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
155
141
  # Runs in all validation contexts by default +nil+. You can pass a symbol
156
142
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
157
143
  # <tt>on: :custom_validation_context</tt> or
158
144
  # <tt>on: [:create, :custom_validation_context]</tt>)
159
- # * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
145
+ # * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
146
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
147
+ # or an array of symbols. (e.g. <tt>except: :create</tt> or
148
+ # <tt>except_on: :custom_validation_context</tt> or
149
+ # <tt>except_on: [:create, :custom_validation_context]</tt>)
150
+ # * <tt>:if</tt> - Specifies a method or proc to call to determine
160
151
  # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
161
- # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
162
- # proc or string should return or evaluate to a +true+ or +false+ value.
163
- # * <tt>:unless</tt> - Specifies a method, proc, or string to call to
152
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method or
153
+ # proc should return or evaluate to a +true+ or +false+ value.
154
+ # * <tt>:unless</tt> - Specifies a method or proc to call to
164
155
  # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
165
156
  # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
166
- # method, proc, or string should return or evaluate to a +true+ or +false+
157
+ # method or proc should return or evaluate to a +true+ or +false+
167
158
  # value.
168
159
  #
169
160
  # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
@@ -183,6 +174,15 @@ module ActiveModel
183
174
  options = options.merge(if: [predicate_for_validation_context(options[:on]), *options[:if]])
184
175
  end
185
176
 
177
+ if options.key?(:except_on)
178
+ options = options.dup
179
+ options[:except_on] = Array(options[:except_on])
180
+ options[:unless] = [
181
+ ->(o) { options[:except_on].intersect?(Array(o.validation_context)) },
182
+ *options[:unless]
183
+ ]
184
+ end
185
+
186
186
  set_callback(:validate, *args, options, &block)
187
187
  end
188
188
 
@@ -361,15 +361,23 @@ module ActiveModel
361
361
  # person.valid? # => true
362
362
  # person.valid?(:new) # => false
363
363
  def valid?(context = nil)
364
- current_context, self.validation_context = validation_context, context
364
+ current_context = validation_context
365
+ context_for_validation.context = context
365
366
  errors.clear
366
367
  run_validations!
367
368
  ensure
368
- self.validation_context = current_context
369
+ context_for_validation.context = current_context
369
370
  end
370
371
 
371
372
  alias_method :validate, :valid?
372
373
 
374
+ def freeze
375
+ errors
376
+ context_for_validation
377
+
378
+ super
379
+ end
380
+
373
381
  # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
374
382
  # added, +false+ otherwise.
375
383
  #
@@ -430,11 +438,38 @@ module ActiveModel
430
438
  # end
431
439
  alias :read_attribute_for_validation :send
432
440
 
441
+ # Returns the context when running validations.
442
+ #
443
+ # This is useful when running validations except a certain context (opposite to the +on+ option).
444
+ #
445
+ # class Person
446
+ # include ActiveModel::Validations
447
+ #
448
+ # attr_accessor :name
449
+ # validates :name, presence: true, if: -> { validation_context != :custom }
450
+ # end
451
+ #
452
+ # person = Person.new
453
+ # person.valid? #=> false
454
+ # person.valid?(:new) #=> false
455
+ # person.valid?(:custom) #=> true
456
+ def validation_context
457
+ context_for_validation.context
458
+ end
459
+
433
460
  private
461
+ def validation_context=(context)
462
+ context_for_validation.context = context
463
+ end
464
+
465
+ def context_for_validation
466
+ @context_for_validation ||= ValidationContext.new
467
+ end
468
+
434
469
  def init_internals
435
470
  super
436
471
  @errors = nil
437
- @validation_context = nil
472
+ @context_for_validation = nil
438
473
  end
439
474
 
440
475
  def run_validations!
@@ -466,6 +501,10 @@ module ActiveModel
466
501
  super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
467
502
  end
468
503
  end
504
+
505
+ class ValidationContext # :nodoc:
506
+ attr_accessor :context
507
+ end
469
508
  end
470
509
 
471
510
  Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.2.2
4
+ version: 8.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 7.2.2.2
18
+ version: 8.0.3
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 7.2.2.2
25
+ version: 8.0.3
26
26
  description: A toolkit for building modeling frameworks like Active Record. Rich support
27
27
  for attributes, callbacks, validations, serialization, internationalization, and
28
28
  testing.
@@ -111,10 +111,10 @@ licenses:
111
111
  - MIT
112
112
  metadata:
113
113
  bug_tracker_uri: https://github.com/rails/rails/issues
114
- changelog_uri: https://github.com/rails/rails/blob/v7.2.2.2/activemodel/CHANGELOG.md
115
- documentation_uri: https://api.rubyonrails.org/v7.2.2.2/
114
+ changelog_uri: https://github.com/rails/rails/blob/v8.0.3/activemodel/CHANGELOG.md
115
+ documentation_uri: https://api.rubyonrails.org/v8.0.3/
116
116
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
117
- source_code_uri: https://github.com/rails/rails/tree/v7.2.2.2/activemodel
117
+ source_code_uri: https://github.com/rails/rails/tree/v8.0.3/activemodel
118
118
  rubygems_mfa_required: 'true'
119
119
  rdoc_options: []
120
120
  require_paths:
@@ -123,7 +123,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
123
  requirements:
124
124
  - - ">="
125
125
  - !ruby/object:Gem::Version
126
- version: 3.1.0
126
+ version: 3.2.0
127
127
  required_rubygems_version: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="