require 'cases/helper' require 'models/topic' # For booleans require 'models/pirate' # For timestamps require 'models/parrot' require 'models/person' # For optimistic locking class Pirate # Just reopening it, not defining it attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected attr_accessor :changes_detected_in_after_update # Actual changes after_update :check_changes private # after_save/update in sweepers, observers, and the model itself # can end up checking dirty status and acting on the results def check_changes if self.changed? self.detected_changes_in_after_update = true self.changes_detected_in_after_update = self.changes end end end class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' end class DirtyTest < ActiveRecord::TestCase def test_attribute_changes # New record - no changes. pirate = Pirate.new assert !pirate.catchphrase_changed? assert_nil pirate.catchphrase_change # Change catchphrase. pirate.catchphrase = 'arrr' assert pirate.catchphrase_changed? assert_nil pirate.catchphrase_was assert_equal [nil, 'arrr'], pirate.catchphrase_change # Saved - no changes. pirate.save! assert !pirate.catchphrase_changed? assert_nil pirate.catchphrase_change # Same value - no changes. pirate.catchphrase = 'arrr' assert !pirate.catchphrase_changed? assert_nil pirate.catchphrase_change end def test_aliased_attribute_changes # the actual attribute here is name, title is an # alias setup via alias_attribute parrot = Parrot.new assert !parrot.title_changed? assert_nil parrot.title_change parrot.name = 'Sam' assert parrot.title_changed? assert_nil parrot.title_was assert_equal parrot.name_change, parrot.title_change end def test_nullable_number_not_marked_as_changed_if_new_value_is_blank pirate = Pirate.new ["", nil].each do |value| pirate.parrot_id = value assert !pirate.parrot_id_changed? assert_nil pirate.parrot_id_change end end def test_nullable_decimal_not_marked_as_changed_if_new_value_is_blank numeric_data = NumericData.new ["", nil].each do |value| numeric_data.bank_balance = value assert !numeric_data.bank_balance_changed? assert_nil numeric_data.bank_balance_change end end def test_nullable_float_not_marked_as_changed_if_new_value_is_blank numeric_data = NumericData.new ["", nil].each do |value| numeric_data.temperature = value assert !numeric_data.temperature_changed? assert_nil numeric_data.temperature_change end end def test_nullable_integer_zero_to_string_zero_not_marked_as_changed pirate = Pirate.new pirate.parrot_id = 0 pirate.catchphrase = 'arrr' assert pirate.save! assert !pirate.changed? pirate.parrot_id = '0' assert !pirate.changed? end def test_zero_to_blank_marked_as_changed pirate = Pirate.new pirate.catchphrase = "Yarrrr, me hearties" pirate.parrot_id = 1 pirate.save # check the change from 1 to '' pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = '' assert pirate.parrot_id_changed? assert_equal([1, nil], pirate.parrot_id_change) pirate.save # check the change from nil to 0 pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = 0 assert pirate.parrot_id_changed? assert_equal([nil, 0], pirate.parrot_id_change) pirate.save # check the change from 0 to '' pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") pirate.parrot_id = '' assert pirate.parrot_id_changed? assert_equal([0, nil], pirate.parrot_id_change) end def test_object_should_be_changed_if_any_attribute_is_changed pirate = Pirate.new assert !pirate.changed? assert_equal [], pirate.changed assert_equal Hash.new, pirate.changes pirate.catchphrase = 'arrr' assert pirate.changed? assert_nil pirate.catchphrase_was assert_equal %w(catchphrase), pirate.changed assert_equal({'catchphrase' => [nil, 'arrr']}, pirate.changes) pirate.save assert !pirate.changed? assert_equal [], pirate.changed assert_equal Hash.new, pirate.changes end def test_attribute_will_change! pirate = Pirate.create!(:catchphrase => 'arr') pirate.catchphrase << ' matey' assert !pirate.catchphrase_changed? assert pirate.catchphrase_will_change! assert pirate.catchphrase_changed? assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change pirate.catchphrase << '!' assert pirate.catchphrase_changed? assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change end def test_association_assignment_changes_foreign_key pirate = Pirate.create!(:catchphrase => 'jarl') pirate.parrot = Parrot.create!(:name => 'Lorre') assert pirate.changed? assert_equal %w(parrot_id), pirate.changed end def test_attribute_should_be_compared_with_type_cast topic = Topic.new assert topic.approved? assert !topic.approved_changed? # Coming from web form. params = {:topic => {:approved => 1}} # In the controller. topic.attributes = params[:topic] assert topic.approved? assert !topic.approved_changed? end def test_partial_update pirate = Pirate.new(:catchphrase => 'foo') old_updated_on = 1.hour.ago.beginning_of_day with_partial_updates Pirate, false do assert_queries(2) { 2.times { pirate.save! } } Pirate.update_all({ :updated_on => old_updated_on }, :id => pirate.id) end with_partial_updates Pirate, true do assert_queries(0) { 2.times { pirate.save! } } assert_equal old_updated_on, pirate.reload.updated_on assert_queries(1) { pirate.catchphrase = 'bar'; pirate.save! } assert_not_equal old_updated_on, pirate.reload.updated_on end end def test_partial_update_with_optimistic_locking person = Person.new(:first_name => 'foo') old_lock_version = 1 with_partial_updates Person, false do assert_queries(2) { 2.times { person.save! } } Person.update_all({ :first_name => 'baz' }, :id => person.id) end with_partial_updates Person, true do assert_queries(0) { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version assert_queries(1) { person.first_name = 'bar'; person.save! } assert_not_equal old_lock_version, person.reload.lock_version end end def test_changed_attributes_should_be_preserved_if_save_failure pirate = Pirate.new pirate.parrot_id = 1 assert !pirate.save check_pirate_after_save_failure(pirate) pirate = Pirate.new pirate.parrot_id = 1 assert_raise(ActiveRecord::RecordInvalid) { pirate.save! } check_pirate_after_save_failure(pirate) end def test_reload_should_clear_changed_attributes pirate = Pirate.create!(:catchphrase => "shiver me timbers") pirate.catchphrase = "*hic*" assert pirate.changed? pirate.reload assert !pirate.changed? end def test_reverted_changes_are_not_dirty phrase = "shiver me timbers" pirate = Pirate.create!(:catchphrase => phrase) pirate.catchphrase = "*hic*" assert pirate.changed? pirate.catchphrase = phrase assert !pirate.changed? end def test_reverted_changes_are_not_dirty_after_multiple_changes phrase = "shiver me timbers" pirate = Pirate.create!(:catchphrase => phrase) 10.times do |i| pirate.catchphrase = "*hic*" * i assert pirate.changed? end assert pirate.changed? pirate.catchphrase = phrase assert !pirate.changed? end def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back pirate = Pirate.create!(:catchphrase => "Yar!") pirate.parrot_id = 1 assert pirate.changed? assert pirate.parrot_id_changed? assert !pirate.catchphrase_changed? pirate.parrot_id = nil assert !pirate.changed? assert !pirate.parrot_id_changed? assert !pirate.catchphrase_changed? end def test_save_should_store_serialized_attributes_even_with_partial_updates with_partial_updates(Topic) do topic = Topic.create!(:content => {:a => "a"}) topic.content[:b] = "b" #assert topic.changed? # Known bug, will fail topic.save! assert_equal "b", topic.content[:b] topic.reload assert_equal "b", topic.content[:b] end end private def with_partial_updates(klass, on = true) old = klass.partial_updates? klass.partial_updates = on yield ensure klass.partial_updates = old end def check_pirate_after_save_failure(pirate) assert pirate.changed? assert pirate.parrot_id_changed? assert_equal %w(parrot_id), pirate.changed assert_nil pirate.parrot_id_was end end