activemodel 7.2.1.1 → 8.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="