2023-09-27 05:45:03 +08:00
|
|
|
|
* Remove change in the typography of user facing error messages.
|
|
|
|
|
For example, “can’t be blank” is again “can't be blank”.
|
|
|
|
|
|
|
|
|
|
*Rafael Mendonça França*
|
|
|
|
|
|
|
|
|
|
|
2023-09-13 08:36:01 +08:00
|
|
|
|
## Rails 7.1.0.beta1 (September 13, 2023) ##
|
|
|
|
|
|
2023-09-01 23:40:40 +08:00
|
|
|
|
* Support composite identifiers in `to_key`
|
|
|
|
|
|
|
|
|
|
`to_key` avoids wrapping `#id` value into an `Array` if `#id` already an array
|
|
|
|
|
|
|
|
|
|
*Nikita Vasilevsky*
|
|
|
|
|
|
2023-08-24 02:22:26 +08:00
|
|
|
|
* Add `ActiveModel::Conversion.param_delimiter` to configure delimiter being used in `to_param`
|
|
|
|
|
|
|
|
|
|
*Nikita Vasilevsky*
|
|
|
|
|
|
2023-08-23 22:34:04 +08:00
|
|
|
|
* `undefine_attribute_methods` undefines alias attribute methods along with attribute methods.
|
|
|
|
|
|
|
|
|
|
*Nikita Vasilevsky*
|
|
|
|
|
|
2023-08-03 03:52:02 +08:00
|
|
|
|
* Error.full_message now strips ":base" from the message.
|
2023-07-09 19:55:02 +08:00
|
|
|
|
|
2023-08-03 03:52:02 +08:00
|
|
|
|
*zzak*
|
2023-07-09 19:55:02 +08:00
|
|
|
|
|
2023-08-03 03:52:02 +08:00
|
|
|
|
* Add a load hook for `ActiveModel::Model` (named `active_model`) to match the load hook for
|
|
|
|
|
`ActiveRecord::Base` and allow for overriding aspects of the `ActiveModel::Model` class.
|
2023-08-02 18:39:12 +08:00
|
|
|
|
|
2023-08-03 03:52:02 +08:00
|
|
|
|
*Lewis Buckley*
|
2023-08-02 18:39:12 +08:00
|
|
|
|
|
2023-08-03 03:52:02 +08:00
|
|
|
|
* Improve password length validation in ActiveModel::SecurePassword to consider byte size for BCrypt
|
|
|
|
|
compatibility.
|
2023-03-18 09:18:23 +08:00
|
|
|
|
|
|
|
|
|
The previous password length validation only considered the character count, which may not
|
|
|
|
|
accurately reflect the 72-byte size limit imposed by BCrypt. This change updates the validation
|
|
|
|
|
to consider both character count and byte size while keeping the character length validation in place.
|
|
|
|
|
|
|
|
|
|
```ruby
|
2023-09-07 02:03:58 +08:00
|
|
|
|
user = User.new(password: "a" * 73) # 73 characters
|
|
|
|
|
user.valid? # => false
|
|
|
|
|
user.errors[:password] # => ["is too long"]
|
2023-03-18 09:18:23 +08:00
|
|
|
|
|
|
|
|
|
|
2023-09-07 02:03:58 +08:00
|
|
|
|
user = User.new(password: "あ" * 25) # 25 characters, 75 bytes
|
|
|
|
|
user.valid? # => false
|
|
|
|
|
user.errors[:password] # => ["is too long"]
|
2023-03-18 09:18:23 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
*ChatGPT*, *Guillermo Iguaran*
|
|
|
|
|
|
2023-02-24 12:25:21 +08:00
|
|
|
|
* `has_secure_password` now generates an `#{attribute}_salt` method that returns the salt
|
|
|
|
|
used to compute the password digest. The salt will change whenever the password is changed,
|
|
|
|
|
so it can be used to create single-use password reset tokens with `generates_token_for`:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class User < ActiveRecord::Base
|
|
|
|
|
has_secure_password
|
|
|
|
|
|
|
|
|
|
generates_token_for :password_reset, expires_in: 15.minutes do
|
|
|
|
|
password_salt&.last(10)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
*Lázaro Nixon*
|
|
|
|
|
|
2022-06-26 21:11:27 +08:00
|
|
|
|
* Improve typography of user facing error messages. In English contractions,
|
2023-03-04 07:51:05 +08:00
|
|
|
|
the Unicode APOSTROPHE (`U+0027`) is now RIGHT SINGLE QUOTATION MARK
|
|
|
|
|
(`U+2019`). For example, "can't be blank" is now "can’t be blank".
|
2022-06-26 21:11:27 +08:00
|
|
|
|
|
|
|
|
|
*Jon Dufresne*
|
|
|
|
|
|
2023-03-04 07:51:05 +08:00
|
|
|
|
* Add class to `ActiveModel::MissingAttributeError` error message.
|
2023-03-02 05:00:04 +08:00
|
|
|
|
|
|
|
|
|
Show which class is missing the attribute in the error message:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
user = User.first
|
|
|
|
|
user.pets.select(:id).first.user_id
|
|
|
|
|
# => ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
*Petrik de Heus*
|
|
|
|
|
|
2022-11-21 03:36:29 +08:00
|
|
|
|
* Raise `NoMethodError` in `ActiveModel::Type::Value#as_json` to avoid unpredictable
|
|
|
|
|
results.
|
|
|
|
|
|
|
|
|
|
*Vasiliy Ermolovich*
|
|
|
|
|
|
Avoid double cast in types that only override cast
Follow-up to #44625.
In #44625, the `SerializeCastValue` module was added to allow types to
avoid a redundant call to `cast` when serializing a value for the
database. Because it introduced a new method (`serialize_cast_value`)
that was not part of the `ActiveModel::Type::Value` contract, it was
designed to be opt-in. Furthermore, to guard against incompatible
`serialize` and `serialize_cast_value` implementations in types that
override `serialize` but (unintentionally) inherit `serialize_cast_value`,
types were required to explicitly include the `SerializeCastValue`
module to activate the optimization. i.e. It was not sufficient just to
have `SerializeCastValue` in the ancestor chain.
The `SerializeCastValue` module is not part of the public API, and there
are no plans to change that, which meant user-created custom types could
not benefit from this optimization.
This commit changes the opt-in condition such that it is sufficient for
the owner of the `serialize_cast_value` method to be the same or below
the owner of the `serialize` method in the ancestor chain. This means
a user-created type that only overrides `cast`, **not** `serialize`,
will now benefit from the optimization. For example, a type like:
```ruby
class DowncasedString < ActiveModel::Type::String
def cast(value)
super&.downcase
end
end
```
As demonstrated in the benchmark below, this commit does not change the
current performance of the built-in Active Model types. However, for a
simple custom type like `DowncasedString`, the performance of
`value_for_database` is twice as fast. For types with more expensive
`cast` operations, the improvement may be greater.
**Benchmark**
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_model"
class DowncasedString < ActiveModel::Type::String
def cast(value)
super&.downcase
end
end
ActiveModel::Type.register(:downcased_string, DowncasedString)
VALUES = {
my_big_integer: "123456",
my_boolean: "true",
my_date: "1999-12-31",
my_datetime: "1999-12-31 12:34:56 UTC",
my_decimal: "123.456",
my_float: "123.456",
my_immutable_string: "abcdef",
my_integer: "123456",
my_string: "abcdef",
my_time: "1999-12-31T12:34:56.789-10:00",
my_downcased_string: "AbcDef",
}
TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] }
class MyModel
include ActiveModel::API
include ActiveModel::Attributes
TYPES.each do |name, type|
attribute name, type
end
end
attribute_set = MyModel.new(VALUES).instance_variable_get(:@attributes)
TYPES.each do |name, type|
attribute = attribute_set[name.to_s]
Benchmark.ips do |x|
x.report(type.to_s) { attribute.value_for_database }
end
end
```
**Before**
```
big_integer 2.986M (± 1.2%) i/s - 15.161M in 5.078972s
boolean 2.980M (± 1.1%) i/s - 15.074M in 5.059456s
date 2.960M (± 1.1%) i/s - 14.831M in 5.011355s
datetime 1.368M (± 0.9%) i/s - 6.964M in 5.092074s
decimal 2.930M (± 1.2%) i/s - 14.911M in 5.089048s
float 2.932M (± 1.3%) i/s - 14.713M in 5.018512s
immutable_string 3.013M (± 1.3%) i/s - 15.239M in 5.058085s
integer 1.603M (± 0.8%) i/s - 8.096M in 5.052046s
string 2.977M (± 1.1%) i/s - 15.168M in 5.094874s
time 1.338M (± 0.9%) i/s - 6.699M in 5.006046s
downcased_string 1.394M (± 0.9%) i/s - 7.034M in 5.046972s
```
**After**
```
big_integer 3.016M (± 1.0%) i/s - 15.238M in 5.053005s
boolean 2.965M (± 1.3%) i/s - 15.037M in 5.071921s
date 2.924M (± 1.0%) i/s - 14.754M in 5.046294s
datetime 1.435M (± 0.9%) i/s - 7.295M in 5.082498s
decimal 2.950M (± 0.9%) i/s - 14.800M in 5.017225s
float 2.964M (± 0.9%) i/s - 14.987M in 5.056405s
immutable_string 2.907M (± 1.4%) i/s - 14.677M in 5.049194s
integer 1.638M (± 0.9%) i/s - 8.227M in 5.022401s
string 2.971M (± 1.0%) i/s - 14.891M in 5.011709s
time 1.454M (± 0.9%) i/s - 7.384M in 5.079993s
downcased_string 2.939M (± 0.9%) i/s - 14.872M in 5.061100s
```
2022-10-09 04:34:17 +08:00
|
|
|
|
* Custom attribute types that inherit from Active Model built-in types and do
|
|
|
|
|
not override the `serialize` method will now benefit from an optimization
|
|
|
|
|
when serializing attribute values for the database.
|
|
|
|
|
|
|
|
|
|
For example, with a custom type like the following:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class DowncasedString < ActiveModel::Type::String
|
|
|
|
|
def cast(value)
|
|
|
|
|
super&.downcase
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
ActiveRecord::Type.register(:downcased_string, DowncasedString)
|
|
|
|
|
|
|
|
|
|
class User < ActiveRecord::Base
|
|
|
|
|
attribute :email, :downcased_string
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
user = User.new(email: "FooBar@example.com")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Serializing the `email` attribute for the database will be roughly twice as
|
|
|
|
|
fast. More expensive `cast` operations will likely see greater improvements.
|
|
|
|
|
|
|
|
|
|
*Jonathan Hefner*
|
|
|
|
|
|
2022-07-26 01:16:46 +08:00
|
|
|
|
* `has_secure_password` now supports password challenges via a
|
|
|
|
|
`password_challenge` accessor and validation.
|
|
|
|
|
|
|
|
|
|
A password challenge is a safeguard to verify that the current user is
|
|
|
|
|
actually the password owner. It can be used when changing sensitive model
|
|
|
|
|
fields, such as the password itself. It is different than a password
|
|
|
|
|
confirmation, which is used to prevent password typos.
|
|
|
|
|
|
|
|
|
|
When `password_challenge` is set, the validation checks that the value's
|
|
|
|
|
digest matches the *currently persisted* `password_digest` (i.e.
|
|
|
|
|
`password_digest_was`).
|
|
|
|
|
|
|
|
|
|
This allows a password challenge to be done as part of a typical `update`
|
|
|
|
|
call, just like a password confirmation. It also allows a password
|
|
|
|
|
challenge error to be handled in the same way as other validation errors.
|
|
|
|
|
|
|
|
|
|
For example, in the controller, instead of:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
password_params = params.require(:password).permit(
|
|
|
|
|
:password_challenge,
|
|
|
|
|
:password,
|
|
|
|
|
:password_confirmation,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
password_challenge = password_params.delete(:password_challenge)
|
|
|
|
|
@password_challenge_failed = !current_user.authenticate(password_challenge)
|
|
|
|
|
|
|
|
|
|
if !@password_challenge_failed && current_user.update(password_params)
|
|
|
|
|
# ...
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You can now write:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
password_params = params.require(:password).permit(
|
|
|
|
|
:password_challenge,
|
|
|
|
|
:password,
|
|
|
|
|
:password_confirmation,
|
|
|
|
|
).with_defaults(password_challenge: "")
|
|
|
|
|
|
|
|
|
|
if current_user.update(password_params)
|
|
|
|
|
# ...
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
And, in the view, instead of checking `@password_challenge_failed`, you can
|
|
|
|
|
render an error for the `password_challenge` field just as you would for
|
|
|
|
|
other form fields, including utilizing `config.action_view.field_error_proc`.
|
|
|
|
|
|
|
|
|
|
*Jonathan Hefner*
|
|
|
|
|
|
2022-05-20 07:13:09 +08:00
|
|
|
|
* Support infinite ranges for `LengthValidator`s `:in`/`:within` options
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
validates_length_of :first_name, in: ..30
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
*fatkodima*
|
|
|
|
|
|
2022-05-18 12:57:15 +08:00
|
|
|
|
* Add support for beginless ranges to inclusivity/exclusivity validators:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
validates_inclusion_of :birth_date, in: -> { (..Date.today) }
|
|
|
|
|
```
|
|
|
|
|
|
2023-09-20 05:26:56 +08:00
|
|
|
|
```ruby
|
|
|
|
|
validates_exclusion_of :birth_date, in: -> { (..Date.today) }
|
|
|
|
|
```
|
|
|
|
|
|
2022-05-18 12:57:15 +08:00
|
|
|
|
*Bo Jeanes*
|
|
|
|
|
|
2022-05-18 01:46:00 +08:00
|
|
|
|
* Make validators accept lambdas without record argument
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
# Before
|
|
|
|
|
validates_comparison_of :birth_date, less_than_or_equal_to: ->(_record) { Date.today }
|
|
|
|
|
|
|
|
|
|
# After
|
|
|
|
|
validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
*fatkodima*
|
|
|
|
|
|
2022-05-03 00:59:50 +08:00
|
|
|
|
* Fix casting long strings to `Date`, `Time` or `DateTime`
|
|
|
|
|
|
|
|
|
|
*fatkodima*
|
|
|
|
|
|
2022-02-03 16:17:03 +08:00
|
|
|
|
* Use different cache namespace for proxy calls
|
2021-11-17 03:56:30 +08:00
|
|
|
|
|
2022-02-03 16:17:03 +08:00
|
|
|
|
Models can currently have different attribute bodies for the same method
|
|
|
|
|
names, leading to conflicts. Adding a new namespace `:active_model_proxy`
|
|
|
|
|
fixes the issue.
|
|
|
|
|
|
|
|
|
|
*Chris Salzberg*
|
2021-11-17 03:56:30 +08:00
|
|
|
|
|
2021-12-07 23:52:30 +08:00
|
|
|
|
Please check [7-0-stable](https://github.com/rails/rails/blob/7-0-stable/activemodel/CHANGELOG.md) for previous changes.
|