Permalink
Browse files

Track changes to unsaved attributes

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@9127 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
jeremy committed Mar 29, 2008
1 parent 3704f4b commit bbf738f269bac83bcc8a0100455c69cba638d877
View
@@ -1,5 +1,7 @@
*SVN*
+* Track changes to unsaved attributes. [Jeremy Kemper]
+
* Switched to UTC-timebased version numbers for migrations and the schema. This will as good as eliminate the problem of multiple migrations getting the same version assigned in different branches. Also added rake db:migrate:up/down to apply individual migrations that may need to be run when you merge branches #11458 [jbarnette]
* Fixed that has_many :through would ignore the hash conditions #11447 [miloops]
@@ -55,6 +55,7 @@
require 'active_record/calculations'
require 'active_record/serialization'
require 'active_record/attribute_methods'
+require 'active_record/dirty'
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
@@ -73,6 +74,7 @@
include ActiveRecord::Calculations
include ActiveRecord::Serialization
include ActiveRecord::AttributeMethods
+ include ActiveRecord::Dirty
end
require 'active_record/connection_adapters/abstract_adapter'
@@ -0,0 +1,117 @@
+module ActiveRecord
+ # Track unsaved attribute changes.
+ #
+ # A newly instantiated object is unchanged:
+ # person = Person.find_by_name('uncle bob')
+ # person.changed? # => false
+ #
+ # Change the name:
+ # person.name = 'Bob'
+ # person.changed? # => true
+ # person.name_changed? # => true
+ # person.name_was # => 'uncle bob'
+ # person.name_change # => ['uncle bob', 'Bob']
+ # person.name = 'Bill'
+ # person.name_change # => ['uncle bob', 'Bill']
+ #
+ # Save the changes:
+ # person.save
+ # person.changed? # => false
+ # person.name_changed? # => false
+ #
+ # Assigning the same value leaves the attribute unchanged:
+ # person.name = 'Bill'
+ # person.name_changed? # => false
+ # person.name_change # => nil
+ #
+ # Which attributes have changed?
+ # person.name = 'bob'
+ # person.changed # => ['name']
+ # person.changes # => { 'name' => ['Bill', 'bob'] }
+ module Dirty
+ def self.included(base)
+ base.attribute_method_suffix '_changed?', '_change', '_was'
+ base.alias_method_chain :write_attribute, :dirty
+ base.alias_method_chain :save, :dirty
+ base.alias_method_chain :save!, :dirty
+ end
+
+ # Do any attributes have unsaved changes?
+ # person.changed? # => false
+ # person.name = 'bob'
+ # person.changed? # => true
+ def changed?
+ !changed_attributes.empty?
+ end
+
+ # List of attributes with unsaved changes.
+ # person.changed # => []
+ # person.name = 'bob'
+ # person.changed # => ['name']
+ def changed
+ changed_attributes.keys
+ end
+
+ # Map of changed attrs => [original value, new value]
+ # person.changes # => {}
+ # person.name = 'bob'
+ # person.changes # => { 'name' => ['bill', 'bob'] }
+ def changes
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
+ end
+
+
+ # Clear changed attributes after they are saved.
+ def save_with_dirty(*args) #:nodoc:
+ save_without_dirty(*args)
+ ensure
+ changed_attributes.clear
+ end
+
+ # Clear changed attributes after they are saved.
+ def save_with_dirty!(*args) #:nodoc:
+ save_without_dirty!(*args)
+ ensure
+ changed_attributes.clear
+ end
+
+ private
+ # Map of change attr => original value.
+ def changed_attributes
+ @changed_attributes ||= {}
+ end
+
+
+ # Wrap write_attribute to remember original attribute value.
+ def write_attribute_with_dirty(attr, value)
+ attr = attr.to_s
+
+ # The attribute already has an unsaved change.
+ unless changed_attributes.include?(attr)
+ old = read_attribute(attr)
+
+ # Remember the original value if it's different.
+ changed_attributes[attr] = old unless old == value
+ end
+
+ # Carry on.
+ write_attribute_without_dirty(attr, value)
+ end
+
+
+ # Handle *_changed? for method_missing.
+ def attribute_changed?(attr)
+ changed_attributes.include?(attr)
+ end
+
+ # Handle *_change for method_missing.
+ def attribute_change(attr)
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
+ end
+
+ # Handle *_was for method_missing.
+ def attribute_was(attr)
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
+ end
+ end
+end
@@ -0,0 +1,80 @@
+require 'cases/helper'
+
+# Stub out an AR-alike.
+class DirtyTestSubject
+ def self.table_name; 'people' end
+ def self.primary_key; 'id' end
+ def self.attribute_method_suffix(*suffixes) suffixes end
+
+ def initialize(attrs = {}) @attributes = attrs end
+
+ def save
+ changed_attributes.clear
+ end
+
+ alias_method :save!, :save
+
+ def name; read_attribute('name') end
+ def name=(value); write_attribute('name', value) end
+ def name_was; attribute_was('name') end
+ def name_change; attribute_change('name') end
+ def name_changed?; attribute_changed?('name') end
+
+ private
+ def define_read_methods; nil end
+
+ def read_attribute(attr)
+ @attributes[attr]
+ end
+
+ def write_attribute(attr, value)
+ @attributes[attr] = value
+ end
+end
+
+# Include the module after the class is all set up.
+DirtyTestSubject.module_eval { include ActiveRecord::Dirty }
+
+
+class DirtyTest < Test::Unit::TestCase
+ def test_attribute_changes
+ # New record - no changes.
+ person = DirtyTestSubject.new
+ assert !person.name_changed?
+ assert_nil person.name_change
+
+ # Change name.
+ person.name = 'a'
+ assert person.name_changed?
+ assert_nil person.name_was
+ assert_equal [nil, 'a'], person.name_change
+
+ # Saved - no changes.
+ person.save!
+ assert !person.name_changed?
+ assert_nil person.name_change
+
+ # Same value - no changes.
+ person.name = 'a'
+ assert !person.name_changed?
+ assert_nil person.name_change
+ end
+
+ def test_object_should_be_changed_if_any_attribute_is_changed
+ person = DirtyTestSubject.new
+ assert !person.changed?
+ assert_equal [], person.changed
+ assert_equal Hash.new, person.changes
+
+ person.name = 'a'
+ assert person.changed?
+ assert_nil person.name_was
+ assert_equal %w(name), person.changed
+ assert_equal({'name' => [nil, 'a']}, person.changes)
+
+ person.save
+ assert !person.changed?
+ assert_equal [], person.changed
+ assert_equal({}, person.changes)
+ end
+end

0 comments on commit bbf738f

Please sign in to comment.