diff options
Diffstat (limited to 'activemodel')
-rw-r--r-- | activemodel/CHANGELOG.md | 7 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute/user_provided_default.rb | 22 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute_mutation_tracker.rb | 7 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute_set.rb | 1 | ||||
-rw-r--r-- | activemodel/lib/active_model/attributes.rb | 3 | ||||
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 106 | ||||
-rw-r--r-- | activemodel/test/cases/attributes_test.rb | 9 | ||||
-rw-r--r-- | activemodel/test/cases/dirty_test.rb | 27 | ||||
-rw-r--r-- | activemodel/test/cases/validations/format_validation_test.rb | 2 |
9 files changed, 108 insertions, 76 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index b67a803b9d..86353674d9 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,7 +1,14 @@ +* Models using the attributes API with a proc default can now be marshalled. + + Fixes #31216. + + *Sean Griffin* + * Fix to working before/after validation callbacks on multiple contexts. *Yoshiyuki Hirano* + ## Rails 5.2.0.beta2 (November 28, 2017) ## * No changes. diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb index f274b687d4..a5dc6188d2 100644 --- a/activemodel/lib/active_model/attribute/user_provided_default.rb +++ b/activemodel/lib/active_model/attribute/user_provided_default.rb @@ -22,6 +22,28 @@ module ActiveModel self.class.new(name, user_provided_value, type, original_attribute) end + def marshal_dump + result = [ + name, + value_before_type_cast, + type, + original_attribute, + ] + result << value if defined?(@value) + result + end + + def marshal_load(values) + name, user_provided_value, type, original_attribute, value = values + @name = name + @user_provided_value = user_provided_value + @type = type + @original_attribute = original_attribute + if values.length == 5 + @value = value + end + end + protected attr_reader :user_provided_value diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index c67e1b809a..f55613ecd5 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -35,6 +35,10 @@ 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 @@ -109,8 +113,5 @@ module ActiveModel def original_value(*) end - - def force_change(*) - end end end diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb index 54a5dd4064..a890ee3932 100644 --- a/activemodel/lib/active_model/attribute_set.rb +++ b/activemodel/lib/active_model/attribute_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set/builder" require "active_model/attribute_set/yaml_encoder" diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index cac461b549..046ae67ad7 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set" require "active_model/attribute/user_provided_default" @@ -24,7 +23,7 @@ module ActiveModel end self.attribute_types = attribute_types.merge(name => type) define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type) - define_attribute_methods(name) + define_attribute_method(name) end private diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index d2ebd18107..0044fde6c5 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -3,6 +3,7 @@ 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 @@ -142,9 +143,8 @@ module ActiveModel end def changes_applied # :nodoc: - @previously_changed = changes + _prepare_changes @mutations_before_last_save = mutations_from_database - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end @@ -155,7 +155,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.present? + mutations_from_database.any_changes? end # Returns an array with the name of the attributes with unsaved changes. @@ -164,24 +164,24 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - changed_attributes.keys + mutations_from_database.changed_attribute_names end # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: - !!changes_include?(attr) && + !!mutations_from_database.changed?(attr) && (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && - (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) + (from == OPTION_NOT_GIVEN || from == attribute_was(attr)) end # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) + mutations_from_database.original_value(attr) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. def attribute_previously_changed?(attr) #:nodoc: - previous_changes_include?(attr) + mutations_before_last_save.changed?(attr) end # Restore all previous data of the provided attributes. @@ -191,15 +191,12 @@ 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 @@ -212,13 +209,7 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - # 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 + mutations_from_database.changed_values.freeze end # Returns a hash of changed attributes indicating their original @@ -228,9 +219,8 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - cache_changed_attributes do - ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] - end + _prepare_changes + mutations_from_database.changes end # Returns a hash of attributes that were changed before the model was saved. @@ -240,8 +230,7 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new - @previously_changed.merge(mutations_before_last_save.changes) + mutations_before_last_save.changes end def attribute_changed_in_place?(attr_name) # :nodoc: @@ -257,11 +246,17 @@ module ActiveModel unless defined?(@mutations_from_database) @mutations_from_database = nil end - @mutations_from_database ||= if defined?(@attributes) - ActiveModel::AttributeMutationTracker.new(@attributes) - else - NullMutationTracker.instance + + 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) + } + ) end + + @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes) end def forget_attribute_assignments @@ -272,68 +267,45 @@ 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) - [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) + [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + mutations_before_last_save.change_to_attribute(attr) end # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) - unless attribute_changed?(attr) - begin - value = _read_attribute(attr) - value = value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - end - - set_attribute_was(attr, value) + attr = attr.to_s + mutations_from_database.force_change(attr).tap do + @attributes[attr] if defined?(@_pseudo_attributes) 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}=", changed_attributes[attr]) + __send__("#{attr}=", attribute_was(attr)) clear_attribute_changes([attr]) end end - def attributes_changed_by_setter - @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new + def _prepare_changes + if defined?(@_pseudo_attributes) + changed.each do |attr| + @attributes.write_from_user(attr, _read_attribute(attr)) + end + end end - # Force an attribute to have a particular "before" value - def set_attribute_was(attr, old_value) - attributes_changed_by_setter[attr] = old_value + def _clone_attribute(attr) + value = _read_attribute(attr) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value end end end diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb index e43bf15335..7c1d813ce0 100644 --- a/activemodel/test/cases/attributes_test.rb +++ b/activemodel/test/cases/attributes_test.rb @@ -64,5 +64,14 @@ module ActiveModel assert_equal "4.4", data.integer_field end + + test "attributes with proc defaults can be marshalled" do + data = ModelForAttributesTest.new + attributes = data.instance_variable_get(:@attributes) + round_tripped = Marshal.load(Marshal.dump(data)) + new_attributes = round_tripped.instance_variable_get(:@attributes) + + assert_equal attributes, new_attributes + end end end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index dfe041ff50..8c183ec516 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -5,12 +5,13 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods :name, :color, :size + define_attribute_methods :name, :color, :size, :status def initialize @name = nil @color = nil @size = nil + @status = "initialized" end def name @@ -40,6 +41,15 @@ class DirtyTest < ActiveModel::TestCase @size = val end + def status + @status + end + + def status=(val) + status_will_change! unless val == @status + @status = val + end + def save changes_applied end @@ -135,15 +145,20 @@ class DirtyTest < ActiveModel::TestCase test "saving should preserve previous changes" do @model.name = "Jericho Cane" + @model.status = "waiting" @model.save assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"] + assert_equal ["initialized", "waiting"], @model.previous_changes["status"] end test "setting new attributes should not affect previous changes" do @model.name = "Jericho Cane" + @model.status = "waiting" @model.save @model.name = "DudeFella ManGuy" + @model.status = "finished" assert_equal [nil, "Jericho Cane"], @model.name_previous_change + assert_equal ["initialized", "waiting"], @model.previous_changes["status"] end test "saving should preserve model's previous changed status" do @@ -155,20 +170,26 @@ class DirtyTest < ActiveModel::TestCase test "previous value is preserved when changed after save" do assert_equal({}, @model.changed_attributes) @model.name = "Paul" - assert_equal({ "name" => nil }, @model.changed_attributes) + @model.status = "waiting" + assert_equal({ "name" => nil, "status" => "initialized" }, @model.changed_attributes) @model.save @model.name = "John" - assert_equal({ "name" => "Paul" }, @model.changed_attributes) + @model.status = "finished" + assert_equal({ "name" => "Paul", "status" => "waiting" }, @model.changed_attributes) end test "changing the same attribute multiple times retains the correct original value" do @model.name = "Otto" + @model.status = "waiting" @model.save @model.name = "DudeFella ManGuy" @model.name = "Mr. Manfredgensonton" + @model.status = "processing" + @model.status = "finished" assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change + assert_equal ["waiting", "finished"], @model.status_change assert_equal @model.name_was, "Otto" end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 3ddda2154a..b0de80a6fc 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -5,7 +5,7 @@ require "cases/helper" require "models/topic" require "models/person" -class PresenceValidationTest < ActiveModel::TestCase +class FormatValidationTest < ActiveModel::TestCase def teardown Topic.clear_validators! end |