# encoding: utf-8 module Mongoid #:nodoc: module Dirty #:nodoc: extend ActiveSupport::Concern module InstanceMethods #:nodoc: # Gets the changes for a specific field. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.attribute_change("title") # [ "Sir", "Madam" ] # # Returns: # # An +Array+ containing the old and new values. def attribute_change(name) modifications[name] end # Determines if a specific field has chaged. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.attribute_changed?("title") # true # # Returns: # # +true+ if changed, +false+ if not. def attribute_changed?(name) modifications.include?(name) end # Gets the old value for a specific field. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.attribute_was("title") # "Sir" # # Returns: # # The old field value. def attribute_was(name) change = modifications[name] change ? change[0] : nil end # Gets the names of all the fields that have changed in the document. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.changed # returns [ "title" ] # # Returns: # # An +Array+ of changed field names. def changed modifications.keys end # Alerts to whether the document has been modified or not. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.changed? # returns true # # Returns: # # +true+ if changed, +false+ if not. def changed? !modifications.empty? end # Gets all the modifications that have happened to the object as a +Hash+ # with the keys being the names of the fields, and the values being an # +Array+ with the old value and new value. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.changes # returns { "title" => [ "Sir", "Madam" ] } # # Returns: # # A +Hash+ of changes. def changes modifications end # Call this method after save, so the changes can be properly switched. # # Example: # # <tt>person.move_changes</tt> def move_changes @previous_modifications = modifications.dup @modifications = {} end # Gets all the new values for each of the changed fields, to be passed to # a MongoDB $set modifier. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.setters # returns { "title" => "Madam" } # # Returns: # # A +Hash+ of new values. def setters modifications.inject({}) do |sets, (field, changes)| key = embedded? ? "#{_position}.#{field}" : field sets[key] = changes[1]; sets end end # Gets all the modifications that have happened to the object before the # object was saved. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.save! # person.previous_changes # returns { "title" => [ "Sir", "Madam" ] } # # Returns: # # A +Hash+ of changes before save. def previous_changes @previous_modifications end # Resets a changed field back to its old value. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.reset_attribute!("title") # person.title # "Sir" # # Returns: # # The old field value. def reset_attribute!(name) value = attribute_was(name) if value @attributes[name] = value modifications.delete(name) end end # Sets up the modifications hash. This occurs just after the document is # instantiated. # # Example: # # <tt>document.setup_notifications</tt> def setup_modifications @accessed ||= {} @modifications ||= {} @previous_modifications ||= {} end # Reset all modifications for the document. This will wipe all the marked # changes, but not reset the values. # # Example: # # <tt>document.reset_modifications</tt> def reset_modifications @accessed = {} @modifications = {} end protected # Audit the original value for a field that can be modified in place. # # Example: # # <tt>person.accessed("aliases", [ "007" ])</tt> def accessed(name, value) @accessed ||= {} @accessed[name] = value.dup if (value.is_a?(Array) || value.is_a?(Hash)) && !@accessed.has_key?(name) value end # Get all normal modifications plus in place potential changes. # # Example: # # <tt>person.modifications</tt> # # Returns: # # All changes to the document. def modifications @accessed.each_pair do |field, value| current = @attributes[field] @modifications[field] = [ value, current ] if current != value end @accessed.clear @modifications end # Audit the change of a field's value. # # Example: # # <tt>person.modify("name", "Jack", "John")</tt> def modify(name, old_value, new_value) @attributes[name] = new_value if @modifications && (old_value != new_value) original = @modifications[name].first if @modifications[name] @modifications[name] = [ (original || old_value), new_value ] end end end module ClassMethods #:nodoc: # Add the dynamic dirty methods. These are custom methods defined on a # field by field basis that wrap the dirty attribute methods. # # Example: # # person = Person.new(:title => "Sir") # person.title = "Madam" # person.title_change # [ "Sir", "Madam" ] # person.title_changed? # true # person.title_was # "Sir" # person.reset_title! def add_dirty_methods(name) define_method("#{name}_change") { attribute_change(name) } define_method("#{name}_changed?") { attribute_changed?(name) } define_method("#{name}_was") { attribute_was(name) } define_method("reset_#{name}!") { reset_attribute!(name) } end end end end