paper_trail 9.2.0 → 12.1.0

Sign up to get free protection for your applications and to get access to all the features.
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} +15 -38
  5. data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
  6. data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +2 -2
  7. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  8. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  9. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  10. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +8 -46
  13. data/lib/paper_trail/compatibility.rb +51 -0
  14. data/lib/paper_trail/config.rb +9 -2
  15. data/lib/paper_trail/errors.rb +33 -0
  16. data/lib/paper_trail/events/base.rb +305 -0
  17. data/lib/paper_trail/events/create.rb +32 -0
  18. data/lib/paper_trail/events/destroy.rb +42 -0
  19. data/lib/paper_trail/events/update.rb +60 -0
  20. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  21. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  22. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  23. data/lib/paper_trail/frameworks/rails.rb +1 -2
  24. data/lib/paper_trail/has_paper_trail.rb +20 -17
  25. data/lib/paper_trail/model_config.rb +103 -71
  26. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  27. data/lib/paper_trail/queries/versions/where_object.rb +4 -1
  28. data/lib/paper_trail/queries/versions/where_object_changes.rb +8 -13
  29. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  30. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  31. data/lib/paper_trail/record_trail.rb +94 -411
  32. data/lib/paper_trail/reifier.rb +41 -25
  33. data/lib/paper_trail/request.rb +0 -3
  34. data/lib/paper_trail/serializers/json.rb +0 -10
  35. data/lib/paper_trail/serializers/yaml.rb +5 -12
  36. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  37. data/lib/paper_trail/version_concern.rb +141 -61
  38. data/lib/paper_trail/version_number.rb +2 -2
  39. data/lib/paper_trail.rb +16 -123
  40. metadata +159 -56
  41. data/lib/generators/paper_trail/USAGE +0 -2
  42. data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
@@ -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,29 +25,6 @@ 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
28
  # Is PT enabled for this particular record?
110
29
  # @api private
111
30
  def enabled?
@@ -114,77 +33,12 @@ module PaperTrail
114
33
  PaperTrail.request.enabled_for_model?(@record.class)
115
34
  end
116
35
 
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
36
  # Returns true if this instance is the current, live one;
138
37
  # returns false if this instance came from a previous version.
139
38
  def live?
140
39
  source_version.nil?
141
40
  end
142
41
 
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
42
  # Returns the object (not a Version) as it became next.
189
43
  # NOTE: if self (the item) was not reified from a version, i.e. it is the
190
44
  # "live" item, we return nil. Perhaps we should return self instead?
@@ -195,30 +49,6 @@ module PaperTrail
195
49
  nil
196
50
  end
197
51
 
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
52
  # Returns who put `@record` into its current state.
223
53
  #
224
54
  # @api public
@@ -234,28 +64,22 @@ module PaperTrail
234
64
  end
235
65
 
236
66
  def record_create
237
- @in_after_callback = true
238
67
  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
68
+
69
+ build_version_on_create(in_after_callback: true).tap do |version|
70
+ version.save!
71
+ # Because the version object was created using version_class.new instead
72
+ # of versions_assoc.build?, the association cache is unaware. So, we
73
+ # invalidate the `versions` association cache with `reset`.
74
+ versions.reset
75
+ end
243
76
  end
244
77
 
245
- # Returns data for record create
78
+ # PT-AT extends this method to add its transaction id.
79
+ #
246
80
  # @api private
247
81
  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)
257
- end
258
- merge_metadata_into(data)
82
+ {}
259
83
  end
260
84
 
261
85
  # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
@@ -264,77 +88,59 @@ module PaperTrail
264
88
  # @return - The created version object, so that plugins can use it, e.g.
265
89
  # paper_trail-association_tracking
266
90
  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
91
+ return unless enabled? && !@record.new_record?
92
+ in_after_callback = recording_order == "after"
93
+ event = Events::Destroy.new(@record, in_after_callback)
94
+
95
+ # Merge data from `Event` with data from PT-AT. We no longer use
96
+ # `data_for_destroy` but PT-AT still does.
97
+ data = event.data.merge(data_for_destroy)
98
+
99
+ version = @record.class.paper_trail.version_class.create(data)
100
+ if version.errors.any?
101
+ log_version_errors(version, :destroy)
102
+ else
103
+ assign_and_reset_version_association(version)
104
+ version
277
105
  end
278
- ensure
279
- @in_after_callback = false
280
106
  end
281
107
 
282
- # Returns data for record destroy
108
+ # PT-AT extends this method to add its transaction id.
109
+ #
283
110
  # @api private
284
111
  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")
112
+ {}
301
113
  end
302
114
 
303
115
  # @api private
304
116
  # @return - The created version object, so that plugins can use it, e.g.
305
117
  # paper_trail-association_tracking
306
118
  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
119
+ return unless enabled?
120
+
121
+ version = build_version_on_update(
122
+ force: force,
123
+ in_after_callback: in_after_callback,
124
+ is_touch: is_touch
125
+ )
126
+ return unless version
127
+
128
+ if version.save
129
+ # Because the version object was created using version_class.new instead
130
+ # of versions_assoc.build?, the association cache is unaware. So, we
131
+ # invalidate the `versions` association cache with `reset`.
132
+ versions.reset
133
+ version
134
+ else
135
+ log_version_errors(version, :update)
316
136
  end
