paper_trail 9.2.0 → 14.0.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/USAGE +3 -0
  4. data/lib/generators/paper_trail/{install_generator.rb → install/install_generator.rb} +27 -38
  5. data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +5 -3
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  10. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  11. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +17 -45
  12. data/lib/paper_trail/compatibility.rb +51 -0
  13. data/lib/paper_trail/config.rb +9 -2
  14. data/lib/paper_trail/errors.rb +33 -0
  15. data/lib/paper_trail/events/base.rb +343 -0
  16. data/lib/paper_trail/events/create.rb +32 -0
  17. data/lib/paper_trail/events/destroy.rb +42 -0
  18. data/lib/paper_trail/events/update.rb +76 -0
  19. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  20. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  21. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  22. data/lib/paper_trail/frameworks/rails.rb +1 -2
  23. data/lib/paper_trail/has_paper_trail.rb +20 -17
  24. data/lib/paper_trail/model_config.rb +124 -87
  25. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  26. data/lib/paper_trail/queries/versions/where_object.rb +4 -1
  27. data/lib/paper_trail/queries/versions/where_object_changes.rb +9 -14
  28. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  29. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  30. data/lib/paper_trail/record_trail.rb +137 -436
  31. data/lib/paper_trail/reifier.rb +41 -25
  32. data/lib/paper_trail/request.rb +22 -25
  33. data/lib/paper_trail/serializers/json.rb +0 -10
  34. data/lib/paper_trail/serializers/yaml.rb +41 -11
  35. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  36. data/lib/paper_trail/version_concern.rb +152 -62
  37. data/lib/paper_trail/version_number.rb +2 -2
  38. data/lib/paper_trail.rb +23 -123
  39. metadata +152 -61
  40. data/lib/generators/paper_trail/USAGE +0 -2
  41. data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
  42. /data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
@@ -1,72 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "paper_trail/events/create"
4
+ require "paper_trail/events/destroy"
5
+ require "paper_trail/events/update"
6
+
3
7
  module PaperTrail
4
8
  # Represents the "paper trail" for a single record.
5
9
  class RecordTrail
6
- DPR_TOUCH_WITH_VERSION = <<-STR.squish.freeze
7
- my_model.paper_trail.touch_with_version is deprecated, please use
8
- my_model.paper_trail.save_with_version, which is slightly different. It's
9
- a save, not a touch, so make sure you understand the difference by reading
10
- the ActiveRecord documentation for both.
11
- STR
12
- DPR_WHODUNNIT = <<-STR.squish.freeze
13
- my_model_instance.paper_trail.whodunnit('John') is deprecated,
14
- please use PaperTrail.request(whodunnit: 'John')
15
- STR
16
- DPR_WITHOUT_VERSIONING = <<-STR
17
- my_model_instance.paper_trail.without_versioning is deprecated, without
18
- an exact replacement. To disable versioning for a particular model,
19
-
20
- ```
21
- PaperTrail.request.disable_model(Banana)
22
- # changes to Banana model do not create versions,
23
- # but eg. changes to Kiwi model do.
24
- PaperTrail.request.enable_model(Banana)
25
- ```
26
-
27
- Or, you may want to disable all models,
28
-
29
- ```
30
- PaperTrail.request.enabled = false
31
- # no versions created
32
- PaperTrail.request.enabled = true
33
-
34
- # or, with a block,
35
- PaperTrail.request(enabled: false) do
36
- # no versions created
37
- end
38
- ```
39
- STR
40
-
41
- RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
42
-
43
10
  def initialize(record)
44
11
  @record = record
