activemodel 7.2.1.1 → 8.0.0.rc1

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: e589e4c3a8a7002f27353fdc043d9f8662774b0c073e57834e13ced5a4aa8ccc
4
- data.tar.gz: '059bf63fbcb6b05e9744c9d3066b2bfb50c5d00e7ddeed96635cd9a4de592d06'
3
+ metadata.gz: cff9ce182fe19b1ba44acb21c565c8527602855ad42582c68ca865c6c7808418
4
+ data.tar.gz: ac9f1b90cdad91c42d192ccd31fff45f96236a05dfab084a6846a1bba2881f31
5
5
  SHA512:
6
- metadata.gz: cb4eaa35dddcd45300236499be5d319f5605cf6708260e49ef2a29984374868eaf30ab7d60dc7879b67c84be0ead2af83b4426ced1e39d928dae7e3ebc573c2b
7
- data.tar.gz: b9aa71fa74d17e3b169ad035e02bff7bc6254b9e8a6f69608e49f90d3173a2e2349049aba9b897c2b98e25f977a08aa9c20152295f35aecdd61eb309fca69710
6
+ metadata.gz: f1cd9e0aabc319cfa3b73d3951470b2c31adedc2aafc11a5cd25a13ddaa7f346db103d6351009bd0f681184e8fbf840358df747b808e59e2857133bd6e372561
7
+ data.tar.gz: 917d12bda8d9f7f7f442897256fd046d27f29ca0f8a28aee17c68773cf48772f0142f30603af455f952ec1a308a20b4fc70367eba3dcabaa28cb1c24397eccef
data/CHANGELOG.md CHANGED
@@ -1,34 +1,90 @@
1
- ## Rails 7.2.1.1 (October 15, 2024) ##
1
+ ## Rails 8.0.0.rc1 (October 19, 2024) ##
2
2
 
3
- * No changes.
3
+ * Add `:except_on` option for validations. Grants the ability to _skip_ validations in specified contexts.
4
4
 
5
+ ```ruby
6
+ class User < ApplicationRecord
7
+ #...
8
+ validates :birthday, presence: { except_on: :admin }
9
+ #...
10
+ end
5
11
 
6
- ## Rails 7.2.1 (August 22, 2024) ##
12
+ user = User.new(attributes except birthday)
13
+ user.save(context: :admin)
14
+ ```
7
15
 
8
- * No changes.
16
+ *Drew Bragg*
9
17
 
18
+ ## Rails 8.0.0.beta1 (September 26, 2024) ##
10
19
 
11
- ## Rails 7.2.0 (August 09, 2024) ##
20
+ * Make `ActiveModel::Serialization#read_attribute_for_serialization` public
12
21
 
13
- * Fix a bug where type casting of string to `Time` and `DateTime` doesn't
14
- calculate minus minute value in TZ offset correctly.
22
+ *Sean Doyle*
15
23
 
16
- *Akira Matsuda*
24
+ * Add a default token generator for password reset tokens when using `has_secure_password`.
17
25
 
18
- * Port the `type_for_attribute` method to Active Model. Classes that include
19
- `ActiveModel::Attributes` will now provide this method. This method behaves
20
- the same for Active Model as it does for Active Record.
26
+ ```ruby
27
+ class User < ApplicationRecord
28
+ has_secure_password
29
+ end
21
30
 
22
- ```ruby
23
- class MyModel
24
- include ActiveModel::Attributes
31
+ user = User.create!(name: "david", password: "123", password_confirmation: "123")
32
+ token = user.password_reset_token
33
+ User.find_by_password_reset_token(token) # returns user
25
34
 
