lib/mongoid/changeable.rb
# encoding: utf-8
module Mongoid
# Defines behaviour for dirty tracking.
#
# @since 4.0.0
module Changeable
extend ActiveSupport::Concern
# Get the changed attributes for the document.
#
# @example Get the changed attributes.
# model.changed
#
# @return [ Array<String> ] The changed attributes.
#
# @since 2.4.0
def changed
changed_attributes.keys.select { |attr| attribute_change(attr) }
end
# Has the document changed?
#
# @example Has the document changed?
# model.changed?
#
# @return [ true, false ] If the document is changed.
#
# @since 2.4.0
def changed?
changes.values.any? { |val| val } || children_changed?
end
# Have any children (embedded documents) of this document changed?
#
# @example Have any children changed?
# model.children_changed?
#
# @return [ true, false ] If any children have changed.
#
# @since 2.4.1
def children_changed?
_children.any?(&:changed?)
end
# Get the attribute changes.
#
# @example Get the attribute changes.
# model.changed_attributes
#
# @return [ Hash<String, Object> ] The attribute changes.
#
# @since 2.4.0
def changed_attributes
@changed_attributes ||= {}
end
# Get all the changes for the document.
#
# @example Get all the changes.
# model.changes
#
# @return [ Hash<String, Array<Object, Object> ] The changes.
#
# @since 2.4.0
def changes
_changes = {}
changed.each do |attr|
change = attribute_change(attr)
_changes[attr] = change if change
end
_changes
end
# Call this method after save, so the changes can be properly switched.
#
# This will unset the memoized children array, set new record to
# false, set the document as validated, and move the dirty changes.
#
# @example Move the changes to previous.
# person.move_changes
#
# @since 2.1.0
def move_changes
@previous_changes = changes
Atomic::UPDATES.each do |update|
send(update).clear
end
changed_attributes.clear
end
# Things that need to execute after a document has been persisted.
#
# @example Handle post persistence.
# document.post_persist
#
# @since 3.0.0
def post_persist
reset_persisted_children
move_changes
end
# Get the previous changes on the document.
#
# @example Get the previous changes.
# model.previous_changes
#
# @return [ Hash<String, Array<Object, Object> ] The previous changes.
#
# @since 2.4.0
def previous_changes
@previous_changes ||= {}
end
# Remove a change from the dirty attributes hash. Used by the single field
# atomic updators.
#
# @example Remove a flagged change.
# model.remove_change(:field)
#
# @param [ Symbol, String ] name The name of the field.
#
# @since 2.1.0
def remove_change(name)
changed_attributes.delete(name.to_s)
end
# Gets all the new values for each of the changed fields, to be passed to
# a MongoDB $set modifier.
#
# @example Get the setters for the atomic updates.
# person = Person.new(:title => "Sir")
# person.title = "Madam"
# person.setters # returns { "title" => "Madam" }
#
# @return [ Hash ] A +Hash+ of atomic setters.
#
# @since 2.0.0
def setters
mods = {}
changes.each_pair do |name, changes|
if changes
old, new = changes
field = fields[name]
key = atomic_attribute_name(name)
if field && field.resizable?
field.add_atomic_changes(self, name, key, mods, new, old)
else
mods[key] = new unless atomic_unsets.include?(key)
end
end
end
mods
end
private
# Get the old and new value for the provided attribute.
#
# @example Get the attribute change.
# model.attribute_change("name")
#
# @param [ String ] attr The name of the attribute.
#
# @return [ Array<Object> ] The old and new values.
#
# @since 2.1.0
def attribute_change(attr)
attr = database_field_name(attr)
[changed_attributes[attr], attributes[attr]] if attribute_changed?(attr)
end
# Determine if a specific attribute has changed.
#
# @example Has the attribute changed?
# model.attribute_changed?("name")
#
# @param [ String ] attr The name of the attribute.
#
# @return [ true, false ] Whether the attribute has changed.
#
# @since 2.1.6
def attribute_changed?(attr)
attr = database_field_name(attr)
return false unless changed_attributes.key?(attr)
changed_attributes[attr] != attributes[attr]
end
# Get whether or not the field has a different value from the default.
#
# @example Is the field different from the default?
# model.attribute_changed_from_default?
#
# @param [ String ] attr The name of the attribute.
#
# @return [ true, false ] If the attribute differs.
#
# @since 3.0.0
def attribute_changed_from_default?(attr)
field = fields[attr]
return false unless field
attributes[attr] != field.eval_default(self)
end
# Get the previous value for the attribute.
#
# @example Get the previous value.
# model.attribute_was("name")
#
# @param [ String ] attr The attribute name.
#
# @since 2.4.0
def attribute_was(attr)
attr = database_field_name(attr)
attribute_changed?(attr) ? changed_attributes[attr] : attributes[attr]
end
# Flag an attribute as going to change.
#
# @example Flag the attribute.
# model.attribute_will_change!("name")
#
# @param [ String ] attr The name of the attribute.
#
# @return [ Object ] The old value.
#
# @since 2.3.0
def attribute_will_change!(attr)
unless changed_attributes.key?(attr)
changed_attributes[attr] = read_attribute(attr).__deep_copy__
end
end
# Set the attribute back to its old value.
#
# @example Reset the attribute.
# model.reset_attribute!("name")
#
# @param [ String ] attr The name of the attribute.
#
# @return [ Object ] The old value.
#
# @since 2.4.0
def reset_attribute!(attr)
attr = database_field_name(attr)
attributes[attr] = changed_attributes.delete(attr) if attribute_changed?(attr)
end
def reset_attribute_to_default!(attr)
attr = database_field_name(attr)
if field = fields[attr]
__send__("#{attr}=", field.eval_default(self))
else
__send__("#{attr}=", nil)
end
end
module ClassMethods
private
# Generate all the dirty methods needed for the attribute.
#
# @example Generate the dirty methods.
# Model.create_dirty_methods("name", "name")
#
# @param [ String ] name The name of the field.
# @param [ String ] name The name of the accessor.
#
# @return [ Module ] The fields module.
#
# @since 2.4.0
def create_dirty_methods(name, meth)
create_dirty_change_accessor(name, meth)
create_dirty_change_check(name, meth)
create_dirty_change_flag(name, meth)
create_dirty_default_change_check(name, meth)
create_dirty_previous_value_accessor(name, meth)
create_dirty_reset(name, meth)
create_dirty_reset_to_default(name, meth)
end
# Creates the dirty change accessor.
#
# @example Create the accessor.
# Model.create_dirty_change_accessor("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_change_accessor(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_change") do
attribute_change(name)
end
end
end
# Creates the dirty change check.
#
# @example Create the check.
# Model.create_dirty_change_check("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_change_check(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_changed?") do
attribute_changed?(name)
end
end
end
# Creates the dirty default change check.
#
# @example Create the check.
# Model.create_dirty_default_change_check("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_default_change_check(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_changed_from_default?") do
attribute_changed_from_default?(name)
end
end
end
# Creates the dirty change previous value accessor.
#
# @example Create the accessor.
# Model.create_dirty_previous_value_accessor("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_previous_value_accessor(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_was") do
attribute_was(name)
end
end
end
# Creates the dirty change flag.
#
# @example Create the flag.
# Model.create_dirty_change_flag("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_change_flag(name, meth)
generated_methods.module_eval do
re_define_method("#{meth}_will_change!") do
attribute_will_change!(name)
end
end
end
# Creates the dirty change reset.
#
# @example Create the reset.
# Model.create_dirty_reset("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_reset(name, meth)
generated_methods.module_eval do
re_define_method("reset_#{meth}!") do
reset_attribute!(name)
end
end
end
# Creates the dirty change reset to default.
#
# @example Create the reset.
# Model.create_dirty_reset_to_default("name", "alias")
#
# @param [ String ] name The attribute name.
# @param [ String ] meth The name of the accessor.
#
# @since 3.0.0
def create_dirty_reset_to_default(name, meth)
generated_methods.module_eval do
re_define_method("reset_#{meth}_to_default!") do
reset_attribute_to_default!(name)
end
end
end
end
end
end