Skip to content
This repository
tree: 9606bc8832
Fetching contributors…

Cannot retrieve contributors at this time

file 183 lines (161 sloc) 5.918 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
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'] }
  #
  # Before modifying an attribute in-place:
  # person.name_will_change!
  # person.name << 'by'
  # person.name_change # => ['uncle bob', 'uncle bobby']
  module Dirty
    DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']

    def self.included(base)
      base.attribute_method_suffix *DIRTY_SUFFIXES
      base.alias_method_chain :write_attribute, :dirty
      base.alias_method_chain :save, :dirty
      base.alias_method_chain :save!, :dirty
      base.alias_method_chain :update, :dirty
      base.alias_method_chain :reload, :dirty

      base.superclass_delegating_accessor :partial_updates
      base.partial_updates = true

      base.send(:extend, ClassMethods)
    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

    # Attempts to +save+ the record and clears changed attributes if successful.
    def save_with_dirty(*args) #:nodoc:
      if status = save_without_dirty(*args)
        changed_attributes.clear
      end
      status
    end

    # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
    def save_with_dirty!(*args) #:nodoc:
      status = save_without_dirty!(*args)
      changed_attributes.clear
      status
    end

    # <tt>reload</tt> the record and clears changed attributes.
    def reload_with_dirty(*args) #:nodoc:
      record = reload_without_dirty(*args)
      changed_attributes.clear
      record
    end

    private
      # Map of change <tt>attr => original value</tt>.
      def changed_attributes
        @changed_attributes ||= {}
      end

      # Handle <tt>*_changed?</tt> for +method_missing+.
      def attribute_changed?(attr)
        changed_attributes.include?(attr)
      end

      # Handle <tt>*_change</tt> for +method_missing+.
      def attribute_change(attr)
        [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
      end

      # Handle <tt>*_was</tt> for +method_missing+.
      def attribute_was(attr)
        attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
      end

      # Handle <tt>*_will_change!</tt> for +method_missing+.
      def attribute_will_change!(attr)
        changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
      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.
        if changed_attributes.include?(attr)
          old = changed_attributes[attr]
          changed_attributes.delete(attr) unless field_changed?(attr, old, value)
        else
          old = clone_attribute_value(:read_attribute, attr)
          changed_attributes[attr] = old if field_changed?(attr, old, value)
        end

        # Carry on.
        write_attribute_without_dirty(attr, value)
      end

      def update_with_dirty
        if partial_updates?
          # Serialized attributes should always be written in case they've been
          # changed in place.
          update_without_dirty(changed | self.class.serialized_attributes.keys)
        else
          update_without_dirty
        end
      end

      def field_changed?(attr, old, value)
        if column = column_for_attribute(attr)
          if column.number? && column.null && (old.nil? || old == 0) && value.blank?
            # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
            # Hence we don't record it as a change if the value changes from nil to ''.
            # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
            # be typecast back to 0 (''.to_i => 0)
            value = nil
          else
            value = column.type_cast(value)
          end
        end

        old != value
      end

    module ClassMethods
      def self.extended(base)
        base.metaclass.alias_method_chain(:alias_attribute, :dirty)
      end

      def alias_attribute_with_dirty(new_name, old_name)
        alias_attribute_without_dirty(new_name, old_name)
        DIRTY_SUFFIXES.each do |suffix|
          module_eval <<-STR, __FILE__, __LINE__+1
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
STR
        end
      end
    end
  end
end
Something went wrong with that request. Please try again.