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.

Constant Summary collapse

RAILS_GTE_5_1 =
::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")

Instance Method Summary collapse

Constructor Details

#initialize(record) ⇒ RecordTrail

Returns a new instance of RecordTrail.



12
13
14
# File 'lib/paper_trail/record_trail.rb', line 12

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.



20
21
22
# File 'lib/paper_trail/record_trail.rb', line 20

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.



26
27
28
# File 'lib/paper_trail/record_trail.rb', line 26

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

#data_for_createObject

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.

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



83
84
85
# File 'lib/paper_trail/record_trail.rb', line 83

def data_for_create
  {}
end

#data_for_destroyObject

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.

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



113
114
115
# File 'lib/paper_trail/record_trail.rb', line 113

def data_for_destroy
  {}
end

#data_for_updateObject

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.

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



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

def data_for_update
  {}
end

#data_for_update_columnsObject

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.

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



171
172
173
# File 'lib/paper_trail/record_trail.rb', line 171

def data_for_update_columns
  {}
end

#enabled?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.

Is PT enabled for this particular record?

Returns:

  • (Boolean)


32
33
34
35
36
# File 'lib/paper_trail/record_trail.rb', line 32

def enabled?
  PaperTrail.enabled? &&
    PaperTrail.request.enabled? &&
    PaperTrail.request.enabled_for_model?(@record.class)
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)


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

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?


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

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.



57
58
59
# File 'lib/paper_trail/record_trail.rb', line 57

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

#previous_versionObject

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



64
65
66
# File 'lib/paper_trail/record_trail.rb', line 64

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

#record_createObject



68
69
70
71
72
73
74
75
76
77
78
# File 'lib/paper_trail/record_trail.rb', line 68

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.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/paper_trail/record_trail.rb', line 92

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

Returns:

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



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/paper_trail/record_trail.rb', line 120

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

Returns:

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



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/paper_trail/record_trail.rb', line 151

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_neededObject

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



177
178
179
180
181
182
# File 'lib/paper_trail/record_trail.rb', line 177

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)


186
187
188
189
190
# File 'lib/paper_trail/record_trail.rb', line 186

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

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



203
204
205
206
207
208
# File 'lib/paper_trail/record_trail.rb', line 203

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_versionObject



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

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.



213
214
215
# File 'lib/paper_trail/record_trail.rb', line 213

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.



220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/paper_trail/record_trail.rb', line 220

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.



234
235
236
237
238
239
240
# File 'lib/paper_trail/record_trail.rb', line 234

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.



243
244
245
246
# File 'lib/paper_trail/record_trail.rb', line 243

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