45
- @in_after_callback = false
46
- end
47
-
48
- def attributes_before_change(is_touch)
49
- Hash[@record.attributes.map do |k, v|
50
- if @record.class.column_names.include?(k)
51
- [k, attribute_in_previous_version(k, is_touch)]
52
- else
53
- [k, v]
54
- end
55
- end]
56
- end
57
-
58
- def changed_and_not_ignored
59
- ignore = @record.paper_trail_options[:ignore].dup
60
- # Remove Hash arguments and then evaluate whether the attributes (the
61
- # keys of the hash) should also get pushed into the collection.
62
- ignore.delete_if do |obj|
63
- obj.is_a?(Hash) &&
64
- obj.each { |attr, condition|
65
- ignore << attr if condition.respond_to?(:call) && condition.call(@record)
66
- }
67
- end
68
- skip = @record.paper_trail_options[:skip]
69
- changed_in_latest_version - ignore - skip
70
12
  end
71
13
 
72
14
  # Invoked after rollbacks to ensure versions records are not created for
@@ -83,108 +25,12 @@ module PaperTrail
83
25
  @record.send("#{@record.class.version_association_name}=", nil)
84
26
  end
85
27
 
86
- # Determines whether it is appropriate to generate a new version
87
- # instance. A timestamp-only update (e.g. only `updated_at` changed) is
88
- # considered notable unless an ignored attribute was also changed.
89
- def changed_notably?
90
- if ignored_attr_has_changed?
91
- timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
92
- (notably_changed - timestamps).any?
93
- else
94
- notably_changed.any?
95
- end
96
- end
97
-
98
- # @api private
99
- def changes
100
- notable_changes = changes_in_latest_version.delete_if { |k, _v|
101
- !notably_changed.include?(k)
102
- }
103
- AttributeSerializers::ObjectChangesAttribute.
104
- new(@record.class).
105
- serialize(notable_changes)
106
- notable_changes.to_hash
107
- end
108
-
109
- # Is PT enabled for this particular record?
110
- # @api private
111
- def enabled?
112
- PaperTrail.enabled? &&
113
- PaperTrail.request.enabled? &&
114
- PaperTrail.request.enabled_for_model?(@record.class)
115
- end
116
-
117
- # Not sure why, but this method was mentioned in the README in the past,
118
- # so we need to deprecate it properly.
119
- # @deprecated
120
- def enabled_for_model?
121
- ::ActiveSupport::Deprecation.warn(
122
- "MyModel#paper_trail.enabled_for_model? is deprecated, use " \
123
- "PaperTrail.request.enabled_for_model?(MyModel) instead.",
124
- caller(1)
125
- )
126
- PaperTrail.request.enabled_for_model?(@record.class)
127
- end
128
-
129
- # An attributed is "ignored" if it is listed in the `:ignore` option
130
- # and/or the `:skip` option. Returns true if an ignored attribute has
131
- # changed.
132
- def ignored_attr_has_changed?
133
- ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
134
- ignored.any? && (changed_in_latest_version & ignored).any?
135
- end
136
-
137
28
  # Returns true if this instance is the current, live one;
138
29
  # returns false if this instance came from a previous version.
139
30
  def live?
140
31
  source_version.nil?
141
32
  end
142
33
 
143
- # Updates `data` from the model's `meta` option and from `controller_info`.
144
- # Metadata is always recorded; that means all three events (create, update,
145
- # destroy) and `update_columns`.
146
- # @api private
147
- def merge_metadata_into(data)
148
- merge_metadata_from_model_into(data)
149
- merge_metadata_from_controller_into(data)
150
- end
151
-
152
- # Updates `data` from `controller_info`.
153
- # @api private
154
- def merge_metadata_from_controller_into(data)
155
- data.merge(PaperTrail.request.controller_info || {})
156
- end
157
-
158
- # Updates `data` from the model's `meta` option.
159
- # @api private
160
- def merge_metadata_from_model_into(data)
161
- @record.paper_trail_options[:meta].each do |k, v|
162
- data[k] = model_metadatum(v, data[:event])
163
- end
164
- end
165
-
166
- # Given a `value` from the model's `meta` option, returns an object to be
167
- # persisted. The `value` can be a simple scalar value, but it can also
168
- # be a symbol that names a model method, or even a Proc.
169
- # @api private
170
- def model_metadatum(value, event)
171
- if value.respond_to?(:call)
172
- value.call(@record)
173
- elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
174
- # If it is an attribute that is changing in an existing object,
175
- # be sure to grab the current version.
176
- if event != "create" &&
177
- @record.has_attribute?(value) &&
178
- attribute_changed_in_latest_version?(value)
179
- attribute_in_previous_version(value, false)
180
- else
181
- @record.send(value)
182
- end
183
- else
184
- value
185
- end
186
- end
187
-
188
34
  # Returns the object (not a Version) as it became next.
