aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG2
-rwxr-xr-xactiverecord/lib/active_record.rb2
-rw-r--r--activerecord/lib/active_record/dirty.rb117
-rw-r--r--activerecord/test/cases/dirty_test.rb80
4 files changed, 201 insertions, 0 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index eed88ce4ae..71491db213 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -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]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index cedcd8b0a8..3fc40291ba 100755
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -55,6 +55,7 @@ require 'active_record/schema'
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 @@ ActiveRecord::Base.class_eval do
include ActiveRecord::Calculations
include ActiveRecord::Serialization
include ActiveRecord::AttributeMethods
+ include ActiveRecord::Dirty
end
require 'active_record/connection_adapters/abstract_adapter'
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
new file mode 100644
index 0000000000..a9ae2b148c
--- /dev/null
+++ b/activerecord/lib/active_record/dirty.rb
@@ -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
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
new file mode 100644
index 0000000000..c6777f6bfd
--- /dev/null
+++ b/activerecord/test/cases/dirty_test.rb
@@ -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