Class: PaperTrail::RecordTrail

Inherits:
Object
  • Object
show all
Defined in:
lib/mongo_trails/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/mongo_trails/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/mongo_trails/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.



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

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.



87
88
89
# File 'lib/mongo_trails/record_trail.rb', line 87

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.



117
118
119
# File 'lib/mongo_trails/record_trail.rb', line 117

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.



148
149
150
# File 'lib/mongo_trails/record_trail.rb', line 148

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.



175
176
177
# File 'lib/mongo_trails/record_trail.rb', line 175

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)


36
37
38
39
40
# 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?Boolean

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

Returns:

  • (Boolean)


44
45
46
# File 'lib/mongo_trails/record_trail.rb', line 44

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?


51
52
53
54
55
56
# 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

#originatorObject

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



61
62
63
# File 'lib/mongo_trails/record_trail.rb', line 61

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

#previous_versionObject

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



68
69
70
# File 'lib/mongo_trails/record_trail.rb', line 68

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

#record_createObject



72
73
74
75
76
77
78
79
80
81
82
# 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) ⇒ 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.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 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:) ⇒ 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.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# 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) ⇒ 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.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# 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_neededObject

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



181
182
183
184
185
186
# 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?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)


190
191
192
193
194
# 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) ⇒ 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`.



207
208
209
210
211
212
# 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_versionObject



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

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.



217
218
219
# File 'lib/mongo_trails/record_trail.rb', line 217

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.



224
225
226
227
228
229
230
231
232
233
234
235
# 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 = {}) ⇒ Object

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



238
239
240
241
242
243
244
# 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) ⇒ Object

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



247
248
249
250
# 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_resetObject



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

def versions_reset
  @record.class.paper_trail.version_class.reset
end