diff options
Diffstat (limited to 'activemodel')
-rw-r--r-- | activemodel/CHANGELOG.md | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute_mutation_tracker.rb | 17 | ||||
-rw-r--r-- | activemodel/lib/active_model/attributes.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 108 | ||||
-rw-r--r-- | activemodel/lib/active_model/type/binary.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/type/helpers/time_value.rb | 1 | ||||
-rw-r--r-- | activemodel/lib/active_model/type/time.rb | 10 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/inclusion.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/validates.rb | 2 | ||||
-rw-r--r-- | activemodel/test/cases/attributes_dirty_test.rb | 2 | ||||
-rw-r--r-- | activemodel/test/cases/dirty_test.rb | 2 | ||||
-rw-r--r-- | activemodel/test/cases/errors_test.rb | 8 | ||||
-rw-r--r-- | activemodel/test/cases/naming_test.rb | 2 | ||||
-rw-r--r-- | activemodel/test/cases/secure_password_test.rb | 24 | ||||
-rw-r--r-- | activemodel/test/cases/type/time_test.rb | 15 | ||||
-rw-r--r-- | activemodel/test/cases/validations/validates_test.rb | 2 |
16 files changed, 125 insertions, 76 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index b28c83e4ed..6b557a7cb1 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,3 @@ -## Rails 6.0.0.alpha (Unreleased) ## - * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index 8e92c8807f..6abf37bd44 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -11,6 +11,10 @@ module ActiveModel @forced_changes = Set.new end + def changed_attribute_names + attr_names.select { |attr_name| changed?(attr_name) } + end + def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) @@ -23,7 +27,7 @@ module ActiveModel attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| change = change_to_attribute(attr_name) if change - result[attr_name] = change + result.merge!(attr_name => change) end end end @@ -35,10 +39,6 @@ module ActiveModel end end - def changed_attribute_names - attr_names.select { |attr| changed?(attr) } - end - def any_changes? attr_names.any? { |attr| changed?(attr) } end @@ -80,6 +80,10 @@ module ActiveModel class NullMutationTracker # :nodoc: include Singleton + def changed_attribute_names(*) + [] + end + def changed_values(*) {} end @@ -108,5 +112,8 @@ module ActiveModel def original_value(*) end + + def force_change(*) + end end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index 4083bf827b..7d44f7f2a3 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -29,7 +29,7 @@ module ActiveModel private def define_method_attribute=(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 0044fde6c5..eaf8dfb223 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -3,7 +3,6 @@ require "active_support/hash_with_indifferent_access" require "active_support/core_ext/object/duplicable" require "active_model/attribute_mutation_tracker" -require "active_model/attribute_set" module ActiveModel # == Active \Model \Dirty @@ -143,8 +142,11 @@ module ActiveModel end def changes_applied # :nodoc: - _prepare_changes + unless defined?(@attributes) + @previously_changed = changes + end @mutations_before_last_save = mutations_from_database + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end @@ -155,7 +157,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - mutations_from_database.any_changes? + changed_attributes.present? end # Returns an array with the name of the attributes with unsaved changes. @@ -164,24 +166,24 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - mutations_from_database.changed_attribute_names + changed_attributes.keys end # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: - !!mutations_from_database.changed?(attr) && + !!changes_include?(attr) && (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && - (from == OPTION_NOT_GIVEN || from == attribute_was(attr)) + (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) end # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: - mutations_from_database.original_value(attr) + attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. def attribute_previously_changed?(attr) #:nodoc: - mutations_before_last_save.changed?(attr) + previous_changes_include?(attr) end # Restore all previous data of the provided attributes. @@ -191,12 +193,15 @@ module ActiveModel # Clears all dirty data: current changes and previous changes. def clear_changes_information + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end def clear_attribute_changes(attr_names) + attributes_changed_by_setter.except!(*attr_names) attr_names.each do |attr_name| clear_attribute_change(attr_name) end @@ -209,7 +214,13 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - mutations_from_database.changed_values.freeze + # This should only be set by methods which will call changed_attributes + # multiple times when it is known that the computed value cannot change. + if defined?(@cached_changed_attributes) + @cached_changed_attributes + else + attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze + end end # Returns a hash of changed attributes indicating their original @@ -219,8 +230,9 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - _prepare_changes - mutations_from_database.changes + cache_changed_attributes do + ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] + end end # Returns a hash of attributes that were changed before the model was saved. @@ -230,7 +242,8 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - mutations_before_last_save.changes + @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new + @previously_changed.merge(mutations_before_last_save.changes) end def attribute_changed_in_place?(attr_name) # :nodoc: @@ -246,17 +259,11 @@ module ActiveModel unless defined?(@mutations_from_database) @mutations_from_database = nil end - - unless defined?(@attributes) - @_pseudo_attributes = true - @attributes = AttributeSet.new( - Hash.new { |h, attr| - h[attr] = Attribute.with_cast_value(attr, _clone_attribute(attr), Type.default_value) - } - ) + @mutations_from_database ||= if defined?(@attributes) + ActiveModel::AttributeMutationTracker.new(@attributes) + else + NullMutationTracker.instance end - - @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes) end def forget_attribute_assignments @@ -267,45 +274,68 @@ module ActiveModel @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance end + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + clear_changed_attributes_cache + end + + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) + end + + # Returns +true+ if attr_name is changed, +false+ otherwise. + def changes_include?(attr_name) + attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name) + end + alias attribute_changed_by_setter? changes_include? + + # Returns +true+ if attr_name were changed before the model was saved, + # +false+ otherwise. + def previous_changes_include?(attr_name) + previous_changes.include?(attr_name) + end + # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) - [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr) + [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - mutations_before_last_save.change_to_attribute(attr) + previous_changes[attr] if attribute_previously_changed?(attr) end # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) - attr = attr.to_s - mutations_from_database.force_change(attr).tap do - @attributes[attr] if defined?(@_pseudo_attributes) + unless attribute_changed?(attr) + begin + value = _read_attribute(attr) + value = value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + end + + set_attribute_was(attr, value) end + mutations_from_database.force_change(attr) end # Handles <tt>restore_*!</tt> for +method_missing+. def restore_attribute!(attr) if attribute_changed?(attr) - __send__("#{attr}=", attribute_was(attr)) + __send__("#{attr}=", changed_attributes[attr]) clear_attribute_changes([attr]) end end - def _prepare_changes - if defined?(@_pseudo_attributes) - changed.each do |attr| - @attributes.write_from_user(attr, _read_attribute(attr)) - end - end + def attributes_changed_by_setter + @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new end - def _clone_attribute(attr) - value = _read_attribute(attr) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value + # Force an attribute to have a particular "before" value + def set_attribute_was(attr, old_value) + attributes_changed_by_setter[attr] = old_value end end end diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb index dc2eca18be..76203c5a88 100644 --- a/activemodel/lib/active_model/type/binary.rb +++ b/activemodel/lib/active_model/type/binary.rb @@ -40,7 +40,7 @@ module ActiveModel alias_method :to_str, :to_s def hex - @value.unpack("H*")[0] + @value.unpack1("H*") end def ==(other) diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 250c4021c6..cb6aa67a9d 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/string/zones" require "active_support/core_ext/time/zones" module ActiveModel diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index ad7ba0351a..b3056b1333 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -18,6 +18,8 @@ module ActiveModel case value when ::String value = "2000-01-01 #{value}" + time_hash = ::Date._parse(value) + return if time_hash[:hour].nil? when ::Time value = value.change(year: 2000, day: 1, month: 1) end @@ -28,14 +30,10 @@ module ActiveModel private def cast_value(value) - return value unless value.is_a?(::String) + return apply_seconds_precision(value) unless value.is_a?(::String) return if value.empty? - if value.start_with?("2000-01-01") - dummy_time_value = value - else - dummy_time_value = "2000-01-01 #{value}" - end + dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ") fast_string_to_time(dummy_time_value) || begin time_hash = ::Date._parse(dummy_time_value) diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 3104e7e329..9c12dc14c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -19,7 +19,7 @@ module ActiveModel # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, in: %w( m f ) + # validates_inclusion_of :role, in: %w( admin contributor ) # validates_inclusion_of :age, in: 0..99 # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list" # validates_inclusion_of :states, in: ->(person) { STATES[person.country] } diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index e28e7e9219..88cca318ef 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -63,7 +63,7 @@ module ActiveModel # and strings in shortcut form. # # validates :email, format: /@/ - # validates :gender, inclusion: %w(male female) + # validates :role, inclusion: %(admin contributor) # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb index c991176389..f9693a23cd 100644 --- a/activemodel/test/cases/attributes_dirty_test.rb +++ b/activemodel/test/cases/attributes_dirty_test.rb @@ -39,7 +39,7 @@ class AttributesDirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index f769eb0da1..b120e68027 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -78,7 +78,7 @@ class DirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index cb6a8c43d5..6ff3be1308 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -207,26 +207,26 @@ class ErrorsTest < ActiveModel::TestCase test "added? returns false when no errors are present" do person = Person.new - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do person = Person.new person.errors.add(:name, "is invalid") - assert !person.errors.added?(:name, "cannot be blank") + assert_not person.errors.added?(:name, "cannot be blank") end test "added? returns false when checking for an error, but not providing message arguments" do person = Person.new person.errors.add(:name, "cannot be blank") - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking for an error by symbol and a different error with same message is present" do I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) person = Person.new person.errors.add(:name, :wrong) - assert !person.errors.added?(:name, :used) + assert_not person.errors.added?(:name, :used) end test "size calculates the number of error messages" do diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 009f1f47af..4693da434c 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -248,7 +248,7 @@ class NamingHelpersTest < ActiveModel::TestCase def test_uncountable assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable" - assert !uncountable?(@klass), "Expected 'contact' to be countable" + assert_not uncountable?(@klass), "Expected 'contact' to be countable" end def test_uncountable_route_key diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index d19e81a119..c347aa9b24 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -49,14 +49,14 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and a blank password" do @user.password = "" - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["can't be blank"], @user.errors[:password] end test "create a new user with validation and a nil password" do @user.password = nil - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["can't be blank"], @user.errors[:password] end @@ -64,7 +64,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and password length greater than 72" do @user.password = "a" * 73 @user.password_confirmation = "a" * 73 - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password] end @@ -72,7 +72,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and a blank password confirmation" do @user.password = "password" @user.password_confirmation = "" - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] end @@ -86,7 +86,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and an incorrect password confirmation" do @user.password = "password" @user.password_confirmation = "something else" - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] end @@ -125,7 +125,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and a nil password" do @existing_user.password = nil - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["can't be blank"], @existing_user.errors[:password] end @@ -133,7 +133,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and password length greater than 72" do @existing_user.password = "a" * 73 @existing_user.password_confirmation = "a" * 73 - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password] end @@ -141,7 +141,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and a blank password confirmation" do @existing_user.password = "password" @existing_user.password_confirmation = "" - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] end @@ -155,21 +155,21 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and an incorrect password confirmation" do @existing_user.password = "password" @existing_user.password_confirmation = "something else" - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] end test "updating an existing user with validation and a blank password digest" do @existing_user.password_digest = "" - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["can't be blank"], @existing_user.errors[:password] end test "updating an existing user with validation and a nil password digest" do @existing_user.password_digest = nil - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["can't be blank"], @existing_user.errors[:password] end @@ -187,7 +187,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "authenticate" do @user.password = "secret" - assert !@user.authenticate("wrong") + assert_not @user.authenticate("wrong") assert @user.authenticate("secret") end diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb index f7102d1e97..3fbae1a169 100644 --- a/activemodel/test/cases/type/time_test.rb +++ b/activemodel/test/cases/type/time_test.rb @@ -17,6 +17,21 @@ module ActiveModel assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00") assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00") end + + def test_user_input_in_time_zone + ::Time.use_zone("Pacific Time (US & Canada)") do + type = Type::Time.new + assert_nil type.user_input_in_time_zone(nil) + assert_nil type.user_input_in_time_zone("") + assert_nil type.user_input_in_time_zone("ABC") + + offset = ::Time.zone.formatted_offset + time_string = "2015-02-09T19:45:54#{offset}" + + assert_equal 19, type.user_input_in_time_zone(time_string).hour + assert_equal offset, type.user_input_in_time_zone(time_string).formatted_offset + end + end end end end diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 80c347703a..ae5a875c24 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -19,7 +19,7 @@ class ValidatesTest < ActiveModel::TestCase def test_validates_with_messages_empty Person.validates :title, presence: { message: "" } person = Person.new - assert !person.valid?, "person should not be valid." + assert_not person.valid?, "person should not be valid." end def test_validates_with_built_in_validation |