Class: PaperTrail::RecordTrail

Inherits:
Object
  • Object
show all
Defined in:
lib/paper_trail/record_trail.rb

Overview

Represents the “paper trail” for a single record.

Instance Method Summary collapse

Constructor Details

#initialize(record) ⇒ RecordTrail

Returns a new instance of RecordTrail.



10
11
12
# File 'lib/paper_trail/record_trail.rb', line 10

def initialize(record)
  @record = record
end

Instance Method Details

#clear_rolled_back_versionsObject

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.



18
19
20
# File 'lib/paper_trail/record_trail.rb', line 18

def clear_rolled_back_versions
  versions.reset
end

#clear_version_instanceObject

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



24
25
26
# File 'lib/paper_trail/record_trail.rb', line 24

def clear_version_instance
  @record.send("#{@record.class.version_association_name}=", nil)
end

#live?Boolean

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

Returns:

  • (Boolean)


30
31
32
# File 'lib/paper_trail/record_trail.rb', line 30

def live?
  source_version.nil?
end

#next_versionObject

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?


37
38
39
40
41
42
# File 'lib/paper_trail/record_trail.rb', line 37

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

#originatorObject

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



47
48
49
# File 'lib/paper_trail/record_trail.rb', line 47

def originator
  (source_version || versions.last).try(:whodunnit)
end

#previous_versionObject

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



54
55
56
# File 'lib/paper_trail/record_trail.rb', line 54

def previous_version
  (source_version ? source_version.previous : versions.last).try(:reify)
end

#record_createObject



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/paper_trail/record_trail.rb', line 58

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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

paper_trail-association_tracking

Returns:

    • The created version object, so that plugins can use it, e.g.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/paper_trail/record_trail.rb', line 75

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:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

paper_trail-association_tracking

Parameters:

  • force (boolean)

    Insert a ‘Version` even if `@record` has not `changed_notably?`.

  • in_after_callback (boolean)

    True when called from an ‘after_update` or `after_touch` callback.

  • is_touch (boolean)

    True when called from an ‘after_touch` callback.

Returns:

    • The created version object, so that plugins can use it, e.g.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/paper_trail/record_trail.rb', line 101

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

#reset_timestamp_attrs_for_update_if_neededObject

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



124
125
126
127
128
129
# File 'lib/paper_trail/record_trail.rb', line 124

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?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

AR callback.

Returns:

  • (Boolean)


133
134
135
136
137
# File 'lib/paper_trail/record_trail.rb', line 133

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(in_after_callback: false, **options) ⇒ Object

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

‘in_after_callback`: Indicates if this method is being called within an

`after` callback. Defaults to `false`.

‘options`: Optional arguments 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`.



152
153
154
155
156
157
# File 'lib/paper_trail/record_trail.rb', line 152

def save_with_version(in_after_callback: false, **options)
  ::PaperTrail.request(enabled: false) do
    @record.save(**options)
  end
  record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
end

#source_versionObject



139
140
141
# File 'lib/paper_trail/record_trail.rb', line 139

def source_version
  version
end

#update_column(name, value) ⇒ Object

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



162
163
164
# File 'lib/paper_trail/record_trail.rb', line 162

def update_column(name, value)
  update_columns(name => value)
end

#update_columns(attributes) ⇒ Object

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



169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/paper_trail/record_trail.rb', line 169

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 = {}) ⇒ Object

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



183
184
185
186
187
188
189
# File 'lib/paper_trail/record_trail.rb', line 183

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) ⇒ Object

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



192
193
194
195
# File 'lib/paper_trail/record_trail.rb', line 192

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