class PaperTrail::RecordTrail

Represents the “paper trail” for a single record.

Constants

RAILS_GTE_5_1

Public Class Methods

new(record) click to toggle source
# File lib/mongo_trails/record_trail.rb, line 12
def initialize(record)
  @record = record
end

Public Instance Methods

clear_rolled_back_versions() click to toggle source

Invoked after rollbacks to ensure versions records are not created for changes that never actually took place. Optimization: Use lazy `reset` instead of eager `reload` because, in many use cases, the association will not be used.

# File lib/mongo_trails/record_trail.rb, line 20
def clear_rolled_back_versions
  versions_reset
end
clear_version_instance() click to toggle source

Invoked via`after_update` callback for when a previous version is reified and then saved.

# File lib/mongo_trails/record_trail.rb, line 30
def clear_version_instance
  @record.send("#{@record.class.version_association_name}=", nil)
end
data_for_create() click to toggle source

PT-AT extends this method to add its transaction id.

@api private

# File lib/mongo_trails/record_trail.rb, line 87
def data_for_create
  {}
end
data_for_destroy() click to toggle source

PT-AT extends this method to add its transaction id.

@api private

# File lib/mongo_trails/record_trail.rb, line 117
def data_for_destroy
  {}
end
data_for_update() click to toggle source

PT-AT extends this method to add its transaction id.

@api private

# File lib/mongo_trails/record_trail.rb, line 148
def data_for_update
  {}
end
data_for_update_columns() click to toggle source

PT-AT extends this method to add its transaction id.

@api private

# File lib/mongo_trails/record_trail.rb, line 175
def data_for_update_columns
  {}
end
enabled?() click to toggle source

Is PT enabled for this particular record? @api private

# File lib/mongo_trails/record_trail.rb, line 36
def enabled?
  PaperTrail.enabled? &&
    PaperTrail.request.enabled? &&
    PaperTrail.request.enabled_for_model?(@record.class)
end
live?() click to toggle source

Returns true if this instance is the current, live one; returns false if this instance came from a previous version.

# File lib/mongo_trails/record_trail.rb, line 44
def live?
  source_version.nil?
end
next_version() click to toggle source

Returns the object (not a Version) as it became next. NOTE: if self (the item) was not reified from a version, i.e. it is the

"live" item, we return nil.  Perhaps we should return self instead?
# File lib/mongo_trails/record_trail.rb, line 51
def next_version
  subsequent_version = source_version.next
  subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
rescue StandardError # TODO: Rescue something more specific
  nil
end
originator() click to toggle source

Returns who put `@record` into its current state.

@api public

# File lib/mongo_trails/record_trail.rb, line 61
def originator
  (source_version || versions.last).try(:whodunnit)
end
previous_version() click to toggle source

Returns the object (not a Version) as it was most recently.

@api public

# File lib/mongo_trails/record_trail.rb, line 68
def previous_version
  (source_version ? source_version.previous : versions.last).try(:reify)
end
record_create() click to toggle source
# File lib/mongo_trails/record_trail.rb, line 72
def record_create
  return unless enabled?

  build_version_on_create(in_after_callback: true).tap do |version|
    version.save!
    # Because the version object was created using version_class.new instead
    # of versions_assoc.build?, the association cache is unaware. So, we
    # invalidate the `versions` association cache with `reset`.
    versions_reset
  end
end
record_destroy(recording_order) click to toggle source

`recording_order` is “after” or “before”. See ModelConfig#on_destroy.

@api private @return - The created version object, so that plugins can use it, e.g. paper_trail-association_tracking

# File lib/mongo_trails/record_trail.rb, line 96
def record_destroy(recording_order)
  return unless enabled? && !@record.new_record?
  in_after_callback = recording_order == "after"
  event = Events::Destroy.new(@record, in_after_callback)

  # Merge data from `Event` with data from PT-AT. We no longer use
  # `data_for_destroy` but PT-AT still does.
  data = event.data.merge(data_for_destroy)

  version = @record.class.paper_trail.version_class.create(data)
  if version.errors.any?
    log_version_errors(version, :destroy)
  else
    assign_and_reset_version_association(version)
    version
  end
end
record_update(force:, in_after_callback:, is_touch:) click to toggle source

@api private @return - The created version object, so that plugins can use it, e.g. paper_trail-association_tracking

# File lib/mongo_trails/record_trail.rb, line 124
def record_update(force:, in_after_callback:, is_touch:)
  return unless enabled?

  version = build_version_on_update(
    force: force,
    in_after_callback: in_after_callback,
    is_touch: is_touch
  )
  return unless version

  if version.save
    # Because the version object was created using version_class.new instead
    # of versions_assoc.build?, the association cache is unaware. So, we
    # invalidate the `versions` association cache with `reset`.
    versions_reset
    version
  else
    log_version_errors(version, :update)
  end
end
record_update_columns(changes) click to toggle source

@api private @return - The created version object, so that plugins can use it, e.g. paper_trail-association_tracking

# File lib/mongo_trails/record_trail.rb, line 155
def record_update_columns(changes)
  return unless enabled?
  event = Events::Update.new(@record, false, false, changes)

  # Merge data from `Event` with data from PT-AT. We no longer use
  # `data_for_update_columns` but PT-AT still does.
  data = event.data.merge(data_for_update_columns)

  versions_assoc = @record.send(@record.class.versions_association_name)
  version = versions_assoc.create(data)
  if version.errors.any?
    log_version_errors(version, :update)
  else
    version
  end