189
35
  # NOTE: if self (the item) was not reified from a version, i.e. it is the
190
36
  # "live" item, we return nil. Perhaps we should return self instead?
@@ -195,30 +41,6 @@ module PaperTrail
195
41
  nil
196
42
  end
197
43
 
198
- def notably_changed
199
- only = @record.paper_trail_options[:only].dup
200
- # Remove Hash arguments and then evaluate whether the attributes (the
201
- # keys of the hash) should also get pushed into the collection.
202
- only.delete_if do |obj|
203
- obj.is_a?(Hash) &&
204
- obj.each { |attr, condition|
205
- only << attr if condition.respond_to?(:call) && condition.call(@record)
206
- }
207
- end
208
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
209
- end
210
-
211
- # Returns hash of attributes (with appropriate attributes serialized),
212
- # omitting attributes to be skipped.
213
- #
214
- # @api private
215
- def object_attrs_for_paper_trail(is_touch)
216
- attrs = attributes_before_change(is_touch).
217
- except(*@record.paper_trail_options[:skip])
218
- AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
219
- attrs
220
- end
221
-
222
44
  # Returns who put `@record` into its current state.
223
45
  #
224
46
  # @api public
@@ -234,28 +56,15 @@ module PaperTrail
234
56
  end
235
57
 
236
58
  def record_create
237
- @in_after_callback = true
238
59
  return unless enabled?
239
- versions_assoc = @record.send(@record.class.versions_association_name)
240
- versions_assoc.create! data_for_create
241
- ensure
242
- @in_after_callback = false
243
- end
244
60
 
245
- # Returns data for record create
246
- # @api private
247
- def data_for_create
248
- data = {
249
- event: @record.paper_trail_event || "create",
250
- whodunnit: PaperTrail.request.whodunnit
251
- }
252
- if @record.respond_to?(:updated_at)
253
- data[:created_at] = @record.updated_at
254
- end
255
- if record_object_changes? && changed_notably?
256
- data[:object_changes] = recordable_object_changes(changes)
61
+ build_version_on_create(in_after_callback: true).tap do |version|
62
+ version.save!
63
+ # Because the version object was created using version_class.new instead
64
+ # of versions_assoc.build?, the association cache is unaware. So, we
65
+ # invalidate the `versions` association cache with `reset`.
66
+ versions.reset
257
67
  end
258
- merge_metadata_into(data)
259
68
  end
260
69
 
261
70
  # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
@@ -264,138 +73,49 @@ module PaperTrail
264
73
  # @return - The created version object, so that plugins can use it, e.g.
265
74
  # paper_trail-association_tracking
266
75
  def record_destroy(recording_order)
267
- @in_after_callback = recording_order == "after"
268
- if enabled? && !@record.new_record?
269
- version = @record.class.paper_trail.version_class.create(data_for_destroy)
270
- if version.errors.any?
271
- log_version_errors(version, :destroy)
272
- else
273
- @record.send("#{@record.class.version_association_name}=", version)
274
- @record.send(@record.class.versions_association_name).reset
275
- version
276
- end
277
- end
278
- ensure
279
- @in_after_callback = false
280
- end
76
+ return unless enabled? && !@record.new_record?
77
+ in_after_callback = recording_order == "after"
78
+ event = Events::Destroy.new(@record, in_after_callback)
281
79
 
