require "active_support/core_ext/module/attribute_accessors"
require "active_record/attribute_mutation_tracker"
module ActiveRecord
module AttributeMethods
module Dirty # :nodoc:
extend ActiveSupport::Concern
include ActiveModel::Dirty
included do
if self < ::ActiveRecord::Timestamp
raise "You cannot include Dirty after Timestamp"
end
class_attribute :partial_writes, instance_writer: false
self.partial_writes = true
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
changes_applied
end
status
end
# Attempts to save! the record and clears changed attributes if successful.
def save!(*)
super.tap do
changes_applied
end
end
# reload the record and clears changed attributes.
def reload(*)
super.tap do
@mutation_tracker = nil
@previous_mutation_tracker = nil
@changed_attributes = HashWithIndifferentAccess.new
end
end
def initialize_dup(other) # :nodoc:
super
@attributes = self.class._default_attributes.map do |attr|
attr.with_value_from_user(@attributes.fetch_value(attr.name))
end
@mutation_tracker = nil
end
def changes_applied
@previous_mutation_tracker = mutation_tracker
@changed_attributes = HashWithIndifferentAccess.new
store_original_attributes
end
def clear_changes_information
@previous_mutation_tracker = nil
@changed_attributes = HashWithIndifferentAccess.new
store_original_attributes
end
def raw_write_attribute(attr_name, *)
result = super
clear_attribute_change(attr_name)
result
end
def clear_attribute_changes(attr_names)
super
attr_names.each do |attr_name|
clear_attribute_change(attr_name)
end
end
def changed_attributes
# This should only be set by methods which will call changed_attributes
# multiple times when it is known that the computed value cannot change.
if defined?(@cached_changed_attributes)
@cached_changed_attributes
else
super.reverse_merge(mutation_tracker.changed_values).freeze
end
end
def changes
cache_changed_attributes do
super
end
end
def previous_changes
previous_mutation_tracker.changes
end
def attribute_changed_in_place?(attr_name)
mutation_tracker.changed_in_place?(attr_name)
end
private
def mutation_tracker
unless defined?(@mutation_tracker)
@mutation_tracker = nil
end
@mutation_tracker ||= AttributeMutationTracker.new(@attributes)
end
def changes_include?(attr_name)
super || mutation_tracker.changed?(attr_name)
end
def clear_attribute_change(attr_name)
mutation_tracker.forget_change(attr_name)
end
def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
def _create_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
def keys_for_partial_write
changed & self.class.column_names
end
def store_original_attributes
@attributes = @attributes.map(&:forgetting_assignment)
@mutation_tracker = nil
end
def previous_mutation_tracker
@previous_mutation_tracker ||= NullMutationTracker.instance
end
def cache_changed_attributes
@cached_changed_attributes = changed_attributes
yield
ensure
clear_changed_attributes_cache
end
def clear_changed_attributes_cache
remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
end
end
end
end