end
reset_timestamp_attrs_for_update_if_needed() click to toggle source

Invoked via callback when a user attempts to persist a reified `Version`.

# File lib/mongo_trails/record_trail.rb, line 181
def reset_timestamp_attrs_for_update_if_needed
  return if live?
  @record.send(:timestamp_attributes_for_update_in_model).each do |column|
    @record.send("restore_#{column}!")
  end
end
save_version?() click to toggle source

AR callback. @api private

# File lib/mongo_trails/record_trail.rb, line 190
def save_version?
  if_condition = @record.paper_trail_options[:if]
  unless_condition = @record.paper_trail_options[:unless]
  (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
end
save_with_version(*args) click to toggle source

Save, and create a version record regardless of options such as `:on`, `:if`, or `:unless`.

Arguments are passed to `save`.

This is an “update” event. That is, we record the same data we would in the case of a normal AR `update`.

# File lib/mongo_trails/record_trail.rb, line 207
def save_with_version(*args)
  ::PaperTrail.request(enabled: false) do
    @record.save(*args)
  end
  record_update(force: true, in_after_callback: false, is_touch: false)
end
source_version() click to toggle source
# File lib/mongo_trails/record_trail.rb, line 196
def source_version
  version
end
update_column(name, value) click to toggle source

Like the `update_column` method from `ActiveRecord::Persistence`, but also creates a version to record those changes. @api public

# File lib/mongo_trails/record_trail.rb, line 217
def update_column(name, value)
  update_columns(name => value)
end
update_columns(attributes) click to toggle source

Like the `update_columns` method from `ActiveRecord::Persistence`, but also creates a version to record those changes. @api public

# File lib/mongo_trails/record_trail.rb, line 224
def update_columns(attributes)
  # `@record.update_columns` skips dirty-tracking, so we can't just use
  # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
  # We need to build our own hash with the changes that will be made
  # directly to the database.
  changes = {}
  attributes.each do |k, v|
    changes[k] = [@record[k], v]
  end
  @record.update_columns(attributes)
  record_update_columns(changes)
end
version_at(timestamp, reify_options = {}) click to toggle source

Returns the object (not a Version) as it was at the given timestamp.

# File lib/mongo_trails/record_trail.rb, line 238
def version_at(timestamp, reify_options = {})
  # Because a version stores how its object looked *before* the change,
  # we need to look for the first version created *after* the timestamp.
  v = versions.subsequent(timestamp, true).first
  return v.reify(reify_options) if v
  @record unless @record.destroyed?
end
versions_between(start_time, end_time) click to toggle source

Returns the objects (not Versions) as they were between the given times.

# File lib/mongo_trails/record_trail.rb, line 247
def versions_between(start_time, end_time)
  versions = send(@record.class.versions_association_name).between(start_time, end_time)
  versions.collect { |version| version_at(version.created_at) }
end
versions_reset() click to toggle source
# File lib/mongo_trails/record_trail.rb, line 24
def versions_reset
  @record.class.paper_trail.version_class.reset
end

Private Instance Methods

assign_and_reset_version_association(version) click to toggle source

@api private

# File lib/mongo_trails/record_trail.rb, line 255
def assign_and_reset_version_association(version)
  @record.send("#{@record.class.version_association_name}=", version)
  @record.send(@record.class.versions_association_name).reset
end
build_version_on_create(in_after_callback:) click to toggle source

@api private

# File lib/mongo_trails/record_trail.rb, line 261
def build_version_on_create(in_after_callback:)
  event = Events::Create.new(@record, in_after_callback)

  # Merge data from `Event` with data from PT-AT. We no longer use
  # `data_for_create` but PT-AT still does.
  data = event.data.merge!(data_for_create)

  # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
  @record.class.paper_trail.version_class.new(data)
end
build_version_on_update(force:, in_after_callback:, is_touch:) click to toggle source

@api private

# File lib/mongo_trails/record_trail.rb, line 273
def build_version_on_update(force:, in_after_callback:, is_touch:)
  event = Events::Update.new(@record, in_after_callback, is_touch, nil)
  return unless force || event.changed_notably?

  # Merge data from `Event` with data from PT-AT. We no longer use
  # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
  # instead of `merge`.
  data = event.data.merge!(data_for_update)

  # Using `version_class.new` reduces memory usage compared to
  # `versions_assoc.build`. It's a trade-off though. We have to clear
  # the association cache (see `versions.reset`) and that could cause an
  # additional query in certain applications.
  @record.class.paper_trail.version_class.new(data)
end
log_version_errors(version, action) click to toggle source
# File lib/mongo_trails/record_trail.rb, line 289
def log_version_errors(version, action)
  version.logger&.warn(
    "Unable to create version for #{action} of #{@record.class.name}" \
      "##{@record.id}: " + version.errors.full_messages.join(", ")
  )
end
version() click to toggle source
# File lib/mongo_trails/record_trail.rb, line 296
def version
  @record.public_send(@record.class.version_association_name)
end
versions() click to toggle source
# File lib/mongo_trails/record_trail.rb, line 300
def versions
  @record.public_send(@record.class.versions_association_name)
end