282
- # Returns data for record destroy
283
- # @api private
284
- def data_for_destroy
285
- data = {
286
- item_id: @record.id,
287
- item_type: @record.class.base_class.name,
288
- event: @record.paper_trail_event || "destroy",
289
- object: recordable_object(false),
290
- whodunnit: PaperTrail.request.whodunnit
291
- }
292
- merge_metadata_into(data)
293
- end
294
-
295
- # Returns a boolean indicating whether to store serialized version diffs
296
- # in the `object_changes` column of the version record.
297
- # @api private
298
- def record_object_changes?
299
- @record.paper_trail_options[:save_changes] &&
300
- @record.class.paper_trail.version_class.column_names.include?("object_changes")
301
- end
80
+ # Merge data from `Event` with data from PT-AT. We no longer use
81
+ # `data_for_destroy` but PT-AT still does.
82
+ data = event.data.merge(data_for_destroy)
302
83
 
303
- # @api private
304
- # @return - The created version object, so that plugins can use it, e.g.
305
- # paper_trail-association_tracking
306
- def record_update(force:, in_after_callback:, is_touch:)
307
- @in_after_callback = in_after_callback
308
- if enabled? && (force || changed_notably?)
309
- versions_assoc = @record.send(@record.class.versions_association_name)
310
- version = versions_assoc.create(data_for_update(is_touch))
311
- if version.errors.any?
312
- log_version_errors(version, :update)
313
- else
314
- version
315
- end
316
- end
317
- ensure
318
- @in_after_callback = false
319
- end
320
-
321
- # Used during `record_update`, returns a hash of data suitable for an AR
322
- # `create`. That is, all the attributes of the nascent `Version` record.
323
- #
324
- # @api private
325
- def data_for_update(is_touch)
326
- data = {
327
- event: @record.paper_trail_event || "update",
328
- object: recordable_object(is_touch),
329
- whodunnit: PaperTrail.request.whodunnit
330
- }
331
- if @record.respond_to?(:updated_at)
332
- data[:created_at] = @record.updated_at
333
- end
334
- if record_object_changes?
335
- data[:object_changes] = recordable_object_changes(changes)
336
- end
337
- merge_metadata_into(data)
338
- end
339
-
340
- # @api private
341
- # @return - The created version object, so that plugins can use it, e.g.
342
- # paper_trail-association_tracking
343
- def record_update_columns(changes)
344
- return unless enabled?
345
- versions_assoc = @record.send(@record.class.versions_association_name)
346
- version = versions_assoc.create(data_for_update_columns(changes))
84
+ version = @record.class.paper_trail.version_class.create(data)
347
85
  if version.errors.any?
348
- log_version_errors(version, :update)
86
+ log_version_errors(version, :destroy)
349
87
  else
88
+ assign_and_reset_version_association(version)
350
89
  version
351
90
  end
352
91
  end
353
92
 
354
- # Returns data for record_update_columns
355
- # @api private
356
- def data_for_update_columns(changes)
357
- data = {
358
- event: @record.paper_trail_event || "update",
359
- object: recordable_object(false),
360
- whodunnit: PaperTrail.request.whodunnit
361
- }
362
- if record_object_changes?
363
- data[:object_changes] = recordable_object_changes(changes)
364
- end
365
- merge_metadata_into(data)
366
- end
367
-
368
- # Returns an object which can be assigned to the `object` attribute of a
369
- # nascent version record. If the `object` column is a postgres `json`
370
- # column, then a hash can be used in the assignment, otherwise the column
371
- # is a `text` column, and we must perform the serialization here, using
372
- # `PaperTrail.serializer`.
373
- #
374
93
  # @api private
375
- def recordable_object(is_touch)
376
- if @record.class.paper_trail.version_class.object_col_is_json?
377
- object_attrs_for_paper_trail(is_touch)
378
- else
379
- PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch))
380
- end
381
- end
94
+ # @param force [boolean] Insert a `Version` even if `@record` has not
95
+ # `changed_notably?`.
96
+ # @param in_after_callback [boolean] True when called from an `after_update`
97
+ # or `after_touch` callback.
98
+ # @param is_touch [boolean] True when called from an `after_touch` callback.
99
+ # @return - The created version object, so that plugins can use it, e.g.
100
+ # paper_trail-association_tracking
101
+ def record_update(force:, in_after_callback:, is_touch:)
102
+ return unless enabled?
382
103
 
