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
69
70
# 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
  rescue StandardError => e
    handle_version_errors e, version, :create
  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.



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

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.new(data)
  begin
    version.save!
    assign_and_reset_version_association(version)
    version
  rescue StandardError => e
    handle_version_errors e, version, :destroy
  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.



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

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

  begin
    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
  rescue StandardError => e
    handle_version_errors e, version, :update
  end
end

#reset_timestamp_attrs_for_update_if_neededObject

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



128
129
130
131
132
133
# File 'lib/paper_trail/record_trail.rb', line 128

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)


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

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`.



156
157
158
159
160
161
# File 'lib/paper_trail/record_trail.rb', line 156

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



143
144
145
# File 'lib/paper_trail/record_trail.rb', line 143

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.



166
167
168
# File 'lib/paper_trail/record_trail.rb', line 166

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.



173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/paper_trail/record_trail.rb', line 173

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.



187
188
189
190
191
192
193
# File 'lib/paper_trail/record_trail.rb', line 187

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.



196
197
198
199
# File 'lib/paper_trail/record_trail.rb', line 196

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