Skip to content
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
@@ -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.
You can’t perform that action at this time.