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.
- 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} +15 -38
- data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
- 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 +14 -46
- 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 +320 -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 +65 -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 +127 -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 +8 -13
- 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 +94 -411
- data/lib/paper_trail/reifier.rb +41 -25
- data/lib/paper_trail/request.rb +0 -3
- data/lib/paper_trail/serializers/json.rb +0 -10
- data/lib/paper_trail/serializers/yaml.rb +6 -13
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
- data/lib/paper_trail/version_concern.rb +142 -61
- data/lib/paper_trail/version_number.rb +1 -1
- data/lib/paper_trail.rb +18 -123
- metadata +147 -56
- data/lib/generators/paper_trail/USAGE +0 -2
- 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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
#
|
78
|
+
# PT-AT extends this method to add its transaction id.
|
79
|
+
#
|
246
80
|
# @api private
|
247
81
|
def data_for_create
|
248
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
#
|
108
|
+
# PT-AT extends this method to add its transaction id.
|
109
|
+
#
|
283
110
|
# @api private
|
284
111
|
def data_for_destroy
|
285
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
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
|
-
#
|
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
|
326
|
-
|
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(
|
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
|
-
#
|
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
|
391
|
-
|
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(
|
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
|
487
|
-
# @record.saved_changes` from `ActiveModel::Dirty`.
|
488
|
-
# changes that will be made
|
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
|
547
|
-
|
548
|
-
|
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
|
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
|
574
|
-
end
|
255
|
+
def build_version_on_create(in_after_callback:)
|
256
|
+
event = Events::Create.new(@record, in_after_callback)
|
575
257
|
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
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
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
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
|
-
|
286
|
+
"##{@record.id}: " + version.errors.full_messages.join(", ")
|
604
287
|
)
|
605
288
|
end
|
606
289
|
|