paper_trail 8.0.0 → 9.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/generators/paper_trail/install_generator.rb +3 -1
- data/lib/paper_trail.rb +151 -84
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +7 -4
- 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 +30 -15
- 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 -2
- data/lib/paper_trail/model_config.rb +77 -22
- 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 +189 -55
- data/lib/paper_trail/reifier.rb +4 -2
- data/lib/paper_trail/reifiers/belongs_to.rb +3 -1
- data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +3 -1
- data/lib/paper_trail/reifiers/has_many.rb +3 -1
- data/lib/paper_trail/reifiers/has_many_through.rb +3 -1
- 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 -5
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +49 -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 +3 -1
- metadata +55 -81
- data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +0 -48
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file only needs to be loaded if the gem is being used outside of Rails,
|
2
4
|
# since otherwise the model(s) will get loaded in via the `Rails::Engine`.
|
3
5
|
require "paper_trail/frameworks/active_record/models/paper_trail/version_association"
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# before hook for Cucumber
|
2
4
|
Before do
|
3
5
|
PaperTrail.enabled = false
|
4
|
-
PaperTrail.
|
5
|
-
PaperTrail.whodunnit = nil
|
6
|
-
PaperTrail.controller_info = {} if defined?
|
6
|
+
PaperTrail.request.enabled = true
|
7
|
+
PaperTrail.request.whodunnit = nil
|
8
|
+
PaperTrail.request.controller_info = {} if defined?(::Rails)
|
7
9
|
end
|
8
10
|
|
9
11
|
module PaperTrail
|
@@ -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,30 +76,29 @@ 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
|
-
|
70
|
-
|
89
|
+
if ::PaperTrail.request.enabled?
|
90
|
+
::PaperTrail.request.whodunnit = user_for_paper_trail
|
91
|
+
end
|
71
92
|
end
|
72
93
|
|
73
94
|
# Tells PaperTrail any information from the controller you want to store
|
74
95
|
# alongside any changes that occur.
|
96
|
+
#
|
97
|
+
# @api public
|
75
98
|
def set_paper_trail_controller_info
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
# We have removed this warning. We no longer add it as a callback.
|
80
|
-
# However, some people use `skip_after_action :warn_about_not_setting_whodunnit`,
|
81
|
-
# so removing this method would be a breaking change. We can remove it
|
82
|
-
# in the next major version.
|
83
|
-
def warn_about_not_setting_whodunnit
|
84
|
-
::ActiveSupport::Deprecation.warn(
|
85
|
-
"warn_about_not_setting_whodunnit is a no-op and is deprecated."
|
86
|
-
)
|
99
|
+
if ::PaperTrail.request.enabled?
|
100
|
+
::PaperTrail.request.controller_info = info_for_paper_trail
|
101
|
+
end
|
87
102
|
end
|
88
103
|
end
|
89
104
|
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,5 +1,5 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
3
|
require "paper_trail/attribute_serializers/object_attribute"
|
4
4
|
require "paper_trail/attribute_serializers/object_changes_attribute"
|
5
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
|
-
@model_class.before_save
|
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,20 +116,21 @@ 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
|
-
if ::ActiveRecord::VERSION::STRING < "4.2"
|
83
|
-
::ActiveSupport::Deprecation.warn(
|
84
|
-
"Your version of ActiveRecord (< 4.2) has reached EOL. PaperTrail " \
|
85
|
-
"will soon drop support. Please upgrade ActiveRecord ASAP."
|
86
|
-
)
|
87
|
-
@model_class.send :extend, AttributeSerializers::LegacyActiveRecordShim
|
88
|
-
end
|
89
134
|
setup_options(options)
|
90
135
|
setup_associations(options)
|
91
136
|
setup_transaction_callbacks
|
@@ -103,6 +148,14 @@ module PaperTrail
|
|
103
148
|
Gem::Version.new(ActiveRecord::VERSION::STRING)
|
104
149
|
end
|
105
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
|
+
|
106
159
|
def cannot_record_after_destroy?
|
107
160
|
Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
|
108
161
|
::ActiveRecord::Base.belongs_to_required_by_default
|
@@ -128,6 +181,8 @@ module PaperTrail
|
|
128
181
|
|
129
182
|
@model_class.send :attr_accessor, :paper_trail_event
|
130
183
|
|
184
|
+
assert_concrete_activerecord_class(@model_class.version_class_name)
|
185
|
+
|
131
186
|
@model_class.has_many(
|
132
187
|
@model_class.versions_association_name,
|
133
188
|
-> { order(model.timestamp_sort_order) },
|
@@ -182,8 +237,8 @@ module PaperTrail
|
|
182
237
|
|
183
238
|
# Reset the transaction id when the transaction is closed.
|
184
239
|
def setup_transaction_callbacks
|
185
|
-
@model_class.after_commit { PaperTrail.clear_transaction_id }
|
186
|
-
@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 }
|
187
242
|
@model_class.after_rollback { paper_trail.clear_rolled_back_versions }
|
188
243
|
end
|
189
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
|
@@ -333,13 +427,7 @@ module PaperTrail
|
|
333
427
|
def reset_timestamp_attrs_for_update_if_needed
|
334
428
|
return if live?
|
335
429
|
@record.send(:timestamp_attributes_for_update_in_model).each do |column|
|
336
|
-
#
|
337
|
-
# `restore_column!`.
|
338
|
-
if @record.respond_to?("restore_#{column}!")
|
339
|
-
@record.send("restore_#{column}!")
|
340
|
-
else
|
341
|
-
@record.send("reset_#{column}!")
|
342
|
-
end
|
430
|
+
@record.send("restore_#{column}!")
|
343
431
|
end
|
344
432
|
end
|
345
433
|
|
@@ -388,15 +476,23 @@ module PaperTrail
|
|
388
476
|
version
|
389
477
|
end
|
390
478
|
|
391
|
-
# Mimics the `touch` method from `ActiveRecord::Persistence
|
392
|
-
#
|
393
|
-
#
|
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`.
|
394
487
|
#
|
395
|
-
#
|
396
|
-
# `
|
397
|
-
#
|
398
|
-
#
|
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.
|
492
|
+
#
|
493
|
+
# @deprecated
|
399
494
|
def touch_with_version(name = nil)
|
495
|
+
::ActiveSupport::Deprecation.warn(DPR_TOUCH_WITH_VERSION, caller(1))
|
400
496
|
unless @record.persisted?
|
401
497
|
raise ::ActiveRecord::ActiveRecordError, "can not touch on a new record object"
|
402
498
|
end
|
@@ -406,8 +502,32 @@ module PaperTrail
|
|
406
502
|
attributes.each { |column|
|
407
503
|
@record.send(:write_attribute, column, current_time)
|
408
504
|
}
|
409
|
-
|
410
|
-
|
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)
|
411
531
|
end
|
412
532
|
|
413
533
|
# Returns the object (not a Version) as it was at the given timestamp.
|
@@ -426,9 +546,11 @@ module PaperTrail
|
|
426
546
|
end
|
427
547
|
|
428
548
|
# Executes the given method or block without creating a new version.
|
549
|
+
# @deprecated
|
429
550
|
def without_versioning(method = nil)
|
430
|
-
|
431
|
-
@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)
|
432
554
|
if method
|
433
555
|
if respond_to?(method)
|
434
556
|
public_send(method)
|
@@ -439,27 +561,28 @@ module PaperTrail
|
|
439
561
|
yield @record
|
440
562
|
end
|
441
563
|
ensure
|
442
|
-
@record.class
|
564
|
+
PaperTrail.request.enable_model(@record.class) if paper_trail_was_enabled
|
443
565
|
end
|
444
566
|
|
445
|
-
#
|
446
|
-
# provided block.
|
567
|
+
# @deprecated
|
447
568
|
def whodunnit(value)
|
448
569
|
raise ArgumentError, "expected to receive a block" unless block_given?
|
449
|
-
|
450
|
-
PaperTrail.whodunnit
|
451
|
-
|
452
|
-
|
453
|
-
PaperTrail.whodunnit = current_whodunnit
|
570
|
+
::ActiveSupport::Deprecation.warn(DPR_WHODUNNIT, caller(1))
|
571
|
+
::PaperTrail.request(whodunnit: value) do
|
572
|
+
yield @record
|
573
|
+
end
|
454
574
|
end
|
455
575
|
|
456
576
|
private
|
457
577
|
|
458
578
|
def add_transaction_id_to(data)
|
459
579
|
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
|
460
|
-
data[:transaction_id] = PaperTrail.transaction_id
|
580
|
+
data[:transaction_id] = PaperTrail.request.transaction_id
|
461
581
|
end
|
462
582
|
|
583
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
584
|
+
# https://github.com/airblade/paper_trail/pull/899
|
585
|
+
#
|
463
586
|
# @api private
|
464
587
|
def attribute_changed_in_latest_version?(attr_name)
|
465
588
|
if @in_after_callback && RAILS_GTE_5_1
|
@@ -469,15 +592,29 @@ module PaperTrail
|
|
469
592
|
end
|
470
593
|
end
|
471
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
|
+
#
|
472
600
|
# @api private
|
473
601
|
def attribute_in_previous_version(attr_name)
|
474
|
-
if
|
475
|
-
@
|
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
|
476
610
|
else
|
477
611
|
@record.attribute_was(attr_name.to_s)
|
478
612
|
end
|
479
613
|
end
|
480
614
|
|
615
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
616
|
+
# https://github.com/airblade/paper_trail/pull/899
|
617
|
+
#
|
481
618
|
# @api private
|
482
619
|
def changed_in_latest_version
|
483
620
|
if @in_after_callback && RAILS_GTE_5_1
|
@@ -487,6 +624,9 @@ module PaperTrail
|
|
487
624
|
end
|
488
625
|
end
|
489
626
|
|
627
|
+
# Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
|
628
|
+
# https://github.com/airblade/paper_trail/pull/899
|
629
|
+
#
|
490
630
|
# @api private
|
491
631
|
def changes_in_latest_version
|
492
632
|
if @in_after_callback && RAILS_GTE_5_1
|
@@ -497,6 +637,7 @@ module PaperTrail
|
|
497
637
|
end
|
498
638
|
|
499
639
|
# Given a HABTM association, returns an array of ids.
|
640
|
+
#
|
500
641
|
# @api private
|
501
642
|
def habtm_assoc_ids(habtm_assoc)
|
502
643
|
current = @record.send(habtm_assoc.name).to_a.map(&:id) # TODO: `pluck` would use less memory
|
@@ -506,7 +647,7 @@ module PaperTrail
|
|
506
647
|
end
|
507
648
|
|
508
649
|
def log_version_errors(version, action)
|
509
|
-
version.logger
|
650
|
+
version.logger&.warn(
|
510
651
|
"Unable to create version for #{action} of #{@record.class.name}" +
|
511
652
|
"##{@record.id}: " + version.errors.full_messages.join(", ")
|
512
653
|
)
|
@@ -522,10 +663,10 @@ module PaperTrail
|
|
522
663
|
|
523
664
|
if assoc.options[:polymorphic]
|
524
665
|
associated_record = @record.send(assoc.name) if @record.send(assoc.foreign_type)
|
525
|
-
if associated_record && associated_record.class
|
666
|
+
if associated_record && PaperTrail.request.enabled_for_model?(associated_record.class)
|
526
667
|
assoc_version_args[:foreign_key_id] = associated_record.id
|
527
668
|
end
|
528
|
-
elsif assoc.klass
|
669
|
+
elsif PaperTrail.request.enabled_for_model?(assoc.klass)
|
529
670
|
assoc_version_args[:foreign_key_id] = @record.send(assoc.foreign_key)
|
530
671
|
end
|
531
672
|
|
@@ -538,20 +679,13 @@ module PaperTrail
|
|
538
679
|
# @api private
|
539
680
|
def save_habtm_association?(assoc)
|
540
681
|
@record.class.paper_trail_save_join_tables.include?(assoc.name) ||
|
541
|
-
assoc.klass
|
542
|
-
end
|
543
|
-
|
544
|
-
# Returns true if `save` will cause `record_update`
|
545
|
-
# to be called via the `after_update` callback.
|
546
|
-
def will_record_after_update?
|
547
|
-
on = @record.paper_trail_options[:on]
|
548
|
-
on.nil? || on.include?(:update)
|
682
|
+
PaperTrail.request.enabled_for_model?(assoc.klass)
|
549
683
|
end
|
550
684
|
|
551
685
|
def update_transaction_id(version)
|
552
686
|
return unless @record.class.paper_trail.version_class.column_names.include?("transaction_id")
|
553
|
-
if PaperTrail.transaction? && PaperTrail.transaction_id.nil?
|
554
|
-
PaperTrail.transaction_id = version.id
|
687
|
+
if PaperTrail.transaction? && PaperTrail.request.transaction_id.nil?
|
688
|
+
PaperTrail.request.transaction_id = version.id
|
555
689
|
version.transaction_id = version.id
|
556
690
|
version.save
|
557
691
|
end
|