317
- ensure
318
- @in_after_callback = false
319
137
  end
320
138
 
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.
139
+ # PT-AT extends this method to add its transaction id.
323
140
  #
324
141
  # @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)
142
+ def data_for_update
143
+ {}
338
144
  end
339
145
 
340
146
  # @api private
@@ -342,8 +148,14 @@ module PaperTrail
342
148
  # paper_trail-association_tracking
343
149
  def record_update_columns(changes)
344
150
  return unless enabled?
151
+ event = Events::Update.new(@record, false, false, changes)
152
+
153
+ # Merge data from `Event` with data from PT-AT. We no longer use
154
+ # `data_for_update_columns` but PT-AT still does.
155
+ data = event.data.merge(data_for_update_columns)
156
+
345
157
  versions_assoc = @record.send(@record.class.versions_association_name)
346
- version = versions_assoc.create(data_for_update_columns(changes))
158
+ version = versions_assoc.create(data)
347
159
  if version.errors.any?
348
160
  log_version_errors(version, :update)
349
161
  else
@@ -351,52 +163,11 @@ module PaperTrail
351
163
  end
352
164
  end
353
165
 
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
- # @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
382
-
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`.
166
+ # PT-AT extends this method to add its transaction id.
388
167
  #
389
168
  # @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
394
-
395
- if @record.class.paper_trail.version_class.object_changes_col_is_json?
396
- changes
397
- else
398
- PaperTrail.serializer.dump(changes)
399
- end
169
+ def data_for_update_columns
170
+ {}
400
171
  end
401
172
 
402
173
  # Invoked via callback when a user attempts to persist a reified
@@ -420,38 +191,6 @@ module PaperTrail
420
191
  version
421
192
  end
422
193
 
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
194
  # Save, and create a version record regardless of options such as `:on`,
456
195
  # `:if`, or `:unless`.
457
196
  #
@@ -459,15 +198,9 @@ module PaperTrail
459
198
  #
460
199
  # This is an "update" event. That is, we record the same data we would in
461
200
  # 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)
201
+ def save_with_version(**options)
469
202
  ::PaperTrail.request(enabled: false) do
470
- @record.save(*args)
203
+ @record.save(**options)
471
204
  end
472
205
  record_update(force: true, in_after_callback: false, is_touch: false)
473
206
  end
@@ -483,9 +216,10 @@ module PaperTrail
483
216
  # creates a version to record those changes.
484
217
  # @api public
485
218
  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.
219
+ # `@record.update_columns` skips dirty-tracking, so we can't just use
220
+ # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
221
+ # We need to build our own hash with the changes that will be made
222
+ # directly to the database.
489
223
  changes = {}
490
224
  attributes.each do |k, v|
491
225
  changes[k] = [@record[k], v]
@@ -509,98 +243,47 @@ module PaperTrail
509
243
  versions.collect { |version| version_at(version.created_at) }
510
244
  end
511
245
 
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
530
-
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
538
- end
539
-
540
246
  private
541
247
 
542
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
543
- # https://github.com/paper-trail-gem/paper_trail/pull/899
544
- #
545
248
  # @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)
551
- end
249
+ def assign_and_reset_version_association(version)
250
+ @record.send("#{@record.class.version_association_name}=", version)
251
+ @record.send(@record.class.versions_association_name).reset
552
252
  end
553
253
 
554
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
555
- # https://github.com/paper-trail-gem/paper_trail/pull/899
556
- #
557
- # Event can be any of the three (create, update, destroy).
558
- #
559
254
  # @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
574
- end
255
+ def build_version_on_create(in_after_callback:)
256
+ event = Events::Create.new(@record, in_after_callback)
575
257
 
576
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
577
- # https://github.com/paper-trail-gem/paper_trail/pull/899
578
- #
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
258
+ # Merge data from `Event` with data from PT-AT. We no longer use
259
+ # `data_for_create` but PT-AT still does.
260
+ data = event.data.merge!(data_for_create)
261
+
262
+ # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
263
+ @record.class.paper_trail.version_class.new(data)
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
590
- #
591
266
  # @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
267
+ def build_version_on_update(force:, in_after_callback:, is_touch:)
268
+ event = Events::Update.new(@record, in_after_callback, is_touch, nil)
269
+ return unless force || event.changed_notably?
270
+
271
+ # Merge data from `Event` with data from PT-AT. We no longer use
272
+ # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
273
+ # instead of `merge`.
274
+ data = event.data.merge!(data_for_update)
275
+
276
+ # Using `version_class.new` reduces memory usage compared to
277
+ # `versions_assoc.build`. It's a trade-off though. We have to clear
278
+ # the association cache (see `versions.reset`) and that could cause an
279
+ # additional query in certain applications.
280
+ @record.class.paper_trail.version_class.new(data)
598
281
  end
599
282
 
600
283
  def log_version_errors(version, action)
601
284
  version.logger&.warn(
602
285
  "Unable to create version for #{action} of #{@record.class.name}" \
603
- "##{@record.id}: " + version.errors.full_messages.join(", ")
286
+ "##{@record.id}: " + version.errors.full_messages.join(", ")
604
287
  )
605
288
  end
606
289