26
- attribute :my_attribute, :integer
35
+ # 16 minutes later...
36
+ User.find_by_password_reset_token(token) # returns nil
37
+
38
+ # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
39
+ User.find_by_password_reset_token!(token)
40
+ ```
41
+
42
+ *DHH*
43
+
44
+ * Add a load hook `active_model_translation` for `ActiveModel::Translation`.
45
+
46
+ *Shouichi Kamiya*
47
+
48
+ * Add `raise_on_missing_translations` option to `ActiveModel::Translation`.
49
+ When the option is set, `human_attribute_name` raises an error if a translation of the given attribute is missing.
50
+
51
+ ```ruby
52
+ # ActiveModel::Translation.raise_on_missing_translations = false
53
+ Post.human_attribute_name("title")
54
+ => "Title"
55
+
56
+ # ActiveModel::Translation.raise_on_missing_translations = true
57
+ Post.human_attribute_name("title")
58
+ => Translation missing. Options considered were: (I18n::MissingTranslationData)
59
+ - en.activerecord.attributes.post.title
60
+ - en.attributes.title
61
+
62
+ raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
63
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64
+ ```
65
+
66
+ *Shouichi Kamiya*
67
+
68
+ * Introduce `ActiveModel::AttributeAssignment#attribute_writer_missing`
69
+
70
+ Provide instances with an opportunity to gracefully handle assigning to an
71
+ unknown attribute:
72
+
73
+ ```ruby
74
+ class Rectangle
75
+ include ActiveModel::AttributeAssignment
76
+
77
+ attr_accessor :length, :width
78
+
79
+ def attribute_writer_missing(name, value)
80
+ Rails.logger.warn "Tried to assign to unknown attribute #{name}"
27
81
  end
82
+ end
28
83
 
29
- MyModel.type_for_attribute(:my_attribute) # => #<ActiveModel::Type::Integer ...>
30
- ```
84
+ rectangle = Rectangle.new
85
+ rectangle.assign_attributes(height: 10) # => Logs "Tried to assign to unknown attribute 'height'"
86
+ ```
31
87
 
32
- *Jonathan Hefner*
88
+ *Sean Doyle*
33
89
 
34
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activemodel/CHANGELOG.md) for previous changes.
90
+ Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activemodel/CHANGELOG.md) for previous changes.
@@ -36,6 +36,27 @@ 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
62
  attributes.each do |k, v|
@@ -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
@@ -215,7 +215,12 @@ module ActiveModel
215
215
  end
216
216
 
217
217
  def generate_alias_attribute_methods(code_generator, new_name, old_name)
218
- define_attribute_method(old_name, _owner: code_generator, as: new_name)
218
+ ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
219
+ attribute_method_patterns.each do |pattern|
220
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
221
+ end
222
+ attribute_method_patterns_cache.clear
223
+ end
219
224
  end
220
225
 
221
226
  def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
@@ -228,7 +233,7 @@ module ActiveModel
228
233
  call_args = []
229
234
  call_args << parameters if parameters
230
235
 
231
- define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute)
236
+ define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
232
237
  end
233
238
 
234
239
  # Is +new_name+ an alias?
@@ -441,7 +446,7 @@ module ActiveModel
441
446
  mangled_name = name
442
447
 
443
448
  unless NAME_COMPILABLE_REGEXP.match?(name)
444
- mangled_name = "__temp__#{name.unpack1("h*")}"
449
+ mangled_name = :"__temp__#{name.unpack1("h*")}"
445
450
  end
446
451
 
447
452
  mangled_name
@@ -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
  #
@@ -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
 
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 2
12
- TINY = 1
13
- PRE = "1"
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "rc1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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 reponds 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
@@ -142,11 +157,30 @@ module ActiveModel
142
157
 
143
158
  validates_confirmation_of attribute, allow_blank: true
144
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
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
@@ -60,13 +62,17 @@ module ActiveModel
60
62
  end
61
63
  end
62
64
 
65
+ raise_on_missing = options.fetch(:raise, Translation.raise_on_missing_translations)
66
+
63
67
  defaults << :"attributes.#{attribute}"
64
68
  defaults << options[:default] if options[:default]
65
- defaults << MISSING_TRANSLATION
69
+ defaults << MISSING_TRANSLATION unless raise_on_missing
66
70
 
67
- translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
71
+ translation = I18n.translate(defaults.shift, count: 1, raise: raise_on_missing, **options, default: defaults)
68
72
  translation = attribute.humanize if translation == MISSING_TRANSLATION
69
73
  translation
70
74
  end
71
75
  end
76
+
77
+ ActiveSupport.run_load_hooks(:active_model_translation, Translation)
72
78
  end
@@ -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
@@ -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)
@@ -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] = [] }
@@ -90,6 +69,11 @@ module ActiveModel
90
69
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
91
70
  # <tt>on: :custom_validation_context</tt> or
92
71
  # <tt>on: [:create, :custom_validation_context]</tt>)
72
+ # * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
73
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
74
+ # or an array of symbols. (e.g. <tt>except: :create</tt> or
75
+ # <tt>except_on: :custom_validation_context</tt> or
76
+ # <tt>except_on: [:create, :custom_validation_context]</tt>)
93
77
  # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
94
78
  # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
95
79
  # * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
@@ -105,7 +89,7 @@ module ActiveModel
105
89
  validates_with BlockValidator, _merge_attributes(attr_names), &block
106
90
  end
107
91
 
108
- VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
92
+ VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend, :except_on].freeze # :nodoc:
109
93
 
110
94
  # Adds a validation method or block to the class. This is useful when
111
95
  # overriding the +validate+ instance method becomes too unwieldy and
@@ -156,7 +140,12 @@ module ActiveModel
156
140
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
157
141
  # <tt>on: :custom_validation_context</tt> or
158
142
  # <tt>on: [:create, :custom_validation_context]</tt>)
159
- # * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
143
+ # * <tt>:except_on</tt> - Specifies the contexts where this validation is not active.
144
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
145
+ # or an array of symbols. (e.g. <tt>except: :create</tt> or
146
+ # <tt>except_on: :custom_validation_context</tt> or
147
+ # <tt>except_on: [:create, :custom_validation_context]</tt>)
148
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
160
149
  # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
161
150
  # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
162
151
  # proc or string should return or evaluate to a +true+ or +false+ value.
@@ -183,6 +172,15 @@ module ActiveModel
183
172
  options = options.merge(if: [predicate_for_validation_context(options[:on]), *options[:if]])
184
173
  end
185
174
 
175
+ if options.key?(:except_on)
176
+ options = options.dup
177
+ options[:except_on] = Array(options[:except_on])
178
+ options[:unless] = [
179
+ ->(o) { (options[:except_on] & Array(o.validation_context)).any? },
180
+ *options[:unless]
181
+ ]
182
+ end
183
+
186
184
  set_callback(:validate, *args, options, &block)
187
185
  end
188
186
 
@@ -361,15 +359,23 @@ module ActiveModel
361
359
  # person.valid? # => true
362
360
  # person.valid?(:new) # => false
363
361
  def valid?(context = nil)
364
- current_context, self.validation_context = validation_context, context
362
+ current_context = validation_context
363
+ context_for_validation.context = context
365
364
  errors.clear
366
365
  run_validations!
367
366
  ensure
368
- self.validation_context = current_context
367
+ context_for_validation.context = current_context
369
368
  end
370
369
 
371
370
  alias_method :validate, :valid?
372
371
 
372
+ def freeze
373
+ errors
374
+ context_for_validation
375
+
376
+ super
377
+ end
378
+
373
379
  # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
374
380
  # added, +false+ otherwise.
375
381
  #
@@ -430,11 +436,38 @@ module ActiveModel
430
436
  # end
431
437
  alias :read_attribute_for_validation :send
432
438
 
439
+ # Returns the context when running validations.
440
+ #
441
+ # This is useful when running validations except a certain context (opposite to the +on+ option).
442
+ #
443
+ # class Person
444
+ # include ActiveModel::Validations
445
+ #
446
+ # attr_accessor :name
447
+ # validates :name, presence: true, if: -> { validation_context != :custom }
448
+ # end
449
+ #
450
+ # person = Person.new
451
+ # person.valid? #=> false
452
+ # person.valid?(:new) #=> false
453
+ # person.valid?(:custom) #=> true
454
+ def validation_context
455
+ context_for_validation.context
456
+ end
457
+
433
458
  private
459
+ def validation_context=(context)
460
+ context_for_validation.context = context
461
+ end
462
+
463
+ def context_for_validation
464
+ @context_for_validation ||= ValidationContext.new
465
+ end
466
+
434
467
  def init_internals
435
468
  super
436
469
  @errors = nil
437
- @validation_context = nil
470
+ @context_for_validation = nil
438
471
  end
439
472
 
440
473
  def run_validations!
@@ -466,6 +499,10 @@ module ActiveModel
466
499
  super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
467
500
  end
468
501
  end
502
+
503
+ class ValidationContext # :nodoc:
504
+ attr_accessor :context
505
+ end
469
506
  end
470
507
 
471
508
  Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.1.1
4
+ version: 8.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-15 00:00:00.000000000 Z
11
+ date: 2024-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 7.2.1.1
19
+ version: 8.0.0.rc1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 7.2.1.1
26
+ version: 8.0.0.rc1
27
27
  description: A toolkit for building modeling frameworks like Active Record. Rich support
28
28
  for attributes, callbacks, validations, serialization, internationalization, and
29
29
  testing.
@@ -112,10 +112,10 @@ licenses:
112
112
  - MIT
113
113
  metadata:
114
114
  bug_tracker_uri: https://github.com/rails/rails/issues
115
- changelog_uri: https://github.com/rails/rails/blob/v7.2.1.1/activemodel/CHANGELOG.md
116
- documentation_uri: https://api.rubyonrails.org/v7.2.1.1/
115
+ changelog_uri: https://github.com/rails/rails/blob/v8.0.0.rc1/activemodel/CHANGELOG.md
116
+ documentation_uri: https://api.rubyonrails.org/v8.0.0.rc1/
117
117
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
118
- source_code_uri: https://github.com/rails/rails/tree/v7.2.1.1/activemodel
118
+ source_code_uri: https://github.com/rails/rails/tree/v8.0.0.rc1/activemodel
119
119
  rubygems_mfa_required: 'true'
120
120
  post_install_message:
121
121
  rdoc_options: []
@@ -125,7 +125,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
125
  requirements:
126
126
  - - ">="
127
127
  - !ruby/object:Gem::Version
128
- version: 3.1.0
128
+ version: 3.2.0
129
129
  required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  requirements:
131
131
  - - ">="