aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG.md25
-rw-r--r--activemodel/lib/active_model/errors.rb7
-rw-r--r--activemodel/lib/active_model/serializers/json.rb19
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb8
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb13
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb6
-rw-r--r--activemodel/test/cases/type/decimal_test.rb5
-rw-r--r--activemodel/test/cases/type/float_test.rb5
-rw-r--r--activemodel/test/cases/type/integer_test.rb6
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb59
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb10
-rw-r--r--activemodel/test/models/topic.rb14
13 files changed, 158 insertions, 21 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 8f80838a33..6048911217 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,8 +1,31 @@
+* Fix numericality validator to still use value before type cast except Active Record.
+
+ Fixes #33651, #33686.
+
+ *Ryuta Kamizono*
+
+* Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps.
+
+ Before:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC
+ ```
+
+ After:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z"
+ ```
+
+ *Bogdan Gusiev*
+
* Allows configurable attribute name for `#has_secure_password`. This
still defaults to an attribute named 'password', causing no breaking
change. There is a new method `#authenticate_XXX` where XXX is the
configured attribute name, making the existing `#authenticate` now an
alias for this when the attribute is the default 'password'.
+
Example:
class User < ActiveRecord::Base
@@ -14,7 +37,7 @@
user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..."
user.authenticate_recovery_password('42password') # => user
- *Unathi Chonco*
+ *Unathi Chonco*
* Add `config.active_model.i18n_full_message` in order to control whether
the `full_message` error format can be overridden at the attribute or model
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index edc30ee64d..af94d52d45 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -379,9 +379,11 @@ module ActiveModel
# * <tt>errors.format</tt>
def full_message(attribute, message)
return message if attribute == :base
+ attribute = attribute.to_s
if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
- parts = attribute.to_s.split(".")
+ attribute = attribute.remove(/\[\d\]/)
+ parts = attribute.split(".")
attribute_name = parts.pop
namespace = parts.join("/") unless parts.empty?
attributes_scope = "#{@base.class.i18n_scope}.errors.models"
@@ -410,8 +412,9 @@ module ActiveModel
defaults << :"errors.format"
defaults << "%{attribute} %{message}"
- attr_name = attribute.to_s.tr(".", "_").humanize
+ attr_name = attribute.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
+
I18n.t(defaults.shift,
default: defaults,
attribute: attr_name,
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index 25e1541d66..f77fb98c32 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -26,13 +26,13 @@ module ActiveModel
# user = User.find(1)
# user.as_json
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true}
+ # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
#
# ActiveRecord::Base.include_root_in_json = true
#
# user.as_json
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true } }
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
# This behavior can also be achieved by setting the <tt>:root</tt> option
# to +true+ as in:
@@ -40,7 +40,7 @@ module ActiveModel
# user = User.find(1)
# user.as_json(root: true)
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true } }
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
# Without any +options+, the returned Hash will include all the model's
# attributes.
@@ -48,7 +48,7 @@ module ActiveModel
# user = User.find(1)
# user.as_json
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true}
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
# the attributes included, and work similar to the +attributes+ method.
@@ -63,14 +63,14 @@ module ActiveModel
#
# user.as_json(methods: :permalink)
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "permalink" => "1-konata-izumi" }
#
# To include associations use <tt>:include</tt>:
#
# user.as_json(include: :posts)
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
# # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
#
@@ -81,7 +81,7 @@ module ActiveModel
# only: :body } },
# only: :title } })
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
# # "title" => "Welcome to the weblog" },
# # { "comments" => [ { "body" => "Don't think too hard" } ],
@@ -93,11 +93,12 @@ module ActiveModel
include_root_in_json
end
+ hash = serializable_hash(options).as_json
if root
root = model_name.element if root == true
- { root => serializable_hash(options) }
+ { root => hash }
else
- serializable_hash(options)
+ hash
end
end
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index 16e14f9e5f..473cdb0c67 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -29,7 +29,7 @@ module ActiveModel
# 'wibble'.to_i will give zero, we want to make sure
# that we aren't marking int zero to string zero as
# changed.
- value.to_s !~ /\A-?\d+\.?\d*\z/
+ !/\A[-+]?\d+/.match?(value.to_s)
end
end
end
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
index cb6aa67a9d..da56073436 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -70,7 +70,13 @@ module ActiveModel
# Doesn't handle time zones.
def fast_string_to_time(string)
if string =~ ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
+ microsec_part = $7
+ if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7
+ microsec_part[0] = ""
+ microsec = microsec_part.to_i
+ else
+ microsec = (microsec_part.to_r * 1_000_000).to_i
+ end
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
end
end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 0478915be7..126a87ac6e 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -21,8 +21,17 @@ module ActiveModel
def validate_each(record, attr_name, value)
came_from_user = :"#{attr_name}_came_from_user?"
- if record.respond_to?(came_from_user) && record.public_send(came_from_user)
- raw_value = record.read_attribute_before_type_cast(attr_name)
+ if record.respond_to?(came_from_user)
+ if record.public_send(came_from_user)
+ raw_value = record.read_attribute_before_type_cast(attr_name)
+ elsif record.respond_to?(:read_attribute)
+ raw_value = record.read_attribute(attr_name)
+ end
+ else
+ before_type_cast = :"#{attr_name}_before_type_cast"
+ if record.respond_to?(before_type_cast)
+ raw_value = record.public_send(before_type_cast)
+ end
end
raw_value ||= value
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index aae98c9fe4..625e0a427a 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -129,6 +129,10 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_equal :name, options[:except]
end
+ test "as_json should serialize timestamps" do
+ assert_equal "2006-08-01T00:00:00.000Z", @contact.as_json["created_at"]
+ end
+
test "as_json should return a hash if include_root_in_json is true" do
begin
original_include_root_in_json = Contact.include_root_in_json
@@ -138,7 +142,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_kind_of Hash, json
assert_kind_of Hash, json["contact"]
%w(name age created_at awesome preferences).each do |field|
- assert_equal @contact.send(field), json["contact"][field]
+ assert_equal @contact.send(field).as_json, json["contact"][field]
end
ensure
Contact.include_root_in_json = original_include_root_in_json
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index c0cf6ce590..be60c4f7fa 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -57,9 +57,12 @@ module ActiveModel
def test_changed?
type = Decimal.new
- assert type.changed?(5.0, 5.0, "5.0wibble")
+ assert type.changed?(0.0, 0, "wibble")
+ assert type.changed?(5.0, 0, "wibble")
+ assert_not type.changed?(5.0, 5.0, "5.0wibble")
assert_not type.changed?(5.0, 5.0, "5.0")
assert_not type.changed?(-5.0, -5.0, "-5.0")
+ assert_not type.changed?(5.0, 5.0, "0.5e+1")
end
def test_scale_is_applied_before_precision_to_prevent_rounding_errors
diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb
index 28318e06f8..230a8dda32 100644
--- a/activemodel/test/cases/type/float_test.rb
+++ b/activemodel/test/cases/type/float_test.rb
@@ -21,9 +21,12 @@ module ActiveModel
def test_changing_float
type = Type::Float.new
- assert type.changed?(5.0, 5.0, "5wibble")
+ assert type.changed?(0.0, 0, "wibble")
+ assert type.changed?(5.0, 0, "wibble")
+ assert_not type.changed?(5.0, 5.0, "5wibble")
assert_not type.changed?(5.0, 5.0, "5")
assert_not type.changed?(5.0, 5.0, "5.0")
+ assert_not type.changed?(500.0, 500.0, "0.5E+4")
assert_not type.changed?(nil, nil, nil)
end
end
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
index 8c5d18c9b3..df12098974 100644
--- a/activemodel/test/cases/type/integer_test.rb
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -53,9 +53,13 @@ module ActiveModel
test "changed?" do
type = Type::Integer.new
- assert type.changed?(5, 5, "5wibble")
+ assert type.changed?(0, 0, "wibble")
+ assert type.changed?(5, 0, "wibble")
+ assert_not type.changed?(5, 5, "5wibble")
assert_not type.changed?(5, 5, "5")
assert_not type.changed?(5, 5, "5.0")
+ assert_not type.changed?(5, 5, "+5")
+ assert_not type.changed?(5, 5, "+5.0")
assert_not type.changed?(-5, -5, "-5")
assert_not type.changed?(-5, -5, "-5.0")
assert_not type.changed?(nil, nil, nil)
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index 1c62e750de..ccb565c5bd 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -35,7 +35,7 @@ class I18nValidationTest < ActiveModel::TestCase
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
@person.errors.add(:name, "not found")
- assert_called_with(Person, :human_attribute_name, [:name, default: "Name"], returns: "Person's name") do
+ assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do
assert_equal ["Person's name not found"], @person.errors.full_messages
end
end
@@ -101,6 +101,63 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
end
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name
+ ActiveModel::Errors.i18n_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ attributes: { 'person/contacts/addresses': { country: "Country" } }
+ })
+
+ person = Person.new
+ assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config
+ ActiveModel::Errors.i18n_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
+ ActiveModel::Errors.i18n_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } }
+ })
+
+ person = Person.new
+ assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
# ActiveModel::Validations
# A set of common cases for ActiveModel::Validations message generation that
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 01b78ae72e..ca3c3bc40d 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -262,6 +262,16 @@ class NumericalityValidationTest < ActiveModel::TestCase
Person.clear_validators!
end
+ def test_validates_numericality_using_value_before_type_cast_if_possible
+ Topic.validates_numericality_of :price
+
+ topic = Topic.new(price: 50)
+
+ assert_equal "$50.00", topic.price
+ assert_equal 50, topic.price_before_type_cast
+ assert_predicate topic, :valid?
+ end
+
def test_validates_numericality_with_exponent_number
base = 10_000_000_000_000_000
Topic.validates_numericality_of :approved, less_than_or_equal_to: base
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index b0af00ee45..db3284f833 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -3,6 +3,11 @@
class Topic
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
+ include ActiveModel::AttributeMethods
+ include ActiveSupport::NumberHelper
+
+ attribute_method_suffix "_before_type_cast"
+ define_attribute_method :price
def self._validates_default_keys
super | [ :message ]
@@ -10,6 +15,7 @@ class Topic
attr_accessor :title, :author_name, :content, :approved, :created_at
attr_accessor :after_validation_performed
+ attr_writer :price
after_validation :perform_after_validation
@@ -38,4 +44,12 @@ class Topic
def my_validation_with_arg(attr)
errors.add attr, "is missing" unless send(attr)
end
+
+ def price
+ number_to_currency @price
+ end
+
+ def attribute_before_type_cast(attr)
+ instance_variable_get(:"@#{attr}")
+ end
end