paper_trail 8.1.2 → 9.0.0

Sign up to get free protection for your applications and to get access to all the features.
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