# frozen_string_literal: true
require "active_support/core_ext/module/attribute_accessors"
module ActiveRecord
module AttributeMethods
module Dirty
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, default: true
after_create { changes_applied }
after_update { changes_applied }
# Attribute methods for "changed in last call to save?"
attribute_method_affix(prefix: "saved_change_to_", suffix: "?")
attribute_method_prefix("saved_change_to_")
attribute_method_suffix("_before_last_save")
# Attribute methods for "will change if I call save?"
attribute_method_affix(prefix: "will_save_change_to_", suffix: "?")
attribute_method_suffix("_change_to_be_saved", "_in_database")
end
# reload the record and clears changed attributes.
def reload(*)
super.tap do
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
@mutations_from_database = nil
end
end
# Did this attribute change when we last saved? This method can be invoked
# as +saved_change_to_name?+ instead of saved_change_to_attribute?("name").
# Behaves similarly to +attribute_changed?+. This method is useful in
# after callbacks to determine if the call to save changed a certain
# attribute.
#
# ==== Options
#
# +from+ When passed, this method will return false unless the original
# value is equal to the given option
#
# +to+ When passed, this method will return false unless the value was
# changed to the given value
def saved_change_to_attribute?(attr_name, **options)
mutations_before_last_save.changed?(attr_name, **options)
end
# Returns the change to an attribute during the last save. If the
# attribute was changed, the result will be an array containing the
# original value and the saved value.
#
# Behaves similarly to +attribute_change+. This method is useful in after
# callbacks, to see the change in an attribute that just occurred
#
# This method can be invoked as +saved_change_to_name+ in instead of
# saved_change_to_attribute("name")
def saved_change_to_attribute(attr_name)
mutations_before_last_save.change_to_attribute(attr_name)
end
# Returns the original value of an attribute before the last save.
# Behaves similarly to +attribute_was+. This method is useful in after
# callbacks to get the original value of an attribute before the save that
# just occurred
def attribute_before_last_save(attr_name)
mutations_before_last_save.original_value(attr_name)
end
# Did the last call to +save+ have any changes to change?
def saved_changes?
mutations_before_last_save.any_changes?
end
# Returns a hash containing all the changes that were just saved.
def saved_changes
mutations_before_last_save.changes
end
# Alias for +attribute_changed?+
def will_save_change_to_attribute?(attr_name, **options)
mutations_from_database.changed?(attr_name, **options)
end
# Alias for +attribute_change+
def attribute_change_to_be_saved(attr_name)
mutations_from_database.change_to_attribute(attr_name)
end
# Alias for +attribute_was+
def attribute_in_database(attr_name)
mutations_from_database.original_value(attr_name)
end
# Alias for +changed?+
def has_changes_to_save?
mutations_from_database.any_changes?
end
# Alias for +changes+
def changes_to_save
mutations_from_database.changes
end
# Alias for +changed+
def changed_attribute_names_to_save
mutations_from_database.changed_attribute_names
end
# Alias for +changed_attributes+
def attributes_in_database
mutations_from_database.changed_values
end
private
def write_attribute_without_type_cast(attr_name, _)
result = super
clear_attribute_change(attr_name)
result
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_attribute_names_to_save & self.class.column_names
end
end
end
end