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.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/paper_trail/install_generator.rb +3 -1
  3. data/lib/paper_trail.rb +151 -84
  4. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +27 -0
  5. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +7 -4
  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 +30 -15
  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 -2
  20. data/lib/paper_trail/model_config.rb +77 -22
  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 +189 -55
  25. data/lib/paper_trail/reifier.rb +4 -2
  26. data/lib/paper_trail/reifiers/belongs_to.rb +3 -1
  27. data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +3 -1
  28. data/lib/paper_trail/reifiers/has_many.rb +3 -1
  29. data/lib/paper_trail/reifiers/has_many_through.rb +3 -1
  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 -5
  34. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +49 -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 +3 -1
  38. metadata +55 -81
  39. 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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/version_concern"
2
4
 
3
5
  module PaperTrail
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "paper_trail/version_association_concern"
2
4
 
3
5
  module PaperTrail
@@ -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.enabled_for_controller = true
5
- PaperTrail.whodunnit = nil
6
- PaperTrail.controller_info = {} if defined? Rails
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,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,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.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
- @set_paper_trail_whodunnit_called = true
70
- ::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
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
- ::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
77
- end
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
  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,5 +1,5 @@
1
- require "active_support/core_ext/object" # provides the `try` method
2
- require "paper_trail/attribute_serializers/legacy_active_record_shim"
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
- 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
- @model_class.before_save(on: :update) { |r|
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,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,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
@@ -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
- # ActiveRecord 4.2 deprecated `reset_column!` in favor of
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`, but also
392
- # creates a version. A version is created regardless of options such as
393
- # `: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`.
394
487
  #
395
- # TODO: look into leveraging the `after_touch` callback from
396
- # `ActiveRecord` to allow the regular `touch` method to generate a version
397
- # as normal. May make sense to switch the `record_update` method to
398
- # leverage an `after_update` callback anyways (likely for v4.0.0)
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
- record_update(true) unless will_record_after_update?
410
- @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)
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
- paper_trail_was_enabled = enabled_for_model?
431
- @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)
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.paper_trail.enable if paper_trail_was_enabled
564
+ PaperTrail.request.enable_model(@record.class) if paper_trail_was_enabled
443
565
  end
444
566
 
445
- # Temporarily overwrites the value of whodunnit and then executes the
446
- # provided block.
567
+ # @deprecated
447
568
  def whodunnit(value)
448
569
  raise ArgumentError, "expected to receive a block" unless block_given?
449
- current_whodunnit = PaperTrail.whodunnit
450
- PaperTrail.whodunnit = value
451
- yield @record
452
- ensure
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 @in_after_callback && RAILS_GTE_5_1
475
- @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
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 && version.logger.warn(
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.paper_trail.enabled?
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.paper_trail.enabled?
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.paper_trail.enabled?
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