diff options
Diffstat (limited to 'activemodel')
-rw-r--r-- | activemodel/CHANGELOG.md | 30 | ||||
-rw-r--r-- | activemodel/lib/active_model/callbacks.rb | 16 | ||||
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/errors.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/naming.rb | 18 | ||||
-rw-r--r-- | activemodel/lib/active_model/secure_password.rb | 12 | ||||
-rw-r--r-- | activemodel/lib/active_model/serializers/json.rb | 19 | ||||
-rw-r--r-- | activemodel/lib/active_model/type/helpers/time_value.rb | 8 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/numericality.rb | 13 | ||||
-rw-r--r-- | activemodel/test/cases/dirty_test.rb | 16 | ||||
-rw-r--r-- | activemodel/test/cases/errors_test.rb | 6 | ||||
-rw-r--r-- | activemodel/test/cases/secure_password_test.rb | 13 | ||||
-rw-r--r-- | activemodel/test/cases/serializers/json_serialization_test.rb | 6 | ||||
-rw-r--r-- | activemodel/test/cases/validations/numericality_validation_test.rb | 10 | ||||
-rw-r--r-- | activemodel/test/models/topic.rb | 14 | ||||
-rw-r--r-- | activemodel/test/models/user.rb | 4 |
16 files changed, 134 insertions, 55 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 1a464c2ffd..0b2fa37787 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,25 @@ +* 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 @@ -6,13 +28,13 @@ Example: class User < ActiveRecord::Base - has_secure_password :activation_token, validations: false + has_secure_password :recovery_password, validations: false end user = User.new() - user.activation_token = "a_new_token" - user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3Be..." - user.authenticate_activation_token('a_new_token') # => user + user.recovery_password = "42password" + user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..." + user.authenticate_recovery_password('42password') # => user *Unathi Chonco* diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 8fa9680cb1..fde3381df2 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -127,26 +127,28 @@ module ActiveModel private def _define_before_model_callback(klass, callback) - klass.define_singleton_method("before_#{callback}") do |*args, &block| - set_callback(:"#{callback}", :before, *args, &block) + klass.define_singleton_method("before_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) + set_callback(:"#{callback}", :before, *args, options, &block) end end def _define_around_model_callback(klass, callback) - klass.define_singleton_method("around_#{callback}") do |*args, &block| - set_callback(:"#{callback}", :around, *args, &block) + klass.define_singleton_method("around_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) + set_callback(:"#{callback}", :around, *args, options, &block) end end def _define_after_model_callback(klass, callback) - klass.define_singleton_method("after_#{callback}") do |*args, &block| - options = args.extract_options! + klass.define_singleton_method("after_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) options[:prepend] = true conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v| v != false } options[:if] = Array(options[:if]) << conditional - set_callback(:"#{callback}", :after, *(args << options), &block) + set_callback(:"#{callback}", :after, *args, options, &block) end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index eaf8dfb223..093052a70c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -304,7 +304,7 @@ module ActiveModel # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + previous_changes[attr] end # Handles <tt>*_will_change!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 56404a036c..edc30ee64d 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -328,7 +328,7 @@ module ActiveModel # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) if message.is_a? Symbol - self.details[attribute].map { |e| e[:error] }.include? message + self.details[attribute.to_sym].map { |e| e[:error] }.include? message else message = message.call if message.respond_to?(:call) message = normalize_message(attribute, message, options) diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index dfccd03cd8..983401801f 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -111,6 +111,22 @@ module ActiveModel # BlogPost.model_name.eql?('Blog Post') # => false ## + # :method: match? + # + # :call-seq: + # match?(regexp) + # + # Equivalent to <tt>String#match?</tt>. Match the class name against the + # given regexp. Returns +true+ if there is a match, otherwise +false+. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name.match?(/Post/) # => true + # BlogPost.model_name.match?(/\d/) # => false + + ## # :method: to_s # # :call-seq: @@ -131,7 +147,7 @@ module ActiveModel # to_str() # # Equivalent to +to_s+. - delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s, :to_str, :as_json, to: :name # Returns a new ActiveModel::Name instance. By default, the +namespace+ diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 7f3763fa56..51d54f34f3 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -17,7 +17,7 @@ module ActiveModel module ClassMethods # Adds methods to set and authenticate against a BCrypt password. # This mechanism requires you to have a +XXX_digest+ attribute. - # Where +XXX+ is the attribute name of your desired password/token or defaults to +password+ + # Where +XXX+ is the attribute name of your desired password. # # The following validations are added automatically: # * Password must be present on creation @@ -38,10 +38,10 @@ module ActiveModel # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # - # # Schema: User(name:string, password_digest:string, activation_token_digest:string) + # # Schema: User(name:string, password_digest:string, recovery_password_digest:string) # class User < ActiveRecord::Base # has_secure_password - # has_secure_password :activation_token, validations: false + # has_secure_password :recovery_password, validations: false # end # # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') @@ -50,12 +50,12 @@ module ActiveModel # user.save # => false, confirmation doesn't match # user.password_confirmation = 'mUc3m00RsqyRe' # user.save # => true - # user.activation_token = "a_new_token" - # user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3BeXO5tPOA85b6xazE9rp8nF2MIJlsUik." + # user.recovery_password = "42password" + # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC" # user.save # => true # user.authenticate('notright') # => false # user.authenticate('mUc3m00RsqyRe') # => user - # user.authenticate_activation_token('a_new_token') # => user + # user.authenticate_recovery_password('42password') # => user # User.find_by(name: 'david').try(:authenticate, 'notright') # => false # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user def has_secure_password(attribute = :password, validations: true) 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/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/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index b120e68027..b38d84fff2 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -14,37 +14,23 @@ class DirtyTest < ActiveModel::TestCase @status = "initialized" end - def name - @name - end + attr_reader :name, :color, :size, :status def name=(val) name_will_change! @name = val end - def color - @color - end - def color=(val) color_will_change! unless val == @color @color = val end - def size - @size - end - def size=(val) attribute_will_change!(:size) unless val == @size @size = val end - def status - @status - end - def status=(val) status_will_change! unless val == @status @status = val diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 6ff3be1308..41ff6443fe 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -185,6 +185,12 @@ class ErrorsTest < ActiveModel::TestCase assert person.errors.added?(:name, :blank) end + test "added? returns true when string attribute is used with a symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.added?("name", :blank) + end + test "added? handles proc messages" do person = Person.new message = Proc.new { "cannot be blank" } diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index bc23316ad5..9ef1148be8 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -186,13 +186,16 @@ class SecurePasswordTest < ActiveModel::TestCase test "authenticate" do @user.password = "secret" - @user.activation_token = "new_token" + @user.recovery_password = "42password" - assert_not @user.authenticate("wrong") - assert @user.authenticate("secret") + assert_equal false, @user.authenticate("wrong") + assert_equal @user, @user.authenticate("secret") - assert !@user.authenticate_activation_token("wrong") - assert @user.authenticate_activation_token("new_token") + assert_equal false, @user.authenticate_password("wrong") + assert_equal @user, @user.authenticate_password("secret") + + assert_equal false, @user.authenticate_recovery_password("wrong") + assert_equal @user, @user.authenticate_recovery_password("42password") end test "Password digest cost defaults to bcrypt default cost when min_cost is false" do 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/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 diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index 1ff3379153..bb1b187694 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -7,7 +7,7 @@ class User define_model_callbacks :create has_secure_password - has_secure_password :activation_token, validations: false + has_secure_password :recovery_password, validations: false - attr_accessor :password_digest, :activation_token_digest + attr_accessor :password_digest, :recovery_password_digest end |