paper_trail 9.2.0 → 10.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/lib/generators/paper_trail/install/USAGE +3 -0
- data/lib/generators/paper_trail/install/install_generator.rb +72 -0
- 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 +1 -1
- data/lib/generators/paper_trail/migration_generator.rb +37 -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 +17 -0
- data/lib/paper_trail.rb +0 -91
- data/lib/paper_trail/config.rb +28 -2
- data/lib/paper_trail/events/base.rb +298 -0
- data/lib/paper_trail/events/create.rb +31 -0
- data/lib/paper_trail/events/destroy.rb +34 -0
- data/lib/paper_trail/events/update.rb +59 -0
- data/lib/paper_trail/has_paper_trail.rb +0 -2
- data/lib/paper_trail/model_config.rb +12 -42
- data/lib/paper_trail/queries/versions/where_object.rb +4 -1
- data/lib/paper_trail/queries/versions/where_object_changes.rb +1 -1
- data/lib/paper_trail/record_trail.rb +65 -412
- data/lib/paper_trail/version_concern.rb +59 -34
- data/lib/paper_trail/version_number.rb +2 -2
- metadata +38 -29
- data/lib/generators/paper_trail/USAGE +0 -2
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "paper_trail/events/base"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
module Events
|
7
|
+
# See docs in `Base`.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class Create < Base
|
11
|
+
# Return attributes of nascent `Version` record.
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
def data
|
15
|
+
data = {
|
16
|
+
event: @record.paper_trail_event || "create",
|
17
|
+
whodunnit: PaperTrail.request.whodunnit
|
18
|
+
}
|
19
|
+
if @record.respond_to?(:updated_at)
|
20
|
+
data[:created_at] = @record.updated_at
|
21
|
+
end
|
22
|
+
if record_object_changes? && changed_notably?
|
23
|
+
changes = notable_changes
|
24
|
+
data[:object_changes] = prepare_object_changes(changes)
|
25
|
+
end
|
26
|
+
merge_item_subtype_into(data)
|
27
|
+
merge_metadata_into(data)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "paper_trail/events/base"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
module Events
|
7
|
+
# See docs in `Base`.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class Destroy < Base
|
11
|
+
# Return attributes of nascent `Version` record.
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
def data
|
15
|
+
data = {
|
16
|
+
item_id: @record.id,
|
17
|
+
item_type: @record.class.base_class.name,
|
18
|
+
event: @record.paper_trail_event || "destroy",
|
19
|
+
whodunnit: PaperTrail.request.whodunnit
|
20
|
+
}
|
21
|
+
if record_object?
|
22
|
+
data[:object] = recordable_object(false)
|
23
|
+
end
|
24
|
+
if record_object_changes?
|
25
|
+
# Rails' implementation returns nothing on destroy :/
|
26
|
+
changes = @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h
|
27
|
+
data[:object_changes] = prepare_object_changes(changes)
|
28
|
+
end
|
29
|
+
merge_item_subtype_into(data)
|
30
|
+
merge_metadata_into(data)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "paper_trail/events/base"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
module Events
|
7
|
+
# See docs in `Base`.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class Update < Base
|
11
|
+
# - is_touch - [boolean] - Used in the two situations that are touch-like:
|
12
|
+
# - `after_touch` we call `RecordTrail#record_update`
|
13
|
+
# - force_changes - [Hash] - Only used by `RecordTrail#update_columns`,
|
14
|
+
# because there dirty-tracking is off, so it has to track its own changes.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def initialize(record, in_after_callback, is_touch, force_changes)
|
18
|
+
super(record, in_after_callback)
|
19
|
+
@is_touch = is_touch
|
20
|
+
@force_changes = force_changes
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return attributes of nascent `Version` record.
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
def data
|
27
|
+
data = {
|
28
|
+
event: @record.paper_trail_event || "update",
|
29
|
+
whodunnit: PaperTrail.request.whodunnit
|
30
|
+
}
|
31
|
+
if @record.respond_to?(:updated_at)
|
32
|
+
data[:created_at] = @record.updated_at
|
33
|
+
end
|
34
|
+
if record_object?
|
35
|
+
data[:object] = recordable_object(@is_touch)
|
36
|
+
end
|
37
|
+
if record_object_changes?
|
38
|
+
changes = @force_changes.nil? ? notable_changes : @force_changes
|
39
|
+
data[:object_changes] = prepare_object_changes(changes)
|
40
|
+
end
|
41
|
+
merge_item_subtype_into(data)
|
42
|
+
merge_metadata_into(data)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# `touch` cannot record `object_changes` because rails' `touch` does not
|
48
|
+
# perform dirty-tracking. Specifically, methods from `Dirty`, like
|
49
|
+
# `saved_changes`, return the same values before and after `touch`.
|
50
|
+
#
|
51
|
+
# See https://github.com/rails/rails/issues/33429
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
def record_object_changes?
|
55
|
+
!@is_touch && super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -51,8 +51,6 @@ module PaperTrail
|
|
51
51
|
# is `:versions`.
|
52
52
|
# - :version - The name to use for the method which returns the version
|
53
53
|
# the instance was reified from. Default is `:version`.
|
54
|
-
# - :save_changes - Whether or not to save changes to the object_changes
|
55
|
-
# column if it exists. Default is true
|
56
54
|
# - :join_tables - If the model has a has_and_belongs_to_many relation
|
57
55
|
# with an unpapertrailed model, passing the name of the association to
|
58
56
|
# the join_tables option will paper trail the join table but not save
|
@@ -4,27 +4,6 @@ module PaperTrail
|
|
4
4
|
# Configures an ActiveRecord model, mostly at application boot time, but also
|
5
5
|
# sometimes mid-request, with methods like enable/disable.
|
6
6
|
class ModelConfig
|
7
|
-
DPR_DISABLE = <<-STR.squish.freeze
|
8
|
-
MyModel.paper_trail.disable is deprecated, use
|
9
|
-
PaperTrail.request.disable_model(MyModel). This new API makes it clear
|
10
|
-
that only the current request is affected, not all threads. Also, all
|
11
|
-
other request-variables now go through the same `request` method, so this
|
12
|
-
new API is more consistent.
|
13
|
-
STR
|
14
|
-
DPR_ENABLE = <<-STR.squish.freeze
|
15
|
-
MyModel.paper_trail.enable is deprecated, use
|
16
|
-
PaperTrail.request.enable_model(MyModel). This new API makes it clear
|
17
|
-
that only the current request is affected, not all threads. Also, all
|
18
|
-
other request-variables now go through the same `request` method, so this
|
19
|
-
new API is more consistent.
|
20
|
-
STR
|
21
|
-
DPR_ENABLED = <<-STR.squish.freeze
|
22
|
-
MyModel.paper_trail.enabled? is deprecated, use
|
23
|
-
PaperTrail.request.enabled_for_model?(MyModel). This new API makes it clear
|
24
|
-
that this is a setting specific to the current request, not all threads.
|
25
|
-
Also, all other request-variables now go through the same `request`
|
26
|
-
method, so this new API is more consistent.
|
27
|
-
STR
|
28
7
|
E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
|
29
8
|
paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
|
30
9
|
belongs_to_required_by_default. Use on_destroy(:before)
|
@@ -44,24 +23,6 @@ module PaperTrail
|
|
44
23
|
@model_class = model_class
|
45
24
|
end
|
46
25
|
|
47
|
-
# @deprecated
|
48
|
-
def disable
|
49
|
-
::ActiveSupport::Deprecation.warn(DPR_DISABLE, caller(1))
|
50
|
-
::PaperTrail.request.disable_model(@model_class)
|
51
|
-
end
|
52
|
-
|
53
|
-
# @deprecated
|
54
|
-
def enable
|
55
|
-
::ActiveSupport::Deprecation.warn(DPR_ENABLE, caller(1))
|
56
|
-
::PaperTrail.request.enable_model(@model_class)
|
57
|
-
end
|
58
|
-
|
59
|
-
# @deprecated
|
60
|
-
def enabled?
|
61
|
-
::ActiveSupport::Deprecation.warn(DPR_ENABLED, caller(1))
|
62
|
-
::PaperTrail.request.enabled_for_model?(@model_class)
|
63
|
-
end
|
64
|
-
|
65
26
|
# Adds a callback that records a version after a "create" event.
|
66
27
|
#
|
67
28
|
# @api public
|
@@ -169,22 +130,31 @@ module PaperTrail
|
|
169
130
|
end
|
170
131
|
|
171
132
|
def setup_associations(options)
|
133
|
+
# @api private - version_association_name
|
172
134
|
@model_class.class_attribute :version_association_name
|
173
135
|
@model_class.version_association_name = options[:version] || :version
|
174
136
|
|
175
137
|
# The version this instance was reified from.
|
138
|
+
# @api public
|
176
139
|
@model_class.send :attr_accessor, @model_class.version_association_name
|
177
140
|
|
141
|
+
# @api private - `version_class_name` - However, `rails_admin` has been
|
142
|
+
# using it since 2014 (see `rails_admin/extensions/paper_trail/auditing_adapter.rb`,
|
143
|
+
# https://github.com/sferik/rails_admin/commit/959e1bd4e47e0369d264b58bbbe972ff863767cd)
|
144
|
+
# In PR _____ () we ask them to use `paper_trail_options` instead.
|
178
145
|
@model_class.class_attribute :version_class_name
|
179
146
|
@model_class.version_class_name = options[:class_name] || "PaperTrail::Version"
|
180
147
|
|
148
|
+
# @api private - versions_association_name
|
181
149
|
@model_class.class_attribute :versions_association_name
|
182
150
|
@model_class.versions_association_name = options[:versions] || :versions
|
183
151
|
|
152
|
+
# @api public - paper_trail_event
|
184
153
|
@model_class.send :attr_accessor, :paper_trail_event
|
185
154
|
|
186
155
|
assert_concrete_activerecord_class(@model_class.version_class_name)
|
187
156
|
|
157
|
+
# @api public
|
188
158
|
@model_class.has_many(
|
189
159
|
@model_class.versions_association_name,
|
190
160
|
-> { order(model.timestamp_sort_order) },
|
@@ -200,6 +170,9 @@ module PaperTrail
|
|
200
170
|
end
|
201
171
|
|
202
172
|
def setup_options(options)
|
173
|
+
# @api public - paper_trail_options - Let's encourage plugins to use
|
174
|
+
# eg. `paper_trail_options[:class_name]` rather than `version_class_name`
|
175
|
+
# because the former is documented and the latter is not.
|
203
176
|
@model_class.class_attribute :paper_trail_options
|
204
177
|
@model_class.paper_trail_options = options.dup
|
205
178
|
|
@@ -211,9 +184,6 @@ module PaperTrail
|
|
211
184
|
end
|
212
185
|
|
213
186
|
@model_class.paper_trail_options[:meta] ||= {}
|
214
|
-
if @model_class.paper_trail_options[:save_changes].nil?
|
215
|
-
@model_class.paper_trail_options[:save_changes] = true
|
216
|
-
end
|
217
187
|
end
|
218
188
|
end
|
219
189
|
end
|
@@ -18,7 +18,10 @@ module PaperTrail
|
|
18
18
|
|
19
19
|
# @api private
|
20
20
|
def execute
|
21
|
-
|
21
|
+
column = @version_model_class.columns_hash["object"]
|
22
|
+
raise "where_object can't be called without an object column" unless column
|
23
|
+
|
24
|
+
case column.type
|
22
25
|
when :jsonb
|
23
26
|
jsonb
|
24
27
|
when :json
|
@@ -23,7 +23,7 @@ module PaperTrail
|
|
23
23
|
|
24
24
|
# @api private
|
25
25
|
def execute
|
26
|
-
if PaperTrail.config.object_changes_adapter
|
26
|
+
if PaperTrail.config.object_changes_adapter&.respond_to?(:where_object_changes)
|
27
27
|
return PaperTrail.config.object_changes_adapter.where_object_changes(
|
28
28
|
@version_model_class, @attributes
|
29
29
|
)
|
@@ -1,72 +1,16 @@
|
|
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
10
|
RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
|
42
11
|
|
43
12
|
def initialize(record)
|
44
13
|
@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
14
|
end
|
71
15
|
|
72
16
|
# Invoked after rollbacks to ensure versions records are not created for
|
@@ -83,29 +27,6 @@ module PaperTrail
|
|
83
27
|
@record.send("#{@record.class.version_association_name}=", nil)
|
84
28
|
end
|
85
29
|
|
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
30
|
# Is PT enabled for this particular record?
|
110
31
|
# @api private
|
111
32
|
def enabled?
|
@@ -114,77 +35,12 @@ module PaperTrail
|
|
114
35
|
PaperTrail.request.enabled_for_model?(@record.class)
|
115
36
|
end
|
116
37
|
|
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
38
|
# Returns true if this instance is the current, live one;
|
138
39
|
# returns false if this instance came from a previous version.
|
139
40
|
def live?
|
140
41
|
source_version.nil?
|
141
42
|
end
|
142
43
|
|
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
44
|
# Returns the object (not a Version) as it became next.
|
189
45
|
# NOTE: if self (the item) was not reified from a version, i.e. it is the
|
190
46
|
# "live" item, we return nil. Perhaps we should return self instead?
|
@@ -195,30 +51,6 @@ module PaperTrail
|
|
195
51
|
nil
|
196
52
|
end
|
197
53
|
|
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
54
|
# Returns who put `@record` into its current state.
|
223
55
|
#
|
224
56
|
# @api public
|
@@ -234,28 +66,22 @@ module PaperTrail
|
|
234
66
|
end
|
235
67
|
|
236
68
|
def record_create
|
237
|
-
@in_after_callback = true
|
238
69
|
return unless enabled?
|
70
|
+
event = Events::Create.new(@record, true)
|
71
|
+
|
72
|
+
# Merge data from `Event` with data from PT-AT. We no longer use
|
73
|
+
# `data_for_create` but PT-AT still does.
|
74
|
+
data = event.data.merge(data_for_create)
|
75
|
+
|
239
76
|
versions_assoc = @record.send(@record.class.versions_association_name)
|
240
|
-
versions_assoc.create!
|
241
|
-
ensure
|
242
|
-
@in_after_callback = false
|
77
|
+
versions_assoc.create!(data)
|
243
78
|
end
|
244
79
|
|
245
|
-
#
|
80
|
+
# PT-AT extends this method to add its transaction id.
|
81
|
+
#
|
246
82
|
# @api private
|
247
83
|
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)
|
84
|
+
{}
|
259
85
|
end
|
260
86
|
|
261
87
|
# `recording_order` is "after" or "before". See ModelConfig#on_destroy.
|
@@ -264,77 +90,56 @@ module PaperTrail
|
|
264
90
|
# @return - The created version object, so that plugins can use it, e.g.
|
265
91
|
# paper_trail-association_tracking
|
266
92
|
def record_destroy(recording_order)
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
93
|
+
return unless enabled? && !@record.new_record?
|
94
|
+
in_after_callback = recording_order == "after"
|
95
|
+
event = Events::Destroy.new(@record, in_after_callback)
|
96
|
+
|
97
|
+
# Merge data from `Event` with data from PT-AT. We no longer use
|
98
|
+
# `data_for_destroy` but PT-AT still does.
|
99
|
+
data = event.data.merge(data_for_destroy)
|
100
|
+
|
101
|
+
version = @record.class.paper_trail.version_class.create(data)
|
102
|
+
if version.errors.any?
|
103
|
+
log_version_errors(version, :destroy)
|
104
|
+
else
|
105
|
+
assign_and_reset_version_association(version)
|
106
|
+
version
|
277
107
|
end
|
278
|
-
ensure
|
279
|
-
@in_after_callback = false
|
280
108
|
end
|
281
109
|
|
282
|
-
#
|
110
|
+
# PT-AT extends this method to add its transaction id.
|
111
|
+
#
|
283
112
|
# @api private
|
284
113
|
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")
|
114
|
+
{}
|
301
115
|
end
|
302
116
|
|
303
117
|
# @api private
|
304
118
|
# @return - The created version object, so that plugins can use it, e.g.
|
305
119
|
# paper_trail-association_tracking
|
306
120
|
def record_update(force:, in_after_callback:, is_touch:)
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
121
|
+
return unless enabled?
|
122
|
+
event = Events::Update.new(@record, in_after_callback, is_touch, nil)
|
123
|
+
return unless force || event.changed_notably?
|
124
|
+
|
125
|
+
# Merge data from `Event` with data from PT-AT. We no longer use
|
126
|
+
# `data_for_update` but PT-AT still does.
|
127
|
+
data = event.data.merge(data_for_update)
|
128
|
+
|
129
|
+
versions_assoc = @record.send(@record.class.versions_association_name)
|
130
|
+
version = versions_assoc.create(data)
|
131
|
+
if version.errors.any?
|
132
|
+
log_version_errors(version, :update)
|
133
|
+
else
|
134
|
+
version
|
316
135
|
end
|
317
|
-
ensure
|
318
|
-
@in_after_callback = false
|
319
136
|
end
|
320
137
|
|
321
|
-
#
|
322
|
-
# `create`. That is, all the attributes of the nascent `Version` record.
|
138
|
+
# PT-AT extends this method to add its transaction id.
|
323
139
|
#
|
324
140
|
# @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)
|
141
|
+
def data_for_update
|
142
|
+
{}
|
338
143
|
end
|
339
144
|
|
340
145
|
# @api private
|
@@ -342,8 +147,14 @@ module PaperTrail
|
|
342
147
|
# paper_trail-association_tracking
|
343
148
|
def record_update_columns(changes)
|
344
149
|
return unless enabled?
|
150
|
+
event = Events::Update.new(@record, false, false, changes)
|
151
|
+
|
152
|
+
# Merge data from `Event` with data from PT-AT. We no longer use
|
153
|
+
# `data_for_update_columns` but PT-AT still does.
|
154
|
+
data = event.data.merge(data_for_update_columns)
|
155
|
+
|
345
156
|
versions_assoc = @record.send(@record.class.versions_association_name)
|
346
|
-
version = versions_assoc.create(
|
157
|
+
version = versions_assoc.create(data)
|
347
158
|
if version.errors.any?
|
348
159
|
log_version_errors(version, :update)
|
349
160
|
else
|
@@ -351,52 +162,11 @@ module PaperTrail
|
|
351
162
|
end
|
352
163
|
end
|
353
164
|
|
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`.
|
165
|
+
# PT-AT extends this method to add its transaction id.
|
373
166
|
#
|
374
167
|
# @api private
|
375
|
-
def
|
376
|
-
|
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`.
|
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
|
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
|
168
|
+
def data_for_update_columns
|
169
|
+
{}
|
400
170
|
end
|
401
171
|
|
402
172
|
# Invoked via callback when a user attempts to persist a reified
|
@@ -420,38 +190,6 @@ module PaperTrail
|
|
420
190
|
version
|
421
191
|
end
|
422
192
|
|
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
193
|
# Save, and create a version record regardless of options such as `:on`,
|
456
194
|
# `:if`, or `:unless`.
|
457
195
|
#
|
@@ -459,12 +197,6 @@ module PaperTrail
|
|
459
197
|
#
|
460
198
|
# This is an "update" event. That is, we record the same data we would in
|
461
199
|
# 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
200
|
def save_with_version(*args)
|
469
201
|
::PaperTrail.request(enabled: false) do
|
470
202
|
@record.save(*args)
|
@@ -483,9 +215,10 @@ module PaperTrail
|
|
483
215
|
# creates a version to record those changes.
|
484
216
|
# @api public
|
485
217
|
def update_columns(attributes)
|
486
|
-
# `@record.update_columns` skips dirty
|
487
|
-
# @record.saved_changes` from `ActiveModel::Dirty`.
|
488
|
-
# changes that will be made
|
218
|
+
# `@record.update_columns` skips dirty-tracking, so we can't just use
|
219
|
+
# `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
|
220
|
+
# We need to build our own hash with the changes that will be made
|
221
|
+
# directly to the database.
|
489
222
|
changes = {}
|
490
223
|
attributes.each do |k, v|
|
491
224
|
changes[k] = [@record[k], v]
|
@@ -509,92 +242,12 @@ module PaperTrail
|
|
509
242
|
versions.collect { |version| version_at(version.created_at) }
|
510
243
|
end
|
511
244
|
|
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
245
|
private
|
541
246
|
|
542
|
-
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
543
|
-
# https://github.com/paper-trail-gem/paper_trail/pull/899
|
544
|
-
#
|
545
247
|
# @api private
|
546
|
-
def
|
547
|
-
|
548
|
-
|
549
|
-
else
|
550
|
-
@record.attribute_changed?(attr_name.to_s)
|
551
|
-
end
|
552
|
-
end
|
553
|
-
|
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
|
-
# @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
|
575
|
-
|
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
|
586
|
-
end
|
587
|
-
|
588
|
-
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
589
|
-
# https://github.com/paper-trail-gem/paper_trail/pull/899
|
590
|
-
#
|
591
|
-
# @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
|
248
|
+
def assign_and_reset_version_association(version)
|
249
|
+
@record.send("#{@record.class.version_association_name}=", version)
|
250
|
+
@record.send(@record.class.versions_association_name).reset
|
598
251
|
end
|
599
252
|
|
600
253
|
def log_version_errors(version, action)
|