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.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/lib/generators/paper_trail/install/USAGE +3 -0
- data/lib/generators/paper_trail/{install_generator.rb → install/install_generator.rb} +27 -38
- data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +5 -3
- data/lib/generators/paper_trail/migration_generator.rb +38 -0
- data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
- data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
- data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +17 -45
- data/lib/paper_trail/compatibility.rb +51 -0
- data/lib/paper_trail/config.rb +9 -2
- data/lib/paper_trail/errors.rb +33 -0
- data/lib/paper_trail/events/base.rb +343 -0
- data/lib/paper_trail/events/create.rb +32 -0
- data/lib/paper_trail/events/destroy.rb +42 -0
- data/lib/paper_trail/events/update.rb +76 -0
- data/lib/paper_trail/frameworks/active_record.rb +9 -2
- data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
- data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
- data/lib/paper_trail/frameworks/rails.rb +1 -2
- data/lib/paper_trail/has_paper_trail.rb +20 -17
- data/lib/paper_trail/model_config.rb +124 -87
- data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
- data/lib/paper_trail/queries/versions/where_object.rb +4 -1
- data/lib/paper_trail/queries/versions/where_object_changes.rb +9 -14
- data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
- data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
- data/lib/paper_trail/record_trail.rb +137 -436
- data/lib/paper_trail/reifier.rb +41 -25
- data/lib/paper_trail/request.rb +22 -25
- data/lib/paper_trail/serializers/json.rb +0 -10
- data/lib/paper_trail/serializers/yaml.rb +41 -11
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
- data/lib/paper_trail/version_concern.rb +152 -62
- data/lib/paper_trail/version_number.rb +2 -2
- data/lib/paper_trail.rb +23 -123
- metadata +152 -61
- data/lib/generators/paper_trail/USAGE +0 -2
- data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
- /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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
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
|
-
|
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, :
|
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
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
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
|
396
|
-
|
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
|
-
|
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
|
-
#
|
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(
|
154
|
+
@record.save(**options)
|
471
155
|
end
|
472
|
-
record_update(force: true, in_after_callback:
|
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
|
487
|
-
# @record.saved_changes` from `ActiveModel::Dirty`.
|
488
|
-
# changes that will be made
|
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
|
-
|
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
|
-
# @
|
532
|
-
def
|
533
|
-
|
534
|
-
|
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
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
560
|
-
def
|
561
|
-
|
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
|
-
#
|
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
|
580
|
-
def
|
581
|
-
|
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
|
-
#
|
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
|
593
|
-
|
594
|
-
|
595
|
-
|
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
|
-
|
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
|