paper_trail 5.1.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ require "active_support/core_ext"
2
+
3
+ module PaperTrail
4
+ # Configures an ActiveRecord model, mostly at application boot time, but also
5
+ # sometimes mid-request, with methods like enable/disable.
6
+ class ModelConfig
7
+ E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
8
+ paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
9
+ belongs_to_required_by_default and has no effect. Please use :before
10
+ or disable belongs_to_required_by_default.
11
+ STR
12
+
13
+ def initialize(model_class)
14
+ @model_class = model_class
15
+ end
16
+
17
+ # Switches PaperTrail off for this class.
18
+ def disable
19
+ ::PaperTrail.enabled_for_model(@model_class, false)
20
+ end
21
+
22
+ # Switches PaperTrail on for this class.
23
+ def enable
24
+ ::PaperTrail.enabled_for_model(@model_class, true)
25
+ end
26
+
27
+ def enabled?
28
+ return false unless @model_class.include?(::PaperTrail::Model::InstanceMethods)
29
+ ::PaperTrail.enabled_for_model?(@model_class)
30
+ end
31
+
32
+ # Adds a callback that records a version after a "create" event.
33
+ def on_create
34
+ @model_class.after_create :record_create, if: ->(m) { m.paper_trail.save_version? }
35
+ return if @model_class.paper_trail_options[:on].include?(:create)
36
+ @model_class.paper_trail_options[:on] << :create
37
+ end
38
+
39
+ # Adds a callback that records a version before or after a "destroy" event.
40
+ def on_destroy(recording_order = "before")
41
+ unless %w(after before).include?(recording_order.to_s)
42
+ raise ArgumentError, 'recording order can only be "after" or "before"'
43
+ end
44
+
45
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
46
+ ::ActiveSupport::Deprecation.warn(E_CANNOT_RECORD_AFTER_DESTROY)
47
+ end
48
+
49
+ @model_class.send "#{recording_order}_destroy", :record_destroy,
50
+ if: ->(m) { m.paper_trail.save_version? }
51
+
52
+ return if @model_class.paper_trail_options[:on].include?(:destroy)
53
+ @model_class.paper_trail_options[:on] << :destroy
54
+ end
55
+
56
+ # Adds a callback that records a version after an "update" event.
57
+ def on_update
58
+ @model_class.before_save :reset_timestamp_attrs_for_update_if_needed!, on: :update
59
+ @model_class.after_update :record_update, if: ->(m) { m.paper_trail.save_version? }
60
+ @model_class.after_update :clear_version_instance!
61
+ return if @model_class.paper_trail_options[:on].include?(:update)
62
+ @model_class.paper_trail_options[:on] << :update
63
+ end
64
+
65
+ # Set up `@model_class` for PaperTrail. Installs callbacks, associations,
66
+ # "class attributes", instance methods, and more.
67
+ # @api private
68
+ def setup(options = {})
69
+ options[:on] ||= [:create, :update, :destroy]
70
+ options[:on] = Array(options[:on]) # Support single symbol
71
+ @model_class.send :include, ::PaperTrail::Model::InstanceMethods
72
+ if ::ActiveRecord::VERSION::STRING < "4.2"
73
+ @model_class.send :extend, AttributeSerializers::LegacyActiveRecordShim
74
+ end
75
+ setup_options(options)
76
+ setup_associations(options)
77
+ setup_transaction_callbacks
78
+ setup_callbacks_from_options options[:on]
79
+ setup_callbacks_for_habtm options[:join_tables]
80
+ end
81
+
82
+ def version_class
83
+ @_version_class ||= @model_class.version_class_name.constantize
84
+ end
85
+
86
+ private
87
+
88
+ def active_record_gem_version
89
+ Gem::Version.new(ActiveRecord::VERSION::STRING)
90
+ end
91
+
92
+ def cannot_record_after_destroy?
93
+ Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
94
+ ::ActiveRecord::Base.belongs_to_required_by_default
95
+ end
96
+
97
+ def setup_associations(options)
98
+ @model_class.class_attribute :version_association_name
99
+ @model_class.version_association_name = options[:version] || :version
100
+
101
+ # The version this instance was reified from.
102
+ @model_class.send :attr_accessor, @model_class.version_association_name
103
+
104
+ @model_class.class_attribute :version_class_name
105
+ @model_class.version_class_name = options[:class_name] || "PaperTrail::Version"
106
+
107
+ @model_class.class_attribute :versions_association_name
108
+ @model_class.versions_association_name = options[:versions] || :versions
109
+
110
+ @model_class.send :attr_accessor, :paper_trail_event
111
+
112
+ # In rails 4, the `has_many` syntax for specifying order uses a lambda.
113
+ if ::ActiveRecord::VERSION::MAJOR >= 4
114
+ @model_class.has_many(
115
+ @model_class.versions_association_name,
116
+ -> { order(model.timestamp_sort_order) },
117
+ class_name: @model_class.version_class_name,
118
+ as: :item
119
+ )
120
+ else
121
+ @model_class.has_many(
122
+ @model_class.versions_association_name,
123
+ class_name: @model_class.version_class_name,
124
+ as: :item,
125
+ order: @model_class.paper_trail_version_class.timestamp_sort_order
126
+ )
127
+ end
128
+ end
129
+
130
+ # Adds callbacks to record changes to habtm associations such that on save
131
+ # the previous version of the association (if changed) can be interpreted.
132
+ def setup_callbacks_for_habtm(join_tables)
133
+ @model_class.send :attr_accessor, :paper_trail_habtm
134
+ @model_class.class_attribute :paper_trail_save_join_tables
135
+ @model_class.paper_trail_save_join_tables = Array.wrap(join_tables)
136
+ @model_class.reflect_on_all_associations(:has_and_belongs_to_many).
137
+ reject { |a| @model_class.paper_trail_options[:skip].include?(a.name.to_s) }.
138
+ each { |a|
139
+ added_callback = lambda do |*args|
140
+ update_habtm_state(a.name, :before_add, args[-2], args.last)
141
+ end
142
+ removed_callback = lambda do |*args|
143
+ update_habtm_state(a.name, :before_remove, args[-2], args.last)
144
+ end
145
+ @model_class.send(:"before_add_for_#{a.name}").send(:<<, added_callback)
146
+ @model_class.send(:"before_remove_for_#{a.name}").send(:<<, removed_callback)
147
+ }
148
+ end
149
+
150
+ def setup_callbacks_from_options(options_on = [])
151
+ options_on.each do |event|
152
+ public_send("on_#{event}")
153
+ end
154
+ end
155
+
156
+ def setup_options(options)
157
+ @model_class.class_attribute :paper_trail_options
158
+ @model_class.paper_trail_options = options.dup
159
+
160
+ [:ignore, :skip, :only].each do |k|
161
+ @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
162
+ flatten.
163
+ compact.
164
+ map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
165
+ end
166
+
167
+ @model_class.paper_trail_options[:meta] ||= {}
168
+ if @model_class.paper_trail_options[:save_changes].nil?
169
+ @model_class.paper_trail_options[:save_changes] = true
170
+ end
171
+ end
172
+
173
+ # Reset the transaction id when the transaction is closed.
174
+ def setup_transaction_callbacks
175
+ @model_class.after_commit { PaperTrail.clear_transaction_id }
176
+ @model_class.after_rollback { PaperTrail.clear_transaction_id }
177
+ @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
178
+ end
179
+
180
+ def update_habtm_state(name, callback, model, assoc)
181
+ model.paper_trail_habtm ||= {}
182
+ model.paper_trail_habtm.reverse_merge!(name => { removed: [], added: [] })
183
+ case callback
184
+ when :before_add
185
+ model.paper_trail_habtm[name][:added] |= [assoc.id]
186
+ model.paper_trail_habtm[name][:removed] -= [assoc.id]
187
+ when :before_remove
188
+ model.paper_trail_habtm[name][:removed] |= [assoc.id]
189
+ model.paper_trail_habtm[name][:added] -= [assoc.id]
190
+ else
191
+ raise "Invalid callback: #{callback}"
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,450 @@
1
+ module PaperTrail
2
+ # Represents the "paper trail" for a single record.
3
+ class RecordTrail
4
+ def initialize(record)
5
+ @record = record
6
+ end
7
+
8
+ # Utility method for reifying. Anything executed inside the block will
9
+ # appear like a new record.
10
+ def appear_as_new_record
11
+ @record.instance_eval {
12
+ alias :old_new_record? :new_record?
13
+ alias :new_record? :present?
14
+ }
15
+ yield
16
+ @record.instance_eval { alias :new_record? :old_new_record? }
17
+ end
18
+
19
+ def attributes_before_change
20
+ changed = @record.changed_attributes.select { |k, _v|
21
+ @record.class.column_names.include?(k)
22
+ }
23
+ @record.attributes.merge(changed)
24
+ end
25
+
26
+ def changed_and_not_ignored
27
+ ignore = @record.paper_trail_options[:ignore].dup
28
+ # Remove Hash arguments and then evaluate whether the attributes (the
29
+ # keys of the hash) should also get pushed into the collection.
30
+ ignore.delete_if do |obj|
31
+ obj.is_a?(Hash) &&
32
+ obj.each { |attr, condition|
33
+ ignore << attr if condition.respond_to?(:call) && condition.call(@record)
34
+ }
35
+ end
36
+ skip = @record.paper_trail_options[:skip]
37
+ @record.changed - ignore - skip
38
+ end
39
+
40
+ # Invoked after rollbacks to ensure versions records are not created for
41
+ # changes that never actually took place. Optimization: Use lazy `reset`
42
+ # instead of eager `reload` because, in many use cases, the association will
43
+ # not be used.
44
+ def clear_rolled_back_versions
45
+ versions.reset
46
+ end
47
+
48
+ # Invoked via`after_update` callback for when a previous version is
49
+ # reified and then saved.
50
+ def clear_version_instance
51
+ @record.send("#{@record.class.version_association_name}=", nil)
52
+ end
53
+
54
+ # Determines whether it is appropriate to generate a new version
55
+ # instance. A timestamp-only update (e.g. only `updated_at` changed) is
56
+ # considered notable unless an ignored attribute was also changed.
57
+ def changed_notably?
58
+ if ignored_attr_has_changed?
59
+ timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
60
+ (notably_changed - timestamps).any?
61
+ else
62
+ notably_changed.any?
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ def changes
68
+ notable_changes = @record.changes.delete_if { |k, _v|
69
+ !notably_changed.include?(k)
70
+ }
71
+ AttributeSerializers::ObjectChangesAttribute.
72
+ new(@record.class).
73
+ serialize(notable_changes)
74
+ notable_changes.to_hash
75
+ end
76
+
77
+ def enabled?
78
+ PaperTrail.enabled? && PaperTrail.enabled_for_controller? && enabled_for_model?
79
+ end
80
+
81
+ def enabled_for_model?
82
+ @record.class.paper_trail.enabled?
83
+ end
84
+
85
+ # An attributed is "ignored" if it is listed in the `:ignore` option
86
+ # and/or the `:skip` option. Returns true if an ignored attribute has
87
+ # changed.
88
+ def ignored_attr_has_changed?
89
+ ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
90
+ ignored.any? && (@record.changed & ignored).any?
91
+ end
92
+
93
+ # Returns true if this instance is the current, live one;
94
+ # returns false if this instance came from a previous version.
95
+ def live?
96
+ source_version.nil?
97
+ end
98
+
99
+ # @api private
100
+ def merge_metadata(data)
101
+ # First we merge the model-level metadata in `meta`.
102
+ @record.paper_trail_options[:meta].each do |k, v|
103
+ data[k] =
104
+ if v.respond_to?(:call)
105
+ v.call(@record)
106
+ elsif v.is_a?(Symbol) && @record.respond_to?(v, true)
107
+ # If it is an attribute that is changing in an existing object,
108
+ # be sure to grab the current version.
109
+ if @record.has_attribute?(v) &&
110
+ @record.send("#{v}_changed?".to_sym) &&
111
+ data[:event] != "create"
112
+ @record.send("#{v}_was".to_sym)
113
+ else
114
+ @record.send(v)
115
+ end
116
+ else
117
+ v
118
+ end
119
+ end
120
+
121
+ # Second we merge any extra data from the controller (if available).
122
+ data.merge(PaperTrail.controller_info || {})
123
+ end
124
+
125
+ # Returns the object (not a Version) as it became next.
126
+ # NOTE: if self (the item) was not reified from a version, i.e. it is the
127
+ # "live" item, we return nil. Perhaps we should return self instead?
128
+ def next_version
129
+ subsequent_version = source_version.next
130
+ subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
131
+ rescue # TODO: Rescue something more specific
132
+ nil
133
+ end
134
+
135
+ def notably_changed
136
+ only = @record.paper_trail_options[:only].dup
137
+ # Remove Hash arguments and then evaluate whether the attributes (the
138
+ # keys of the hash) should also get pushed into the collection.
139
+ only.delete_if do |obj|
140
+ obj.is_a?(Hash) &&
141
+ obj.each { |attr, condition|
142
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
143
+ }
144
+ end
145
+ only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
146
+ end
147
+
148
+ # Returns hash of attributes (with appropriate attributes serialized),
149
+ # omitting attributes to be skipped.
150
+ def object_attrs_for_paper_trail
151
+ attrs = attributes_before_change.except(*@record.paper_trail_options[:skip])
152
+ AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
153
+ attrs
154
+ end
155
+
156
+ # Returns who put `@record` into its current state.
157
+ def originator
158
+ (source_version || versions.last).try(:whodunnit)
159
+ end
160
+
161
+ # Returns the object (not a Version) as it was most recently.
162
+ def previous_version
163
+ (source_version ? source_version.previous : versions.last).try(:reify)
164
+ end
165
+
166
+ def record_create
167
+ return unless enabled?
168
+ data = {
169
+ event: @record.paper_trail_event || "create",
170
+ whodunnit: PaperTrail.whodunnit
171
+ }
172
+ if @record.respond_to?(:updated_at)
173
+ data[PaperTrail.timestamp_field] = @record.updated_at
174
+ end
175
+ if record_object_changes? && changed_notably?
176
+ data[:object_changes] = recordable_object_changes
177
+ end
178
+ add_transaction_id_to(data)
179
+ versions_assoc = @record.send(@record.class.versions_association_name)
180
+ version = versions_assoc.create! merge_metadata(data)
181
+ update_transaction_id(version)
182
+ save_associations(version)
183
+ end
184
+
185
+ def record_destroy
186
+ if enabled? && !@record.new_record?
187
+ data = {
188
+ item_id: @record.id,
189
+ item_type: @record.class.base_class.name,
190
+ event: @record.paper_trail_event || "destroy",
191
+ object: recordable_object,
192
+ whodunnit: PaperTrail.whodunnit
193
+ }
194
+ add_transaction_id_to(data)
195
+ version = @record.class.paper_trail.version_class.create(merge_metadata(data))
196
+ if version.errors.any?
197
+ log_version_errors(version, :destroy)
198
+ else
199
+ @record.send("#{@record.class.version_association_name}=", version)
200
+ @record.send(@record.class.versions_association_name).reset
201
+ update_transaction_id(version)
202
+ save_associations(version)
203
+ end
204
+ end
205
+ end
206
+
207
+ # Returns a boolean indicating whether to store serialized version diffs
208
+ # in the `object_changes` column of the version record.
209
+ # @api private
210
+ def record_object_changes?
211
+ @record.paper_trail_options[:save_changes] &&
212
+ @record.class.paper_trail.version_class.column_names.include?("object_changes")
213
+ end
214
+
215
+ def record_update(force)
216
+ if enabled? && (force || changed_notably?)
217
+ data = {
218
+ event: @record.paper_trail_event || "update",
219
+ object: recordable_object,
220
+ whodunnit: PaperTrail.whodunnit
221
+ }
222
+ if @record.respond_to?(:updated_at)
223
+ data[PaperTrail.timestamp_field] = @record.updated_at
224
+ end
225
+ if record_object_changes?
226
+ data[:object_changes] = recordable_object_changes
227
+ end
228
+ add_transaction_id_to(data)
229
+ versions_assoc = @record.send(@record.class.versions_association_name)
230
+ version = versions_assoc.create(merge_metadata(data))
231
+ if version.errors.any?
232
+ log_version_errors(version, :update)
233
+ else
234
+ update_transaction_id(version)
235
+ save_associations(version)
236
+ end
237
+ end
238
+ end
239
+
240
+ # Returns an object which can be assigned to the `object` attribute of a
241
+ # nascent version record. If the `object` column is a postgres `json`
242
+ # column, then a hash can be used in the assignment, otherwise the column
243
+ # is a `text` column, and we must perform the serialization here, using
244
+ # `PaperTrail.serializer`.
245
+ # @api private
246
+ def recordable_object
247
+ if @record.class.paper_trail.version_class.object_col_is_json?
248
+ object_attrs_for_paper_trail
249
+ else
250
+ PaperTrail.serializer.dump(object_attrs_for_paper_trail)
251
+ end
252
+ end
253
+
254
+ # Returns an object which can be assigned to the `object_changes`
255
+ # attribute of a nascent version record. If the `object_changes` column is
256
+ # a postgres `json` column, then a hash can be used in the assignment,
257
+ # otherwise the column is a `text` column, and we must perform the
258
+ # serialization here, using `PaperTrail.serializer`.
259
+ # @api private
260
+ def recordable_object_changes
261
+ if @record.class.paper_trail.version_class.object_changes_col_is_json?
262
+ changes
263
+ else
264
+ PaperTrail.serializer.dump(changes)
265
+ end
266
+ end
267
+
268
+ # Invoked via callback when a user attempts to persist a reified
269
+ # `Version`.
270
+ def reset_timestamp_attrs_for_update_if_needed
271
+ return if live?
272
+ @record.send(:timestamp_attributes_for_update_in_model).each do |column|
273
+ # ActiveRecord 4.2 deprecated `reset_column!` in favor of
274
+ # `restore_column!`.
275
+ if @record.respond_to?("restore_#{column}!")
276
+ @record.send("restore_#{column}!")
277
+ else
278
+ @record.send("reset_#{column}!")
279
+ end
280
+ end
281
+ end
282
+
283
+ # Saves associations if the join table for `VersionAssociation` exists.
284
+ def save_associations(version)
285
+ return unless PaperTrail.config.track_associations?
286
+ save_associations_belongs_to(version)
287
+ save_associations_habtm(version)
288
+ end
289
+
290
+ def save_associations_belongs_to(version)
291
+ @record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
292
+ assoc_version_args = {
293
+ version_id: version.id,
294
+ foreign_key_name: assoc.foreign_key
295
+ }
296
+
297
+ if assoc.options[:polymorphic]
298
+ associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type)
299
+ if associated_record && associated_record.class.paper_trail.enabled?
300
+ assoc_version_args[:foreign_key_id] = associated_record.id
301
+ end
302
+ elsif assoc.klass.paper_trail.enabled?
303
+ assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key)
304
+ end
305
+
306
+ if assoc_version_args.key?(:foreign_key_id)
307
+ PaperTrail::VersionAssociation.create(assoc_version_args)
308
+ end
309
+ end
310
+ end
311
+
312
+ def save_associations_habtm(version)
313
+ # Use the :added and :removed keys to extrapolate the HABTM associations
314
+ # to before any changes were made
315
+ @record.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
316
+ next unless
317
+ @record.class.paper_trail_save_join_tables.include?(a.name) ||
318
+ a.klass.paper_trail.enabled?
319
+ assoc_version_args = {
320
+ version_id: version.transaction_id,
321
+ foreign_key_name: a.name
322
+ }
323
+ assoc_ids =
324
+ @record.send(a.name).to_a.map(&:id) +
325
+ (@record.paper_trail_habtm.try(:[], a.name).try(:[], :removed) || []) -
326
+ (@record.paper_trail_habtm.try(:[], a.name).try(:[], :added) || [])
327
+ assoc_ids.each do |id|
328
+ PaperTrail::VersionAssociation.create(assoc_version_args.merge(foreign_key_id: id))
329
+ end
330
+ end
331
+ end
332
+
333
+ # AR callback.
334
+ # @api private
335
+ def save_version?
336
+ if_condition = @record.paper_trail_options[:if]
337
+ unless_condition = @record.paper_trail_options[:unless]
338
+ (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
339
+ end
340
+
341
+ def source_version
342
+ version
343
+ end
344
+
345
+ # Mimics the `touch` method from `ActiveRecord::Persistence`, but also
346
+ # creates a version. A version is created regardless of options such as
347
+ # `:on`, `:if`, or `:unless`.
348
+ #
349
+ # TODO: look into leveraging the `after_touch` callback from
350
+ # `ActiveRecord` to allow the regular `touch` method to generate a version
351
+ # as normal. May make sense to switch the `record_update` method to
352
+ # leverage an `after_update` callback anyways (likely for v4.0.0)
353
+ def touch_with_version(name = nil)
354
+ unless @record.persisted?
355
+ raise ActiveRecordError, "can not touch on a new record object"
356
+ end
357
+ attributes = @record.send :timestamp_attributes_for_update_in_model
358
+ attributes << name if name
359
+ current_time = @record.send :current_time_from_proper_timezone
360
+ attributes.each { |column|
361
+ @record.send(:write_attribute, column, current_time)
362
+ }
363
+ @record.record_update(true) unless will_record_after_update?
364
+ @record.save!(validate: false)
365
+ end
366
+
367
+ # Returns the object (not a Version) as it was at the given timestamp.
368
+ def version_at(timestamp, reify_options = {})
369
+ # Because a version stores how its object looked *before* the change,
370
+ # we need to look for the first version created *after* the timestamp.
371
+ v = versions.subsequent(timestamp, true).first
372
+ return v.reify(reify_options) if v
373
+ @record unless @record.destroyed?
374
+ end
375
+
376
+ # Returns the objects (not Versions) as they were between the given times.
377
+ def versions_between(start_time, end_time)
378
+ versions = send(@record.class.versions_association_name).between(start_time, end_time)
379
+ versions.collect { |version|
380
+ version_at(version.send(PaperTrail.timestamp_field))
381
+ }
382
+ end
383
+
384
+ # Executes the given method or block without creating a new version.
385
+ def without_versioning(method = nil)
386
+ paper_trail_was_enabled = enabled_for_model?
387
+ @record.class.paper_trail.disable
388
+ if method
389
+ if respond_to?(method)
390
+ public_send(method)
391
+ else
392
+ @record.send(method)
393
+ end
394
+ else
395
+ yield @record
396
+ end
397
+ ensure
398
+ @record.class.paper_trail.enable if paper_trail_was_enabled
399
+ end
400
+
401
+ # Temporarily overwrites the value of whodunnit and then executes the
402
+ # provided block.
403
+ def whodunnit(value)
404
+ raise ArgumentError, "expected to receive a block" unless block_given?
405
+ current_whodunnit = PaperTrail.whodunnit
406
+ PaperTrail.whodunnit = value
407
+ yield @record
408
+ ensure
409
+ PaperTrail.whodunnit = current_whodunnit
410
+ end
411
+
412
+ private
413
+
414
+ def add_transaction_id_to(data)
415
+ return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
416
+ data[:transaction_id] = PaperTrail.transaction_id
417
+ end
418
+
419
+ def log_version_errors(version, action)
420
+ version.logger.warn(
421
+ "Unable to create version for #{action} of #{@record.class.name}##{id}: " +
422
+ version.errors.full_messages.join(", ")
423
+ )
424
+ end
425
+
426
+ # Returns true if `save` will cause `record_update`
427
+ # to be called via the `after_update` callback.
428
+ def will_record_after_update?
429
+ on = @record.paper_trail_options[:on]
430
+ on.nil? || on.include?(:update)
431
+ end
432
+
433
+ def update_transaction_id(version)
434
+ return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
435
+ if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
436
+ PaperTrail.transaction_id = version.id
437
+ version.transaction_id = version.id
438
+ version.save
439
+ end
440
+ end
441
+
442
+ def version
443
+ @record.public_send(@record.class.version_association_name)
444
+ end
445
+
446
+ def versions
447
+ @record.public_send(@record.class.versions_association_name)
448
+ end
449
+ end
450
+ end