module Mongoid::History::Tracker

Public Instance Methods

affected() click to toggle source

Similar to tracked_changes, but contains only a single value for each affected field:

- :create and :update return the modified values
- :destroy returns original values

Included for legacy compatibility.

@deprecated

@return [ HashWithIndifferentAccess ] a change set in the format:

{ field_1: value, field_2: value }
# File lib/mongoid/history/tracker.rb, line 148
def affected
  target = action.to_sym == :destroy ? :from : :to
  @affected ||= tracked_changes.inject(HashWithIndifferentAccess.new) do |h, (k, v)|
    h[k] = v[target]
    h
  end
end
redo!(modifier = nil) click to toggle source
# File lib/mongoid/history/tracker.rb, line 41
def redo!(modifier = nil)
  if action.to_sym == :destroy
    re_destroy
  elsif action.to_sym == :create
    re_create
  elsif Mongoid::Compatibility::Version.mongoid3?
    trackable.update_attributes!(redo_attr(modifier), without_protection: true)
  else
    trackable.update_attributes!(redo_attr(modifier))
  end
end
redo_attr(modifier) click to toggle source
# File lib/mongoid/history/tracker.rb, line 64
def redo_attr(modifier)
  redo_hash = affected.easy_unmerge(original)
  redo_hash.easy_merge!(modified)
  modifier_field = trackable.history_trackable_options[:modifier_field]
  redo_hash[modifier_field] = modifier if modifier_field
  localize_keys(redo_hash)
end
trackable() click to toggle source
# File lib/mongoid/history/tracker.rb, line 76
def trackable
  @trackable ||= trackable_parents_and_trackable.last
end
trackable_parent() click to toggle source
# File lib/mongoid/history/tracker.rb, line 84
def trackable_parent
  @trackable_parent ||= trackable_parents_and_trackable[-2]
end
trackable_parent_class() click to toggle source

Returns the class of the trackable, irrespective of whether the trackable object has been destroyed.

@return [ Class ] the class of the trackable

# File lib/mongoid/history/tracker.rb, line 160
def trackable_parent_class
  association_chain.first['name'].constantize
end
trackable_parents() click to toggle source
# File lib/mongoid/history/tracker.rb, line 80
def trackable_parents
  @trackable_parents ||= trackable_parents_and_trackable[0, -1]
end
trackable_root() click to toggle source
# File lib/mongoid/history/tracker.rb, line 72
def trackable_root
  @trackable_root ||= trackable_parents_and_trackable.first
end
tracked_changes() click to toggle source

Outputs a :from, :to hash for each affected field. Intentionally excludes fields which are not tracked, even if there are tracked values for such fields present in the database.

@return [ HashWithIndifferentAccess ] a change set in the format:

{ field_1: {to: new_val}, field_2: {from: old_val, to: new_val} }
# File lib/mongoid/history/tracker.rb, line 94
def tracked_changes
  @tracked_changes ||= (modified.keys | original.keys).inject(HashWithIndifferentAccess.new) do |h, k|
    h[k] = { from: original[k], to: modified[k] }.delete_if { |_, vv| vv.nil? }
    h
  end.delete_if { |k, v| v.blank? || !trackable_parent_class.tracked?(k) }
end
tracked_edits() click to toggle source

Outputs summary of edit actions performed: :add, :modify, :remove, or :array. Does deep comparison of arrays. Useful for creating human-readable representations of the history tracker. Considers changing a value to 'blank' to be a removal.

@return [ HashWithIndifferentAccess ] a change set in the format:

{ add: { field_1: new_val, ... },
  modify: { field_2: {from: old_val, to: new_val}, ... },
  remove: { field_3: old_val },
  array: { field_4: {add: ['foo', 'bar'], remove: ['baz']} } }
# File lib/mongoid/history/tracker.rb, line 110
def tracked_edits
  return @tracked_edits if @tracked_edits
  @tracked_edits = HashWithIndifferentAccess.new

  tracked_changes.each do |k, v|
    next if v[:from].blank? && v[:to].blank?

    if trackable_parent_class.tracked_embeds_many?(k)
      prepare_tracked_edits_for_embeds_many(k, v)
    elsif v[:from].blank?
      @tracked_edits[:add] ||= {}
      @tracked_edits[:add][k] = v[:to]
    elsif v[:to].blank?
      @tracked_edits[:remove] ||= {}
      @tracked_edits[:remove][k] = v[:from]
    elsif v[:from].is_a?(Array) && v[:to].is_a?(Array)
      @tracked_edits[:array] ||= {}
      old_values = v[:from] - v[:to]
      new_values = v[:to] - v[:from]
      @tracked_edits[:array][k] = { add: new_values, remove: old_values }.delete_if { |_, vv| vv.blank? }
    else
      @tracked_edits[:modify] ||= {}
      @tracked_edits[:modify][k] = v
    end
  end
  @tracked_edits