383
- # Returns an object which can be assigned to the `object_changes`
384
- # attribute of a nascent version record. If the `object_changes` column is
385
- # a postgres `json` column, then a hash can be used in the assignment,
386
- # otherwise the column is a `text` column, and we must perform the
387
- # serialization here, using `PaperTrail.serializer`.
388
- #
389
- # @api private
390
- def recordable_object_changes(changes)
391
- if PaperTrail.config.object_changes_adapter
392
- changes = PaperTrail.config.object_changes_adapter.diff(changes)
393
- end
104
+ version = build_version_on_update(
105
+ force: force,
106
+ in_after_callback: in_after_callback,
107
+ is_touch: is_touch
108
+ )
109
+ return unless version
394
110
 
395
- if @record.class.paper_trail.version_class.object_changes_col_is_json?
396
- changes
111
+ if version.save
112
+ # Because the version object was created using version_class.new instead
113
+ # of versions_assoc.build?, the association cache is unaware. So, we
114
+ # invalidate the `versions` association cache with `reset`.
115
+ versions.reset
116
+ version
397
117
  else
398
- PaperTrail.serializer.dump(changes)
118
+ log_version_errors(version, :update)
399
119
  end
400
120
  end
401
121
 
@@ -420,56 +140,20 @@ module PaperTrail
420
140
  version
421
141
  end
422
142
 
423
- # Mimics the `touch` method from `ActiveRecord::Persistence` (without
424
- # actually calling `touch`), but also creates a version.
425
- #
426
- # A version is created regardless of options such as `:on`, `:if`, or
427
- # `:unless`.
428
- #
429
- # This is an "update" event. That is, we record the same data we would in
430
- # the case of a normal AR `update`.
431
- #
432
- # Some advanced PT users disable all callbacks (eg. `has_paper_trail(on:
433
- # [])`) and use only this method, giving them complete control over when
434
- # version records are inserted. It's unclear under which specific
435
- # circumstances this technique should be adopted.
436
- #
437
- # @deprecated
438
- def touch_with_version(name = nil)
439
- ::ActiveSupport::Deprecation.warn(DPR_TOUCH_WITH_VERSION, caller(1))
440
- unless @record.persisted?
441
- raise ::ActiveRecord::ActiveRecordError, "can not touch on a new record object"
442
- end
443
- attributes = @record.send :timestamp_attributes_for_update_in_model
444
- attributes << name if name
445
- current_time = @record.send :current_time_from_proper_timezone
446
- attributes.each { |column|
447
- @record.send(:write_attribute, column, current_time)
448
- }
449
- ::PaperTrail.request(enabled: false) do
450
- @record.save!(validate: false)
451
- end
452
- record_update(force: true, in_after_callback: false, is_touch: false)
453
- end
454
-
455
143
  # Save, and create a version record regardless of options such as `:on`,
456
144
  # `:if`, or `:unless`.
457
145
  #
458
- # Arguments are passed to `save`.
146
+ # `in_after_callback`: Indicates if this method is being called within an
147
+ # `after` callback. Defaults to `false`.
148
+ # `options`: Optional arguments passed to `save`.
459
149
  #
460
150
  # This is an "update" event. That is, we record the same data we would in
461
151
  # the case of a normal AR `update`.
462
- #
463
- # In older versions of PaperTrail, a method named `touch_with_version` was
464
- # used for this purpose. `save_with_version` is not exactly the same.
465
- # First, the arguments are different. It passes all arguments to `save`.
466
- # Second, it doesn't set any timestamp attributes prior to the `save` the
467
- # way `touch_with_version` did.
468
- def save_with_version(*args)
152
+ def save_with_version(in_after_callback: false, **options)
469
153
  ::PaperTrail.request(enabled: false) do
