paper_trail 9.2.0 → 12.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.
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 +5 -3
  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 +14 -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 +320 -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 +65 -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 +127 -87
  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 +6 -13
  36. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  37. data/lib/paper_trail/version_concern.rb +142 -61
  38. data/lib/paper_trail/version_number.rb +1 -1
  39. data/lib/paper_trail.rb +18 -123
  40. metadata +147 -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