paper_trail 8.1.2 → 9.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_generator.rb +2 -0
- data/lib/paper_trail.rb +130 -78
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +3 -1
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +2 -0
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
- data/lib/paper_trail/cleaner.rb +2 -0
- data/lib/paper_trail/config.rb +33 -8
- data/lib/paper_trail/frameworks/active_record.rb +2 -0
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +2 -0
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +2 -0
- data/lib/paper_trail/frameworks/cucumber.rb +5 -3
- data/lib/paper_trail/frameworks/rails.rb +2 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +28 -16
- data/lib/paper_trail/frameworks/rails/engine.rb +2 -0
- data/lib/paper_trail/frameworks/rspec.rb +5 -3
- data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
- data/lib/paper_trail/has_paper_trail.rb +2 -1
- data/lib/paper_trail/model_config.rb +76 -14
- data/lib/paper_trail/queries/versions/where_object.rb +2 -0
- data/lib/paper_trail/queries/versions/where_object_changes.rb +3 -1
- data/lib/paper_trail/record_history.rb +2 -0
- data/lib/paper_trail/record_trail.rb +188 -48
- data/lib/paper_trail/reifier.rb +4 -2
- data/lib/paper_trail/reifiers/belongs_to.rb +2 -0
- data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +2 -0
- data/lib/paper_trail/reifiers/has_many.rb +2 -0
- data/lib/paper_trail/reifiers/has_many_through.rb +2 -0
- data/lib/paper_trail/reifiers/has_one.rb +52 -4
- data/lib/paper_trail/request.rb +183 -0
- data/lib/paper_trail/serializers/json.rb +2 -2
- data/lib/paper_trail/serializers/yaml.rb +10 -14
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +2 -0
- data/lib/paper_trail/version_association_concern.rb +1 -1
- data/lib/paper_trail/version_concern.rb +2 -6
- data/lib/paper_trail/version_number.rb +5 -3
- metadata +8 -21
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
module Rails
|
3
5
|
# Extensions to rails controllers. Provides convenient ways to pass certain
|
4
6
|
# information to the model layer, with `controller_info` and `whodunnit`.
|
5
|
-
# Also includes a convenient on/off switch,
|
7
|
+
# Also includes a convenient on/off switch,
|
8
|
+
# `paper_trail_enabled_for_controller`.
|
6
9
|
module Controller
|
7
10
|
def self.included(controller)
|
8
11
|
controller.before_action(
|
@@ -18,6 +21,8 @@ module PaperTrail
|
|
18
21
|
#
|
19
22
|
# Override this method in your controller to call a different
|
20
23
|
# method, e.g. `current_person`, or anything you like.
|
24
|
+
#
|
25
|
+
# @api public
|
21
26
|
def user_for_paper_trail
|
22
27
|
return unless defined?(current_user)
|
23
28
|
ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
|
@@ -43,6 +48,8 @@ module PaperTrail
|
|
43
48
|
# Use the `:meta` option to
|
44
49
|
# `PaperTrail::Model::ClassMethods.has_paper_trail` to store any extra
|
45
50
|
# model-level data you need.
|
51
|
+
#
|
52
|
+
# @api public
|
46
53
|
def info_for_paper_trail
|
47
54
|
{}
|
48
55
|
end
|
@@ -52,6 +59,15 @@ module PaperTrail
|
|
52
59
|
#
|
53
60
|
# Override this method in your controller to specify when PaperTrail
|
54
61
|
# should be off.
|
62
|
+
#
|
63
|
+
# ```
|
64
|
+
# def paper_trail_enabled_for_controller
|
65
|
+
# # Don't omit `super` without a good reason.
|
66
|
+
# super && request.user_agent != 'Disable User-Agent'
|
67
|
+
# end
|
68
|
+
# ```
|
69
|
+
#
|
70
|
+
# @api public
|
55
71
|
def paper_trail_enabled_for_controller
|
56
72
|
::PaperTrail.enabled?
|
57
73
|
end
|
@@ -60,34 +76,30 @@ module PaperTrail
|
|
60
76
|
|
61
77
|
# Tells PaperTrail whether versions should be saved in the current
|
62
78
|
# request.
|
79
|
+
#
|
80
|
+
# @api public
|
63
81
|
def set_paper_trail_enabled_for_controller
|
64
|
-
::PaperTrail.
|
82
|
+
::PaperTrail.request.enabled = paper_trail_enabled_for_controller
|
65
83
|
end
|
66
84
|
|
67
85
|
# Tells PaperTrail who is responsible for any changes that occur.
|
86
|
+
#
|
87
|
+
# @api public
|
68
88
|
def set_paper_trail_whodunnit
|
69
|
-
if ::PaperTrail.
|
70
|
-
::PaperTrail.whodunnit = user_for_paper_trail
|
89
|
+
if ::PaperTrail.request.enabled?
|
90
|
+
::PaperTrail.request.whodunnit = user_for_paper_trail
|
71
91
|
end
|
72
92
|
end
|
73
93
|
|
74
94
|
# Tells PaperTrail any information from the controller you want to store
|
75
95
|
# alongside any changes that occur.
|
96
|
+
#
|
97
|
+
# @api public
|
76
98
|
def set_paper_trail_controller_info
|
77
|
-
if ::PaperTrail.
|
78
|
-
::PaperTrail.controller_info = info_for_paper_trail
|
99
|
+
if ::PaperTrail.request.enabled?
|
100
|
+
::PaperTrail.request.controller_info = info_for_paper_trail
|
79
101
|
end
|
80
102
|
end
|
81
|
-
|
82
|
-
# We have removed this warning. We no longer add it as a callback.
|
83
|
-
# However, some people use `skip_after_action :warn_about_not_setting_whodunnit`,
|
84
|
-
# so removing this method would be a breaking change. We can remove it
|
85
|
-
# in the next major version.
|
86
|
-
def warn_about_not_setting_whodunnit
|
87
|
-
::ActiveSupport::Deprecation.warn(
|
88
|
-
"warn_about_not_setting_whodunnit is a no-op and is deprecated."
|
89
|
-
)
|
90
|
-
end
|
91
103
|
end
|
92
104
|
end
|
93
105
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "rspec/core"
|
2
4
|
require "rspec/matchers"
|
3
5
|
require "paper_trail/frameworks/rspec/helpers"
|
@@ -8,9 +10,9 @@ RSpec.configure do |config|
|
|
8
10
|
|
9
11
|
config.before(:each) do
|
10
12
|
::PaperTrail.enabled = false
|
11
|
-
::PaperTrail.
|
12
|
-
::PaperTrail.whodunnit = nil
|
13
|
-
::PaperTrail.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
|
13
|
+
::PaperTrail.request.enabled = true
|
14
|
+
::PaperTrail.request.whodunnit = nil
|
15
|
+
::PaperTrail.request.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
|
14
16
|
end
|
15
17
|
|
16
18
|
config.before(:each, versioning: true) do
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "paper_trail/attribute_serializers/object_attribute"
|
3
4
|
require "paper_trail/attribute_serializers/object_changes_attribute"
|
4
5
|
require "paper_trail/model_config"
|
@@ -1,35 +1,70 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
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
|
7
28
|
E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
|
8
29
|
paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
|
9
|
-
belongs_to_required_by_default
|
30
|
+
belongs_to_required_by_default. Use on_destroy(:before)
|
10
31
|
or disable belongs_to_required_by_default.
|
11
32
|
STR
|
33
|
+
E_HPT_ABSTRACT_CLASS = <<~STR.squish.freeze
|
34
|
+
An application model (%s) has been configured to use PaperTrail (via
|
35
|
+
`has_paper_trail`), but the version model it has been told to use (%s) is
|
36
|
+
an `abstract_class`. This could happen when an advanced feature called
|
37
|
+
Custom Version Classes (http://bit.ly/2G4ch0G) is misconfigured. When all
|
38
|
+
version classes are custom, PaperTrail::Version is configured to be an
|
39
|
+
`abstract_class`. This is fine, but all application models must be
|
40
|
+
configured to use concrete (not abstract) version models.
|
41
|
+
STR
|
12
42
|
|
13
43
|
def initialize(model_class)
|
14
44
|
@model_class = model_class
|
15
45
|
end
|
16
46
|
|
17
|
-
#
|
47
|
+
# @deprecated
|
18
48
|
def disable
|
19
|
-
::
|
49
|
+
::ActiveSupport::Deprecation.warn(DPR_DISABLE, caller(1))
|
50
|
+
::PaperTrail.request.disable_model(@model_class)
|
20
51
|
end
|
21
52
|
|
22
|
-
#
|
53
|
+
# @deprecated
|
23
54
|
def enable
|
24
|
-
::
|
55
|
+
::ActiveSupport::Deprecation.warn(DPR_ENABLE, caller(1))
|
56
|
+
::PaperTrail.request.enable_model(@model_class)
|
25
57
|
end
|
26
58
|
|
59
|
+
# @deprecated
|
27
60
|
def enabled?
|
28
|
-
|
29
|
-
::PaperTrail.enabled_for_model?(@model_class)
|
61
|
+
::ActiveSupport::Deprecation.warn(DPR_ENABLED, caller(1))
|
62
|
+
::PaperTrail.request.enabled_for_model?(@model_class)
|
30
63
|
end
|
31
64
|
|
32
65
|
# Adds a callback that records a version after a "create" event.
|
66
|
+
#
|
67
|
+
# @api public
|
33
68
|
def on_create
|
34
69
|
@model_class.after_create { |r|
|
35
70
|
r.paper_trail.record_create if r.paper_trail.save_version?
|
@@ -39,18 +74,23 @@ module PaperTrail
|
|
39
74
|
end
|
40
75
|
|
41
76
|
# Adds a callback that records a version before or after a "destroy" event.
|
77
|
+
#
|
78
|
+
# @api public
|
42
79
|
def on_destroy(recording_order = "before")
|
43
80
|
unless %w[after before].include?(recording_order.to_s)
|
44
81
|
raise ArgumentError, 'recording order can only be "after" or "before"'
|
45
82
|
end
|
46
83
|
|
47
84
|
if recording_order.to_s == "after" && cannot_record_after_destroy?
|
48
|
-
|
85
|
+
raise E_CANNOT_RECORD_AFTER_DESTROY
|
49
86
|
end
|
50
87
|
|
51
88
|
@model_class.send(
|
52
89
|
"#{recording_order}_destroy",
|
53
|
-
|
90
|
+
lambda do |r|
|
91
|
+
return unless r.paper_trail.save_version?
|
92
|
+
r.paper_trail.record_destroy(recording_order)
|
93
|
+
end
|
54
94
|
)
|
55
95
|
|
56
96
|
return if @model_class.paper_trail_options[:on].include?(:destroy)
|
@@ -58,12 +98,16 @@ module PaperTrail
|
|
58
98
|
end
|
59
99
|
|
60
100
|
# Adds a callback that records a version after an "update" event.
|
101
|
+
#
|
102
|
+
# @api public
|
61
103
|
def on_update
|
62
104
|
@model_class.before_save { |r|
|
63
105
|
r.paper_trail.reset_timestamp_attrs_for_update_if_needed
|
64
106
|
}
|
65
107
|
@model_class.after_update { |r|
|
66
|
-
|
108
|
+
if r.paper_trail.save_version?
|
109
|
+
r.paper_trail.record_update(force: false, in_after_callback: true)
|
110
|
+
end
|
67
111
|
}
|
68
112
|
@model_class.after_update { |r|
|
69
113
|
r.paper_trail.clear_version_instance
|
@@ -72,11 +116,19 @@ module PaperTrail
|
|
72
116
|
@model_class.paper_trail_options[:on] << :update
|
73
117
|
end
|
74
118
|
|
119
|
+
# Adds a callback that records a version after a "touch" event.
|
120
|
+
# @api public
|
121
|
+
def on_touch
|
122
|
+
@model_class.after_touch { |r|
|
123
|
+
r.paper_trail.record_update(force: true, in_after_callback: true)
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
75
127
|
# Set up `@model_class` for PaperTrail. Installs callbacks, associations,
|
76
128
|
# "class attributes", instance methods, and more.
|
77
129
|
# @api private
|
78
130
|
def setup(options = {})
|
79
|
-
options[:on] ||= %i[create update destroy]
|
131
|
+
options[:on] ||= %i[create update destroy touch]
|
80
132
|
options[:on] = Array(options[:on]) # Support single symbol
|
81
133
|
@model_class.send :include, ::PaperTrail::Model::InstanceMethods
|
82
134
|
setup_options(options)
|
@@ -96,6 +148,14 @@ module PaperTrail
|
|
96
148
|
Gem::Version.new(ActiveRecord::VERSION::STRING)
|
97
149
|
end
|
98
150
|
|
151
|
+
# Raises an error if the provided class is an `abstract_class`.
|
152
|
+
# @api private
|
153
|
+
def assert_concrete_activerecord_class(class_name)
|
154
|
+
if class_name.constantize.abstract_class?
|
155
|
+
raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
99
159
|
def cannot_record_after_destroy?
|
100
160
|
Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
|
101
161
|
::ActiveRecord::Base.belongs_to_required_by_default
|
@@ -121,6 +181,8 @@ module PaperTrail
|
|
121
181
|
|
122
182
|
@model_class.send :attr_accessor, :paper_trail_event
|
123
183
|
|
184
|
+
assert_concrete_activerecord_class(@model_class.version_class_name)
|
185
|
+
|
124
186
|
@model_class.has_many(
|
125
187
|
@model_class.versions_association_name,
|
126
188
|
-> { order(model.timestamp_sort_order) },
|
@@ -175,8 +237,8 @@ module PaperTrail
|
|
175
237
|
|
176
238
|
# Reset the transaction id when the transaction is closed.
|
177
239
|
def setup_transaction_callbacks
|
178
|
-
@model_class.after_commit { PaperTrail.clear_transaction_id }
|
179
|
-
@model_class.after_rollback { PaperTrail.clear_transaction_id }
|
240
|
+
@model_class.after_commit { PaperTrail.request.clear_transaction_id }
|
241
|
+
@model_class.after_rollback { PaperTrail.request.clear_transaction_id }
|
180
242
|
@model_class.after_rollback { paper_trail.clear_rolled_back_versions }
|
181
243
|
end
|
182
244
|
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
module Queries
|
3
5
|
module Versions
|
4
|
-
# For public API documentation, see `
|
6
|
+
# For public API documentation, see `where_object_changes` in
|
5
7
|
# `paper_trail/version_concern.rb`.
|
6
8
|
# @api private
|
7
9
|
class WhereObjectChanges
|
@@ -1,6 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
# Represents the "paper trail" for a single record.
|
3
5
|
class RecordTrail
|
6
|
+
DPR_TOUCH_WITH_VERSION = <<-STR.squish.freeze
|
7
|
+
my_model_instance.paper_trail.touch_with_version is deprecated,
|
8
|
+
please use my_model_instance.touch
|
9
|
+
STR
|
10
|
+
DPR_WHODUNNIT = <<-STR.squish.freeze
|
11
|
+
my_model_instance.paper_trail.whodunnit('John') is deprecated,
|
12
|
+
please use PaperTrail.request(whodunnit: 'John')
|
13
|
+
STR
|
14
|
+
DPR_WITHOUT_VERSIONING = <<-STR
|
15
|
+
my_model_instance.paper_trail.without_versioning is deprecated, without
|
16
|
+
an exact replacement. To disable versioning for a particular model,
|
17
|
+
|
18
|
+
```
|
19
|
+
PaperTrail.request.disable_model(Banana)
|
20
|
+
# changes to Banana model do not create versions,
|
21
|
+
# but eg. changes to Kiwi model do.
|
22
|
+
PaperTrail.request.enable_model(Banana)
|
23
|
+
```
|
24
|
+
|
25
|
+
Or, you may want to disable all models,
|
26
|
+
|
27
|
+
```
|
28
|
+
PaperTrail.request.enabled = false
|
29
|
+
# no versions created
|
30
|
+
PaperTrail.request.enabled = true
|
31
|
+
|
32
|
+
# or, with a block,
|
33
|
+
PaperTrail.request(enabled: false) do
|
34
|
+
# no versions created
|
35
|
+
end
|
36
|
+
```
|
37
|
+
STR
|
38
|
+
|
4
39
|
RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
|
5
40
|
|
6
41
|
def initialize(record)
|
@@ -21,6 +56,7 @@ module PaperTrail
|
|
21
56
|
# > instead, similar to the other branch in reify_has_one.
|
22
57
|
# > -Sean Griffin (https://github.com/airblade/paper_trail/pull/899)
|
23
58
|
#
|
59
|
+
# @api private
|
24
60
|
def appear_as_new_record
|
25
61
|
@record.instance_eval {
|
26
62
|
alias :old_new_record? :new_record?
|
@@ -96,12 +132,24 @@ module PaperTrail
|
|
96
132
|
notable_changes.to_hash
|
97
133
|
end
|
98
134
|
|
135
|
+
# Is PT enabled for this particular record?
|
136
|
+
# @api private
|
99
137
|
def enabled?
|
100
|
-
PaperTrail.enabled? &&
|
138
|
+
PaperTrail.enabled? &&
|
139
|
+
PaperTrail.request.enabled? &&
|
140
|
+
PaperTrail.request.enabled_for_model?(@record.class)
|
101
141
|
end
|
102
142
|
|
143
|
+
# Not sure why, but this method was mentioned in the README in the past,
|
144
|
+
# so we need to deprecate it properly.
|
145
|
+
# @deprecated
|
103
146
|
def enabled_for_model?
|
104
|
-
|
147
|
+
::ActiveSupport::Deprecation.warn(
|
148
|
+
"MyModel#paper_trail.enabled_for_model? is deprecated, use " \
|
149
|
+
"PaperTrail.request.enabled_for_model?(MyModel) instead.",
|
150
|
+
caller(1)
|
151
|
+
)
|
152
|
+
PaperTrail.request.enabled_for_model?(@record.class)
|
105
153
|
end
|
106
154
|
|
107
155
|
# An attributed is "ignored" if it is listed in the `:ignore` option
|
@@ -119,6 +167,8 @@ module PaperTrail
|
|
119
167
|
end
|
120
168
|
|
121
169
|
# Updates `data` from the model's `meta` option and from `controller_info`.
|
170
|
+
# Metadata is always recorded; that means all three events (create, update,
|
171
|
+
# destroy) and `update_columns`.
|
122
172
|
# @api private
|
123
173
|
def merge_metadata_into(data)
|
124
174
|
merge_metadata_from_model_into(data)
|
@@ -128,7 +178,7 @@ module PaperTrail
|
|
128
178
|
# Updates `data` from `controller_info`.
|
129
179
|
# @api private
|
130
180
|
def merge_metadata_from_controller_into(data)
|
131
|
-
data.merge(PaperTrail.controller_info || {})
|
181
|
+
data.merge(PaperTrail.request.controller_info || {})
|
132
182
|
end
|
133
183
|
|
134
184
|
# Updates `data` from the model's `meta` option.
|
@@ -186,6 +236,8 @@ module PaperTrail
|
|
186
236
|
|
187
237
|
# Returns hash of attributes (with appropriate attributes serialized),
|
188
238
|
# omitting attributes to be skipped.
|
239
|
+
#
|
240
|
+
# @api private
|
189
241
|
def object_attrs_for_paper_trail
|
190
242
|
attrs = attributes_before_change.except(*@record.paper_trail_options[:skip])
|
191
243
|
AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
|
@@ -193,11 +245,15 @@ module PaperTrail
|
|
193
245
|
end
|
194
246
|
|
195
247
|
# Returns who put `@record` into its current state.
|
248
|
+
#
|
249
|
+
# @api public
|
196
250
|
def originator
|
197
251
|
(source_version || versions.last).try(:whodunnit)
|
198
252
|
end
|
199
253
|
|
200
254
|
# Returns the object (not a Version) as it was most recently.
|
255
|
+
#
|
256
|
+
# @api public
|
201
257
|
def previous_version
|
202
258
|
(source_version ? source_version.previous : versions.last).try(:reify)
|
203
259
|
end
|
@@ -218,19 +274,23 @@ module PaperTrail
|
|
218
274
|
def data_for_create
|
219
275
|
data = {
|
220
276
|
event: @record.paper_trail_event || "create",
|
221
|
-
whodunnit: PaperTrail.whodunnit
|
277
|
+
whodunnit: PaperTrail.request.whodunnit
|
222
278
|
}
|
223
279
|
if @record.respond_to?(:updated_at)
|
224
280
|
data[:created_at] = @record.updated_at
|
225
281
|
end
|
226
282
|
if record_object_changes? && changed_notably?
|
227
|
-
data[:object_changes] = recordable_object_changes
|
283
|
+
data[:object_changes] = recordable_object_changes(changes)
|
228
284
|
end
|
229
285
|
add_transaction_id_to(data)
|
230
286
|
merge_metadata_into(data)
|
231
287
|
end
|
232
288
|
|
233
|
-
|
289
|
+
# `recording_order` is "after" or "before". See ModelConfig#on_destroy.
|
290
|
+
#
|
291
|
+
# @api private
|
292
|
+
def record_destroy(recording_order)
|
293
|
+
@in_after_callback = recording_order == "after"
|
234
294
|
if enabled? && !@record.new_record?
|
235
295
|
version = @record.class.paper_trail.version_class.create(data_for_destroy)
|
236
296
|
if version.errors.any?
|
@@ -242,6 +302,8 @@ module PaperTrail
|
|
242
302
|
save_associations(version)
|
243
303
|
end
|
244
304
|
end
|
305
|
+
ensure
|
306
|
+
@in_after_callback = false
|
245
307
|
end
|
246
308
|
|
247
309
|
# Returns data for record destroy
|
@@ -252,7 +314,7 @@ module PaperTrail
|
|
252
314
|
item_type: @record.class.base_class.name,
|
253
315
|
event: @record.paper_trail_event || "destroy",
|
254
316
|
object: recordable_object,
|
255
|
-
whodunnit: PaperTrail.whodunnit
|
317
|
+
whodunnit: PaperTrail.request.whodunnit
|
256
318
|
}
|
257
319
|
add_transaction_id_to(data)
|
258
320
|
merge_metadata_into(data)
|
@@ -266,8 +328,8 @@ module PaperTrail
|
|
266
328
|
@record.class.paper_trail.version_class.column_names.include?("object_changes")
|
267
329
|
end
|
268
330
|
|
269
|
-
def record_update(force)
|
270
|
-
@in_after_callback =
|
331
|
+
def record_update(force:, in_after_callback:)
|
332
|
+
@in_after_callback = in_after_callback
|
271
333
|
if enabled? && (force || changed_notably?)
|
272
334
|
versions_assoc = @record.send(@record.class.versions_association_name)
|
273
335
|
version = versions_assoc.create(data_for_update)
|
@@ -282,19 +344,49 @@ module PaperTrail
|
|
282
344
|
@in_after_callback = false
|
283
345
|
end
|
284
346
|
|
285
|
-
#
|
347
|
+
# Used during `record_update`, returns a hash of data suitable for an AR
|
348
|
+
# `create`. That is, all the attributes of the nascent `Version` record.
|
349
|
+
#
|
286
350
|
# @api private
|
287
351
|
def data_for_update
|
288
352
|
data = {
|
289
353
|
event: @record.paper_trail_event || "update",
|
290
354
|
object: recordable_object,
|
291
|
-
whodunnit: PaperTrail.whodunnit
|
355
|
+
whodunnit: PaperTrail.request.whodunnit
|
292
356
|
}
|
293
357
|
if @record.respond_to?(:updated_at)
|
294
358
|
data[:created_at] = @record.updated_at
|
295
359
|
end
|
296
360
|
if record_object_changes?
|
297
|
-
data[:object_changes] = recordable_object_changes
|
361
|
+
data[:object_changes] = recordable_object_changes(changes)
|
362
|
+
end
|
363
|
+
add_transaction_id_to(data)
|
364
|
+
merge_metadata_into(data)
|
365
|
+
end
|
366
|
+
|
367
|
+
# @api private
|
368
|
+
def record_update_columns(changes)
|
369
|
+
return unless enabled?
|
370
|
+
versions_assoc = @record.send(@record.class.versions_association_name)
|
371
|
+
version = versions_assoc.create(data_for_update_columns(changes))
|
372
|
+
if version.errors.any?
|
373
|
+
log_version_errors(version, :update)
|
374
|
+
else
|
375
|
+
update_transaction_id(version)
|
376
|
+
save_associations(version)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# Returns data for record_update_columns
|
381
|
+
# @api private
|
382
|
+
def data_for_update_columns(changes)
|
383
|
+
data = {
|
384
|
+
event: @record.paper_trail_event || "update",
|
385
|
+
object: recordable_object,
|
386
|
+
whodunnit: PaperTrail.request.whodunnit
|
387
|
+
}
|
388
|
+
if record_object_changes?
|
389
|
+
data[:object_changes] = recordable_object_changes(changes)
|
298
390
|
end
|
299
391
|
add_transaction_id_to(data)
|
300
392
|
merge_metadata_into(data)
|
@@ -305,6 +397,7 @@ module PaperTrail
|
|
305
397
|
# column, then a hash can be used in the assignment, otherwise the column
|
306
398
|
# is a `text` column, and we must perform the serialization here, using
|
307
399
|
# `PaperTrail.serializer`.
|
400
|
+
#
|
308
401
|
# @api private
|
309
402
|
def recordable_object
|
310
403
|
if @record.class.paper_trail.version_class.object_col_is_json?
|
@@ -319,8 +412,9 @@ module PaperTrail
|
|
319
412
|
# a postgres `json` column, then a hash can be used in the assignment,
|
320
413
|
# otherwise the column is a `text` column, and we must perform the
|
321
414
|
# serialization here, using `PaperTrail.serializer`.
|
415
|
+
#
|
322
416
|
# @api private
|
323
|
-
def recordable_object_changes
|
417
|
+
def recordable_object_changes(changes)
|
324
418
|
if @record.class.paper_trail.version_class.object_changes_col_is_json?
|
325
419
|
changes
|
326
420
|
else
|
@@ -382,15 +476,23 @@ module PaperTrail
|
|
382
476
|
version
|
383
477
|
end
|
384
478
|
|
385
|
-
# Mimics the `touch` method from `ActiveRecord::Persistence
|
386
|
-
#
|
387
|
-
#
|
479
|
+
# Mimics the `touch` method from `ActiveRecord::Persistence` (without
|
480
|
+
# actually calling `touch`), but also creates a version.
|
481
|
+
#
|
482
|
+
# A version is created regardless of options such as `:on`, `:if`, or
|
483
|
+
# `:unless`.
|
484
|
+
#
|
485
|
+
# This is an "update" event. That is, we record the same data we would in
|
486
|
+
# the case of a normal AR `update`.
|
487
|
+
#
|
488
|
+
# Some advanced PT users disable all callbacks (eg. `has_paper_trail(on:
|
489
|
+
# [])`) and use only this method, giving them complete control over when
|
490
|
+
# version records are inserted. It's unclear under which specific
|
491
|
+
# circumstances this technique should be adopted.
|
388
492
|
#
|
389
|
-
#
|
390
|
-
# `ActiveRecord` to allow the regular `touch` method to generate a version
|
391
|
-
# as normal. May make sense to switch the `record_update` method to
|
392
|
-
# leverage an `after_update` callback anyways (likely for v4.0.0)
|
493
|
+
# @deprecated
|
393
494
|
def touch_with_version(name = nil)
|
495
|
+
::ActiveSupport::Deprecation.warn(DPR_TOUCH_WITH_VERSION, caller(1))
|
394
496
|
unless @record.persisted?
|
395
497
|
raise ::ActiveRecord::ActiveRecordError, "can not touch on a new record object"
|
396
498
|
end
|
@@ -400,8 +502,32 @@ module PaperTrail
|
|
400
502
|
attributes.each { |column|
|
401
503
|
@record.send(:write_attribute, column, current_time)
|
402
504
|
}
|
403
|
-
|
404
|
-
|
505
|
+
::PaperTrail.request(enabled: false) do
|
506
|
+
@record.save!(validate: false)
|
507
|
+
end
|
508
|
+
record_update(force: true, in_after_callback: false)
|
509
|
+
end
|
510
|
+
|
511
|
+
# Like the `update_column` method from `ActiveRecord::Persistence`, but also
|
512
|
+
# creates a version to record those changes.
|
513
|
+
# @api public
|
514
|
+
def update_column(name, value)
|
515
|
+
update_columns(name => value)
|
516
|
+
end
|
517
|
+
|
518
|
+
# Like the `update_columns` method from `ActiveRecord::Persistence`, but also
|
519
|
+
# creates a version to record those changes.
|
520
|
+
# @api public
|
521
|
+
def update_columns(attributes)
|
522
|
+
# `@record.update_columns` skips dirty tracking, so we can't just use `@record.changes` or
|
523
|
+
# @record.saved_changes` from `ActiveModel::Dirty`. We need to build our own hash with the
|
524
|
+
# changes that will be made directly to the database.
|
525
|
+
changes = {}
|
526
|
+
attributes.each do |k, v|
|
527
|
+
changes[k] = [@record[k], v]
|
528
|
+
end
|
529
|
+
@record.update_columns(attributes)
|
530
|
+
record_update_columns(changes)
|
405
531
|
end
|
406
532
|
|
407
533
|
# Returns the object (not a Version) as it was at the given timestamp.
|
@@ -420,9 +546,11 @@ module PaperTrail
|
|
420
546
|
end
|
421
547
|
|
422
548
|
# Executes the given method or block without creating a new version.
|
549
|
+
# @deprecated
|
423
550
|
def without_versioning(method = nil)
|
424
|
-
|
425
|
-
@record.class
|
551
|
+
::ActiveSupport::Deprecation.warn(DPR_WITHOUT_VERSIONING, caller(1))
|
552
|
+
paper_trail_was_enabled = PaperTrail.request.enabled_for_model?(@record.class)
|
553
|
+
PaperTrail.request.disable_model(@record.class)
|
426
554
|
if method
|
427
555
|
if respond_to?(method)
|
428
556
|
public_send(method)
|
@@ -433,27 +561,28 @@ module PaperTrail
|
|
433
561
|
yield @record
|
434
562
|
end
|
435
563
|
ensure
|
436
|
-
@record.class
|
564
|
+
PaperTrail.request.enable_model(@record.class) if paper_trail_was_enabled
|
437
565
|
end
|
438
566
|
|
439
|
-
#
|
440
|
-
# provided block.
|
567
|
+
# @deprecated
|
441
568
|
def whodunnit(value)
|
442
569
|
raise ArgumentError, "expected to receive a block" unless block_given?
|
443
|
-
|
444
|
-
PaperTrail.whodunnit
|
445
|
-
|
446
|
-
|
447
|
-
PaperTrail.whodunnit = current_whodunnit
|
570
|
+
::ActiveSupport::Deprecation.warn(DPR_WHODUNNIT, caller(1))
|
571
|
+
::PaperTrail.request(whodunnit: value) do
|
572
|
+
yield @record
|
573
|
+
end
|
448
574
|
end
|
449
575
|
|
450
576
|
private
|
451
577
|
|
452
578
|
def add_transaction_id_to(data)
|
453
579
|
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
|
454
|
-
data[:transaction_id] = PaperTrail.transaction_id
|
580
|
+
data[:transaction_id] = PaperTrail.request.transaction_id
|
455
581
|
end
|
456
582
|
|
583
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
584
|
+
# https://github.com/airblade/paper_trail/pull/899
|
585
|
+
#
|
457
586
|
# @api private
|
458
587
|
def attribute_changed_in_latest_version?(attr_name)
|
459
588
|
if @in_after_callback && RAILS_GTE_5_1
|
@@ -463,15 +592,29 @@ module PaperTrail
|
|
463
592
|
end
|
464
593
|
end
|
465
594
|
|
595
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
596
|
+
# https://github.com/airblade/paper_trail/pull/899
|
597
|
+
#
|
598
|
+
# Event can be any of the three (create, update, destroy).
|
599
|
+
#
|
466
600
|
# @api private
|
467
601
|
def attribute_in_previous_version(attr_name)
|
468
|
-
if
|
469
|
-
@
|
602
|
+
if RAILS_GTE_5_1
|
603
|
+
if @in_after_callback
|
604
|
+
@record.attribute_before_last_save(attr_name.to_s)
|
605
|
+
else
|
606
|
+
# We are performing a `record_destroy`. Other events,
|
607
|
+
# like `record_create`, can only be done in an after-callback.
|
608
|
+
@record.attribute_in_database(attr_name.to_s)
|
609
|
+
end
|
470
610
|
else
|
471
611
|
@record.attribute_was(attr_name.to_s)
|
472
612
|
end
|
473
613
|
end
|
474
614
|
|
615
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
616
|
+
# https://github.com/airblade/paper_trail/pull/899
|
617
|
+
#
|
475
618
|
# @api private
|
476
619
|
def changed_in_latest_version
|
477
620
|
if @in_after_callback && RAILS_GTE_5_1
|
@@ -481,6 +624,9 @@ module PaperTrail
|
|
481
624
|
end
|
482
625
|
end
|
483
626
|
|
627
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
628
|
+
# https://github.com/airblade/paper_trail/pull/899
|
629
|
+
#
|
484
630
|
# @api private
|
485
631
|
def changes_in_latest_version
|
486
632
|
if @in_after_callback && RAILS_GTE_5_1
|
@@ -491,6 +637,7 @@ module PaperTrail
|
|
491
637
|
end
|
492
638
|
|
493
639
|
# Given a HABTM association, returns an array of ids.
|
640
|
+
#
|
494
641
|
# @api private
|
495
642
|
def habtm_assoc_ids(habtm_assoc)
|
496
643
|
current = @record.send(habtm_assoc.name).to_a.map(&:id) # TODO: `pluck` would use less memory
|
@@ -500,7 +647,7 @@ module PaperTrail
|
|
500
647
|
end
|
501
648
|
|
502
649
|
def log_version_errors(version, action)
|
503
|
-
version.logger
|
650
|
+
version.logger&.warn(
|
504
651
|
"Unable to create version for #{action} of #{@record.class.name}" +
|
505
652
|
"##{@record.id}: " + version.errors.full_messages.join(", ")
|
506
653
|
)
|
@@ -516,10 +663,10 @@ module PaperTrail
|
|
516
663
|
|
517
664
|
if assoc.options[:polymorphic]
|
518
665
|
associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type)
|
519
|
-
if associated_record && associated_record.class
|
666
|
+
if associated_record && PaperTrail.request.enabled_for_model?(associated_record.class)
|
520
667
|
assoc_version_args[:foreign_key_id] = associated_record.id
|
521
668
|
end
|
522
|
-
elsif assoc.klass
|
669
|
+
elsif PaperTrail.request.enabled_for_model?(assoc.klass)
|
523
670
|
assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key)
|
524
671
|
end
|
525
672
|
|
@@ -532,20 +679,13 @@ module PaperTrail
|
|
532
679
|
# @api private
|
533
680
|
def save_habtm_association?(assoc)
|
534
681
|
@record.class.paper_trail_save_join_tables.include?(assoc.name) ||
|
535
|
-
assoc.klass
|
536
|
-
end
|
537
|
-
|
538
|
-
# Returns true if `save` will cause `record_update`
|
539
|
-
# to be called via the `after_update` callback.
|
540
|
-
def will_record_after_update?
|
541
|
-
on = @record.paper_trail_options[:on]
|
542
|
-
on.nil? || on.include?(:update)
|
682
|
+
PaperTrail.request.enabled_for_model?(assoc.klass)
|
543
683
|
end
|
544
684
|
|
545
685
|
def update_transaction_id(version)
|
546
686
|
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
|
547
|
-
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
|
548
|
-
PaperTrail.transaction_id = version.id
|
687
|
+
if PaperTrail.transaction? && PaperTrail.request.transaction_id.nil?
|
688
|
+
PaperTrail.request.transaction_id = version.id
|
549
689
|
version.transaction_id = version.id
|
550
690
|
version.save
|
551
691
|
end
|