470
- @record.save(*args)
154
+ @record.save(**options)
471
155
  end
472
- record_update(force: true, in_after_callback: false, is_touch: false)
156
+ record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
473
157
  end
474
158
 
475
159
  # Like the `update_column` method from `ActiveRecord::Persistence`, but also
@@ -483,9 +167,10 @@ module PaperTrail
483
167
  # creates a version to record those changes.
484
168
  # @api public
485
169
  def update_columns(attributes)
486
- # `@record.update_columns` skips dirty tracking, so we can't just use `@record.changes` or
487
- # @record.saved_changes` from `ActiveModel::Dirty`. We need to build our own hash with the
488
- # changes that will be made directly to the database.
170
+ # `@record.update_columns` skips dirty-tracking, so we can't just use
171
+ # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
172
+ # We need to build our own hash with the changes that will be made
173
+ # directly to the database.
489
174
  changes = {}
490
175
  attributes.each do |k, v|
491
176
  changes[k] = [@record[k], v]
@@ -509,101 +194,117 @@ module PaperTrail
509
194
  versions.collect { |version| version_at(version.created_at) }
510
195
  end
511
196
 
512
- # Executes the given method or block without creating a new version.
513
- # @deprecated
514
- def without_versioning(method = nil)
515
- ::ActiveSupport::Deprecation.warn(DPR_WITHOUT_VERSIONING, caller(1))
516
- paper_trail_was_enabled = PaperTrail.request.enabled_for_model?(@record.class)
517
- PaperTrail.request.disable_model(@record.class)
518
- if method
519
- if respond_to?(method)
520
- public_send(method)
521
- else
522
- @record.send(method)
523
- end
524
- else
525
- yield @record
526
- end
527
- ensure
528
- PaperTrail.request.enable_model(@record.class) if paper_trail_was_enabled
529
- end
197
+ private
530
198
 
531
- # @deprecated
532
- def whodunnit(value)
533
- raise ArgumentError, "expected to receive a block" unless block_given?
534
- ::ActiveSupport::Deprecation.warn(DPR_WHODUNNIT, caller(1))
535
- ::PaperTrail.request(whodunnit: value) do
536
- yield @record
537
- end
199
+ # @api private
200
+ def assign_and_reset_version_association(version)
201
+ @record.send("#{@record.class.version_association_name}=", version)
202
+ @record.send(@record.class.versions_association_name).reset
538
203
  end
539
204
 
540
- private
205
+ # @api private
206
+ def build_version_on_create(in_after_callback:)
207
+ event = Events::Create.new(@record, in_after_callback)
208
+
209
+ # Merge data from `Event` with data from PT-AT. We no longer use
210
+ # `data_for_create` but PT-AT still does.
211
+ data = event.data.merge!(data_for_create)
212
+
213
+ # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
214
+ @record.class.paper_trail.version_class.new(data)
215
+ end
541
216
 
542
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
543
- # https://github.com/paper-trail-gem/paper_trail/pull/899
544
- #
545
217
  # @api private
546
- def attribute_changed_in_latest_version?(attr_name)
547
- if @in_after_callback && RAILS_GTE_5_1
548
- @record.saved_change_to_attribute?(attr_name.to_s)
549
- else
550
- @record.attribute_changed?(attr_name.to_s)
218
+ def build_version_on_update(force:, in_after_callback:, is_touch:)
219
+ event = Events::Update.new(@record, in_after_callback, is_touch, nil)
220
+ return unless force || event.changed_notably?
221
+ data = event.data
222
+
223
+ # Copy the (recently set) `updated_at` from the record to the `created_at`
224
+ # of the `Version`. Without this feature, these two timestamps would
225
+ # differ by a few milliseconds. To some people, it seems a little
226
+ # unnatural to tamper with creation timestamps in this way. But, this
227
+ # feature has existed for a long time, almost a decade now, and some users
228
+ # may rely on it now.
229
+ if @record.respond_to?(:updated_at)
230
+ data[:created_at] = @record.updated_at
551
231
  end