end
undo!(modifier = nil) click to toggle source
# File lib/mongoid/history/tracker.rb, line 29
def undo!(modifier = nil)
  if action.to_sym == :destroy
    re_create
  elsif action.to_sym == :create
    re_destroy
  elsif Mongoid::Compatibility::Version.mongoid3?
    trackable.update_attributes!(undo_attr(modifier), without_protection: true)
  else
    trackable.update_attributes!(undo_attr(modifier))
  end
end
undo_attr(modifier) click to toggle source
# File lib/mongoid/history/tracker.rb, line 53
def undo_attr(modifier)
  undo_hash = affected.easy_unmerge(modified)
  undo_hash.easy_merge!(original)
  modifier_field = trackable.history_trackable_options[:modifier_field]
  undo_hash[modifier_field] = modifier if modifier_field
  (modified.keys - undo_hash.keys).each do |k|
    undo_hash[k] = nil
  end
  localize_keys(undo_hash)
end

Private Instance Methods

create_on_parent() click to toggle source
# File lib/mongoid/history/tracker.rb, line 180
def create_on_parent
  name = association_chain.last['name']

  if trackable_parent.class.embeds_one?(name)
    trackable_parent._create_relation(name, localize_keys(original))
  elsif trackable_parent.class.embeds_many?(name)
    trackable_parent._get_relation(name).create!(localize_keys(original))
  else
    raise 'This should never happen. Please report bug!'
  end
end
create_standalone() click to toggle source
# File lib/mongoid/history/tracker.rb, line 174
def create_standalone
  restored = trackable_parent_class.new(localize_keys(original))
  restored.id = original['_id']
  restored.save!
end
localize_keys(hash) click to toggle source
# File lib/mongoid/history/tracker.rb, line 223
def localize_keys(hash)
  klass = association_chain.first['name'].constantize
  if klass.respond_to?(:localized_fields)
    klass.localized_fields.keys.each do |name|
      hash["#{name}_translations"] = hash.delete(name) if hash[name].present?
    end
  end
  hash
end
prepare_tracked_edits_for_embeds_many(key, value) click to toggle source
# File lib/mongoid/history/tracker.rb, line 233
def prepare_tracked_edits_for_embeds_many(key, value)
  @tracked_edits[:embeds_many] ||= {}
  value[:from] ||= []
  value[:to] ||= []
  modify_ids = value[:from].map { |vv| vv['_id'] }.compact & value[:to].map { |vv| vv['_id'] }.compact
  modify_values = modify_ids.map { |id| { from: value[:from].detect { |vv| vv['_id'] == id }, to: value[:to].detect { |vv| vv['_id'] == id } } }
  modify_values.delete_if { |vv| vv[:from] == vv[:to] }
  ignore_values = modify_values.map { |vv| [vv[:from], vv[:to]] }.flatten
  old_values = value[:from] - value[:to] - ignore_values
  new_values = value[:to] - value[:from] - ignore_values
  @tracked_edits[:embeds_many][key] = { add: new_values, remove: old_values, modify: modify_values }.delete_if { |_, vv| vv.blank? }
end
re_create() click to toggle source
# File lib/mongoid/history/tracker.rb, line 166
def re_create
  association_chain.length > 1 ? create_on_parent : create_standalone
end
re_destroy() click to toggle source
# File lib/mongoid/history/tracker.rb, line 170
def re_destroy
  trackable.destroy
end
trackable_parents_and_trackable() click to toggle source
# File lib/mongoid/history/tracker.rb, line 192
def trackable_parents_and_trackable
  @trackable_parents_and_trackable ||= traverse_association_chain
end
traverse_association_chain() click to toggle source
# File lib/mongoid/history/tracker.rb, line 196
def traverse_association_chain
  chain = association_chain.dup
  doc = nil
  documents = []
  loop do
    node = chain.shift
    name = node['name']
    doc = if doc.nil?
            # root association. First element of the association chain
            # unscoped is added to remove any default_scope defined in model
            klass = name.classify.constantize
            klass.unscoped.where(_id: node['id']).first
          elsif doc.class.embeds_one?(name)
            doc._get_relation(name)
          elsif doc.class.embeds_many?(name)
            doc._get_relation(name).unscoped.where(_id: node['id']).first
          else
            relation_klass = doc.class.relation_class_of(name) if doc
            relation_klass ||= 'nil'
            raise "Unexpected relation for field '#{name}': #{relation_klass}. This should never happen. Please report bug."
          end
    documents << doc
    break if chain.empty?
  end
  documents
end