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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/paper_trail/install_generator.rb +2 -0
  3. data/lib/paper_trail.rb +130 -78
  4. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +3 -1
  5. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +2 -0
  6. data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
  7. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
  8. data/lib/paper_trail/cleaner.rb +2 -0
  9. data/lib/paper_trail/config.rb +33 -8
  10. data/lib/paper_trail/frameworks/active_record.rb +2 -0
  11. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +2 -0
  12. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +2 -0
  13. data/lib/paper_trail/frameworks/cucumber.rb +5 -3
  14. data/lib/paper_trail/frameworks/rails.rb +2 -0
  15. data/lib/paper_trail/frameworks/rails/controller.rb +28 -16
  16. data/lib/paper_trail/frameworks/rails/engine.rb +2 -0
  17. data/lib/paper_trail/frameworks/rspec.rb +5 -3
  18. data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
  19. data/lib/paper_trail/has_paper_trail.rb +2 -1
  20. data/lib/paper_trail/model_config.rb +76 -14
  21. data/lib/paper_trail/queries/versions/where_object.rb +2 -0
  22. data/lib/paper_trail/queries/versions/where_object_changes.rb +3 -1
  23. data/lib/paper_trail/record_history.rb +2 -0
  24. data/lib/paper_trail/record_trail.rb +188 -48
  25. data/lib/paper_trail/reifier.rb +4 -2
  26. data/lib/paper_trail/reifiers/belongs_to.rb +2 -0
  27. data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +2 -0
  28. data/lib/paper_trail/reifiers/has_many.rb +2 -0
  29. data/lib/paper_trail/reifiers/has_many_through.rb +2 -0
  30. data/lib/paper_trail/reifiers/has_one.rb +52 -4
  31. data/lib/paper_trail/request.rb +183 -0
  32. data/lib/paper_trail/serializers/json.rb +2 -2
  33. data/lib/paper_trail/serializers/yaml.rb +10 -14
  34. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +2 -0
  35. data/lib/paper_trail/version_association_concern.rb +1 -1
  36. data/lib/paper_trail/version_concern.rb +2 -6
  37. data/lib/paper_trail/version_number.rb +5 -3
  38. metadata +8 -21
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/frameworks/rails/controller"
2
4
  require "paper_trail/frameworks/rails/engine"
@@ -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, `enabled_for_controller`.
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.enabled_for_controller = paper_trail_enabled_for_controller
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.enabled_for_controller?
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.enabled_for_controller?
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
  module PaperTrail
2
4
  module Rails
3
5
  # See http://guides.rubyonrails.org/engines.html
@@ -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.enabled_for_controller = true
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module RSpec
3
5
  module Helpers
@@ -1,4 +1,5 @@
1
- require "active_support/core_ext/object" # provides the `try` method
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
- require "active_support/core_ext"
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 and has no effect. Please use :before
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
- # Switches PaperTrail off for this class.
47
+ # @deprecated
18
48
  def disable
19
- ::PaperTrail.enabled_for_model(@model_class, false)
49
+ ::ActiveSupport::Deprecation.warn(DPR_DISABLE, caller(1))
50
+ ::PaperTrail.request.disable_model(@model_class)
20
51
  end
21
52
 
22
- # Switches PaperTrail on for this class.
53
+ # @deprecated
23
54
  def enable
24
- ::PaperTrail.enabled_for_model(@model_class, true)
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
- return false unless @model_class.include?(::PaperTrail::Model::InstanceMethods)
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
- ::ActiveSupport::Deprecation.warn(E_CANNOT_RECORD_AFTER_DESTROY)
85
+ raise E_CANNOT_RECORD_AFTER_DESTROY
49
86
  end
50
87
 
51
88
  @model_class.send(
52
89
  "#{recording_order}_destroy",
53
- ->(r) { r.paper_trail.record_destroy if r.paper_trail.save_version? }
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
- r.paper_trail.record_update(nil) if r.paper_trail.save_version?
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  module Queries
3
5
  module Versions
@@ -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 `where_object` in
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PaperTrail
2
4
  # Represents the history of a single record.
3
5
  # @api private
@@ -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? && PaperTrail.enabled_for_controller? && enabled_for_model?
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
- @record.class.paper_trail.enabled?
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
- def record_destroy
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 = true
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
- # Returns data for record update
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`, but also
386
- # creates a version. A version is created regardless of options such as
387
- # `:on`, `:if`, or `:unless`.
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
- # TODO: look into leveraging the `after_touch` callback from
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
- record_update(true) unless will_record_after_update?
404
- @record.save!(validate: false)
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
- paper_trail_was_enabled = enabled_for_model?
425
- @record.class.paper_trail.disable
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.paper_trail.enable if paper_trail_was_enabled
564
+ PaperTrail.request.enable_model(@record.class) if paper_trail_was_enabled
437
565
  end
438
566
 
439
- # Temporarily overwrites the value of whodunnit and then executes the
440
- # provided block.
567
+ # @deprecated
441
568
  def whodunnit(value)
442
569
  raise ArgumentError, "expected to receive a block" unless block_given?
443
- current_whodunnit = PaperTrail.whodunnit
444
- PaperTrail.whodunnit = value
445
- yield @record
446
- ensure
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 @in_after_callback && RAILS_GTE_5_1
469
- @record.attribute_before_last_save(attr_name.to_s)
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 && version.logger.warn(
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.paper_trail.enabled?
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.paper_trail.enabled?
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.paper_trail.enabled?
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