232
+
233
+ # Merge data from `Event` with data from PT-AT. We no longer use
234
+ # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
235
+ # instead of `merge`.
236
+ data.merge!(data_for_update)
237
+
238
+ # Using `version_class.new` reduces memory usage compared to
239
+ # `versions_assoc.build`. It's a trade-off though. We have to clear
240
+ # the association cache (see `versions.reset`) and that could cause an
241
+ # additional query in certain applications.
242
+ @record.class.paper_trail.version_class.new(data)
552
243
  end
553
244
 
554
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
555
- # https://github.com/paper-trail-gem/paper_trail/pull/899
245
+ # PT-AT extends this method to add its transaction id.
556
246
  #
557
- # Event can be any of the three (create, update, destroy).
247
+ # @api public
248
+ def data_for_create
249
+ {}
250
+ end
251
+
252
+ # PT-AT extends this method to add its transaction id.
558
253
  #
559
- # @api private
560
- def attribute_in_previous_version(attr_name, is_touch)
561
- if RAILS_GTE_5_1
562
- if @in_after_callback && !is_touch
563
- # For most events, we want the original value of the attribute, before
564
- # the last save.
565
- @record.attribute_before_last_save(attr_name.to_s)
566
- else
567
- # We are either performing a `record_destroy` or a
568
- # `record_update(is_touch: true)`.
569
- @record.attribute_in_database(attr_name.to_s)
570
- end
571
- else
572
- @record.attribute_was(attr_name.to_s)
573
- end
254
+ # @api public
255
+ def data_for_destroy
256
+ {}
574
257
  end
575
258
 
576
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
577
- # https://github.com/paper-trail-gem/paper_trail/pull/899
259
+ # PT-AT extends this method to add its transaction id.
578
260
  #
579
- # @api private
580
- def changed_in_latest_version
581
- if @in_after_callback && RAILS_GTE_5_1
582
- @record.saved_changes.keys
583
- else
584
- @record.changed
585
- end
261
+ # @api public
262
+ def data_for_update
263
+ {}
586
264
  end
587
265
 
588
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
589
- # https://github.com/paper-trail-gem/paper_trail/pull/899
266
+ # PT-AT extends this method to add its transaction id.
590
267
  #
268
+ # @api public
269
+ def data_for_update_columns
270
+ {}
271
+ end
272
+
273
+ # Is PT enabled for this particular record?
591
274
  # @api private
592
- def changes_in_latest_version
593
- if @in_after_callback && RAILS_GTE_5_1
594
- @record.saved_changes
595
- else
596
- @record.changes
597
- end
275
+ def enabled?
276
+ PaperTrail.enabled? &&
277
+ PaperTrail.request.enabled? &&
278
+ PaperTrail.request.enabled_for_model?(@record.class)
598
279
  end
599
280
 
600
281
  def log_version_errors(version, action)
601
282
  version.logger&.warn(
602
283
  "Unable to create version for #{action} of #{@record.class.name}" \
603
- "##{@record.id}: " + version.errors.full_messages.join(", ")
284
+ "##{@record.id}: " + version.errors.full_messages.join(", ")
604
285
  )
605
286
  end
606
287
 
288
+ # @api private
289
+ # @return - The created version object, so that plugins can use it, e.g.
290
+ # paper_trail-association_tracking
291
+ def record_update_columns(changes)
292
+ return unless enabled?
293
+ data = Events::Update.new(@record, false, false, changes).data
294
+
295
+ # Merge data from `Event` with data from PT-AT. We no longer use
296
+ # `data_for_update_columns` but PT-AT still does.
297
+ data.merge!(data_for_update_columns)
298
+
299
+ versions_assoc = @record.send(@record.class.versions_association_name)
300
+ version = versions_assoc.create(data)
301
+ if version.errors.any?
302
+ log_version_errors(version, :update)
303
+ else
304
+ version
305
+ end
306
+ end
307
+
607
308
  def version
608
309
  @record.public_send(@record.class.version_association_name)
609
310
  end