paper_trail 9.2.0 → 12.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/USAGE +3 -0
  4. data/lib/generators/paper_trail/{install_generator.rb → install/install_generator.rb} +15 -38
  5. data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
  6. data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +5 -3
  7. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  8. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  9. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  10. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +14 -46
  13. data/lib/paper_trail/compatibility.rb +51 -0
  14. data/lib/paper_trail/config.rb +9 -2
  15. data/lib/paper_trail/errors.rb +33 -0
  16. data/lib/paper_trail/events/base.rb +320 -0
  17. data/lib/paper_trail/events/create.rb +32 -0
  18. data/lib/paper_trail/events/destroy.rb +42 -0
  19. data/lib/paper_trail/events/update.rb +65 -0
  20. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  21. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  22. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  23. data/lib/paper_trail/frameworks/rails.rb +1 -2
  24. data/lib/paper_trail/has_paper_trail.rb +20 -17
  25. data/lib/paper_trail/model_config.rb +127 -87
  26. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  27. data/lib/paper_trail/queries/versions/where_object.rb +4 -1
  28. data/lib/paper_trail/queries/versions/where_object_changes.rb +8 -13
  29. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  30. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  31. data/lib/paper_trail/record_trail.rb +94 -411
  32. data/lib/paper_trail/reifier.rb +41 -25
  33. data/lib/paper_trail/request.rb +0 -3
  34. data/lib/paper_trail/serializers/json.rb +0 -10
  35. data/lib/paper_trail/serializers/yaml.rb +6 -13
  36. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  37. data/lib/paper_trail/version_concern.rb +142 -61
  38. data/lib/paper_trail/version_number.rb +1 -1
  39. data/lib/paper_trail.rb +18 -123
  40. metadata +147 -56
  41. data/lib/generators/paper_trail/USAGE +0 -2
  42. data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Events
5
+ # We refer to times in the lifecycle of a record as "events". There are
6
+ # three events:
7
+ #
8
+ # - create
9
+ # - `after_create` we call `RecordTrail#record_create`
10
+ # - update
11
+ # - `after_update` we call `RecordTrail#record_update`
12
+ # - `after_touch` we call `RecordTrail#record_update`
13
+ # - `RecordTrail#save_with_version` calls `RecordTrail#record_update`
14
+ # - `RecordTrail#update_columns` is also referred to as an update, though
15
+ # it uses `RecordTrail#record_update_columns` rather than
16
+ # `RecordTrail#record_update`
17
+ # - destroy
18
+ # - `before_destroy` or `after_destroy` we call `RecordTrail#record_destroy`
19
+ #
20
+ # The value inserted into the `event` column of the versions table can also
21
+ # be overridden by the user, with `paper_trail_event`.
22
+ #
23
+ # @api private
24
+ class Base
25
+ # @api private
26
+ def initialize(record, in_after_callback)
27
+ @record = record
28
+ @in_after_callback = in_after_callback
29
+ end
30
+
31
+ # Determines whether it is appropriate to generate a new version
32
+ # instance. A timestamp-only update (e.g. only `updated_at` changed) is
33
+ # considered notable unless an ignored attribute was also changed.
34
+ #
35
+ # @api private
36
+ def changed_notably?
37
+ if ignored_attr_has_changed?
38
+ timestamps = @record.send(:timestamp_attributes_for_update_in_model).map(&:to_s)
39
+ (notably_changed - timestamps).any?
40
+ else
41
+ notably_changed.any?
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
48
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
49
+ #
50
+ # @api private
51
+ def attribute_changed_in_latest_version?(attr_name)
52
+ if @in_after_callback
53
+ @record.saved_change_to_attribute?(attr_name.to_s)
54
+ else
55
+ @record.attribute_changed?(attr_name.to_s)
56
+ end
57
+ end
58
+
59
+ # @api private
60
+ def nonskipped_attributes_before_change(is_touch)
61
+ record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip])
62
+ record_attributes.each_key do |k|
63
+ if @record.class.column_names.include?(k)
64
+ record_attributes[k] = attribute_in_previous_version(k, is_touch)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
70
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
71
+ #
72
+ # Event can be any of the three (create, update, destroy).
73
+ #
74
+ # @api private
75
+ def attribute_in_previous_version(attr_name, is_touch)
76
+ if @in_after_callback && !is_touch
77
+ # For most events, we want the original value of the attribute, before
78
+ # the last save.
79
+ @record.attribute_before_last_save(attr_name.to_s)
80
+ else
81
+ # We are either performing a `record_destroy` or a
82
+ # `record_update(is_touch: true)`.
83
+ @record.attribute_in_database(attr_name.to_s)
84
+ end
85
+ end
86
+
87
+ # @api private
88
+ def calculated_ignored_array
89
+ ignore = @record.paper_trail_options[:ignore].dup
90
+ # Remove Hash arguments and then evaluate whether the attributes (the
91
+ # keys of the hash) should also get pushed into the collection.
92
+ ignore.delete_if do |obj|
93
+ obj.is_a?(Hash) &&
94
+ obj.each { |attr, condition|
95
+ ignore << attr if condition.respond_to?(:call) && condition.call(@record)
96
+ }
97
+ end
98
+ end
99
+
100
+ # @api private
101
+ def changed_and_not_ignored
102
+ skip = @record.paper_trail_options[:skip]
103
+ (changed_in_latest_version - calculated_ignored_array) - skip
104
+ end
105
+
106
+ # @api private
107
+ def changed_in_latest_version
108
+ # Memoized to reduce memory usage
109
+ @changed_in_latest_version ||= changes_in_latest_version.keys
110
+ end
111
+
112
+ # Memoized to reduce memory usage
113
+ #
114
+ # @api private
115
+ def changes_in_latest_version
116
+ @changes_in_latest_version ||= load_changes_in_latest_version
117
+ end
118
+
119
+ # @api private
120
+ def evaluate_only
121
+ only = @record.paper_trail_options[:only].dup
122
+ # Remove Hash arguments and then evaluate whether the attributes (the
123
+ # keys of the hash) should also get pushed into the collection.
124
+ only.delete_if do |obj|
125
+ obj.is_a?(Hash) &&
126
+ obj.each { |attr, condition|
127
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
128
+ }
129
+ end
130
+ only
131
+ end
132
+
133
+ # An attributed is "ignored" if it is listed in the `:ignore` option
134
+ # and/or the `:skip` option. Returns true if an ignored attribute has
135
+ # changed.
136
+ #
137
+ # @api private
138
+ def ignored_attr_has_changed?
139
+ ignored = calculated_ignored_array + @record.paper_trail_options[:skip]
140
+ ignored.any? && (changed_in_latest_version & ignored).any?
141
+ end
142
+
143
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
144
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
145
+ #
146
+ # @api private
147
+ def load_changes_in_latest_version
148
+ if @in_after_callback
149
+ @record.saved_changes
150
+ else
151
+ @record.changes
152
+ end
153
+ end
154
+
155
+ # PT 10 has a new optional column, `item_subtype`
156
+ #
157
+ # @api private
158
+ def merge_item_subtype_into(data)
159
+ if @record.class.paper_trail.version_class.columns_hash.key?("item_subtype")
160
+ data.merge!(item_subtype: @record.class.name)
161
+ end
162
+ end
163
+
164
+ # Updates `data` from the model's `meta` option and from `controller_info`.
165
+ # Metadata is always recorded; that means all three events (create, update,
166
+ # destroy) and `update_columns`.
167
+ #
168
+ # @api private
169
+ def merge_metadata_into(data)
170
+ merge_metadata_from_model_into(data)
171
+ merge_metadata_from_controller_into(data)
172
+ end
173
+
174
+ # Updates `data` from `controller_info`.
175
+ #
176
+ # @api private
177
+ def merge_metadata_from_controller_into(data)
178
+ data.merge(PaperTrail.request.controller_info || {})
179
+ end
180
+
181
+ # Updates `data` from the model's `meta` option.
182
+ #
183
+ # @api private
184
+ def merge_metadata_from_model_into(data)
185
+ @record.paper_trail_options[:meta].each do |k, v|
186
+ data[k] = model_metadatum(v, data[:event])
187
+ end
188
+ end
189
+
190
+ # Given a `value` from the model's `meta` option, returns an object to be
191
+ # persisted. The `value` can be a simple scalar value, but it can also
192
+ # be a symbol that names a model method, or even a Proc.
193
+ #
194
+ # @api private
195
+ def model_metadatum(value, event)
196
+ if value.respond_to?(:call)
197
+ value.call(@record)
198
+ elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
199
+ metadatum_from_model_method(event, value)
200
+ else
201
+ value
202
+ end
203
+ end
204
+
205
+ # The model method can either be an attribute or a non-attribute method.
206
+ #
207
+ # If it is an attribute that is changing in an existing object,
208
+ # be sure to grab the correct version.
209
+ #
210
+ # @api private
211
+ def metadatum_from_model_method(event, method)
212
+ if event != "create" &&
213
+ @record.has_attribute?(method) &&
214
+ attribute_changed_in_latest_version?(method)
215
+ attribute_in_previous_version(method, false)
216
+ else
217
+ @record.send(method)
218
+ end
219
+ end
220
+
221
+ # @api private
222
+ def notable_changes
223
+ changes_in_latest_version.delete_if { |k, _v|
224
+ !notably_changed.include?(k)
225
+ }
226
+ end
227
+
228
+ # @api private
229
+ def notably_changed
230
+ # Memoized to reduce memory usage
231
+ @notably_changed ||= begin
232
+ only = evaluate_only
233
+ cani = changed_and_not_ignored
234
+ only.empty? ? cani : (cani & only)
235
+ end
236
+ end
237
+
238
+ # Returns hash of attributes (with appropriate attributes serialized),
239
+ # omitting attributes to be skipped.
240
+ #
241
+ # @api private
242
+ def object_attrs_for_paper_trail(is_touch)
243
+ attrs = nonskipped_attributes_before_change(is_touch)
244
+ AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
245
+ attrs
246
+ end
247
+
248
+ # @api private
249
+ def prepare_object_changes(changes)
250
+ changes = serialize_object_changes(changes)
251
+ recordable_object_changes(changes)
252
+ end
253
+
254
+ # Returns an object which can be assigned to the `object_changes`
255
+ # attribute of a nascent version record. If the `object_changes` column is
256
+ # a postgres `json` column, then a hash can be used in the assignment,
257
+ # otherwise the column is a `text` column, and we must perform the
258
+ # serialization here, using `PaperTrail.serializer`.
259
+ #
260
+ # @api private
261
+ # @param changes HashWithIndifferentAccess
262
+ def recordable_object_changes(changes)
263
+ if PaperTrail.config.object_changes_adapter.respond_to?(:diff)
264
+ # We'd like to avoid the `to_hash` here, because it increases memory
265
+ # usage, but that would be a breaking change because
266
+ # `object_changes_adapter` expects a plain `Hash`, not a
267
+ # `HashWithIndifferentAccess`.
268
+ changes = PaperTrail.config.object_changes_adapter.diff(changes.to_hash)
269
+ end
270
+
271
+ if @record.class.paper_trail.version_class.object_changes_col_is_json?
272
+ changes
273
+ else
274
+ PaperTrail.serializer.dump(changes)
275
+ end
276
+ end
277
+
278
+ # Returns a boolean indicating whether to store serialized version diffs
279
+ # in the `object_changes` column of the version record.
280
+ #
281
+ # @api private
282
+ def record_object_changes?
283
+ @record.class.paper_trail.version_class.column_names.include?("object_changes")
284
+ end
285
+
286
+ # Returns a boolean indicating whether to store the original object during save.
287
+ #
288
+ # @api private
289
+ def record_object?
290
+ @record.class.paper_trail.version_class.column_names.include?("object")
291
+ end
292
+
293
+ # Returns an object which can be assigned to the `object` attribute of a
294
+ # nascent version record. If the `object` column is a postgres `json`
295
+ # column, then a hash can be used in the assignment, otherwise the column
296
+ # is a `text` column, and we must perform the serialization here, using
297
+ # `PaperTrail.serializer`.
298
+ #
299
+ # @api private
300
+ def recordable_object(is_touch)
301
+ if @record.class.paper_trail.version_class.object_col_is_json?
302
+ object_attrs_for_paper_trail(is_touch)
303
+ else
304
+ PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch))
305
+ end
306
+ end
307
+
308
+ # @api private
309
+ def serialize_object_changes(changes)
310
+ AttributeSerializers::ObjectChangesAttribute.
311
+ new(@record.class).
312
+ serialize(changes)
313
+
314
+ # We'd like to convert this `HashWithIndifferentAccess` to a plain
315
+ # `Hash`, but we don't, to save memory.
316
+ changes
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/events/base"
4
+
5
+ module PaperTrail
6
+ module Events
7
+ # See docs in `Base`.
8
+ #
9
+ # @api private
10
+ class Create < Base
11
+ # Return attributes of nascent `Version` record.
12
+ #
13
+ # @api private
14
+ def data
15
+ data = {
16
+ item: @record,
17
+ event: @record.paper_trail_event || "create",
18
+ whodunnit: PaperTrail.request.whodunnit
19
+ }
20
+ if @record.respond_to?(:updated_at)
21
+ data[:created_at] = @record.updated_at
22
+ end
23
+ if record_object_changes? && changed_notably?
24
+ changes = notable_changes
25
+ data[:object_changes] = prepare_object_changes(changes)
26
+ end
27
+ merge_item_subtype_into(data)
28
+ merge_metadata_into(data)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/events/base"
4
+
5
+ module PaperTrail
6
+ module Events
7
+ # See docs in `Base`.
8
+ #
9
+ # @api private
10
+ class Destroy < Base
11
+ # Return attributes of nascent `Version` record.
12
+ #
13
+ # @api private
14
+ def data
15
+ data = {
16
+ item_id: @record.id,
17
+ item_type: @record.class.base_class.name,
18
+ event: @record.paper_trail_event || "destroy",
19
+ whodunnit: PaperTrail.request.whodunnit
20
+ }
21
+ if record_object?
22
+ data[:object] = recordable_object(false)
23
+ end
24
+ if record_object_changes?
25
+ data[:object_changes] = prepare_object_changes(notable_changes)
26
+ end
27
+ merge_item_subtype_into(data)
28
+ merge_metadata_into(data)
29
+ end
30
+
31
+ private
32
+
33
+ # Rails' implementation (eg. `@record.saved_changes`) returns nothing on
34
+ # destroy, so we have to build the hash we want.
35
+ #
36
+ # @override
37
+ def changes_in_latest_version
38
+ @record.attributes.transform_values { |value| [value, nil] }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/events/base"
4
+
5
+ module PaperTrail
6
+ module Events
7
+ # See docs in `Base`.
8
+ #
9
+ # @api private
10
+ class Update < Base
11
+ # - is_touch - [boolean] - Used in the two situations that are touch-like:
12
+ # - `after_touch` we call `RecordTrail#record_update`
13
+ # - force_changes - [Hash] - Only used by `RecordTrail#update_columns`,
14
+ # because there dirty-tracking is off, so it has to track its own changes.
15
+ #
16
+ # @api private
17
+ def initialize(record, in_after_callback, is_touch, force_changes)
18
+ super(record, in_after_callback)
19
+ @is_touch = is_touch
20
+ @force_changes = force_changes
21
+ end
22
+
23
+ # Return attributes of nascent `Version` record.
24
+ #
25
+ # @api private
26
+ def data
27
+ data = {
28
+ item: @record,
29
+ event: @record.paper_trail_event || "update",
30
+ whodunnit: PaperTrail.request.whodunnit
31
+ }
32
+ if @record.respond_to?(:updated_at)
33
+ data[:created_at] = @record.updated_at
34
+ end
35
+ if record_object?
36
+ data[:object] = recordable_object(@is_touch)
37
+ end
38
+ merge_object_changes_into(data)
39
+ merge_item_subtype_into(data)
40
+ merge_metadata_into(data)
41
+ end
42
+
43
+ private
44
+
45
+ # @api private
46
+ def merge_object_changes_into(data)
47
+ if record_object_changes?
48
+ changes = @force_changes.nil? ? notable_changes : @force_changes
49
+ data[:object_changes] = prepare_object_changes(changes)
50
+ end
51
+ end
52
+
53
+ # `touch` cannot record `object_changes` because rails' `touch` does not
54
+ # perform dirty-tracking. Specifically, methods from `Dirty`, like
55
+ # `saved_changes`, return the same values before and after `touch`.
56
+ #
57
+ # See https://github.com/rails/rails/issues/33429
58
+ #
59
+ # @api private
60
+ def record_object_changes?
61
+ !@is_touch && super
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This file only needs to be loaded if the gem is being used outside of Rails,
4
- # since otherwise the model(s) will get loaded in via the `Rails::Engine`.
3
+ # Either ActiveRecord has already been loaded by the Lazy Load Hook in our
4
+ # Railtie, or else we load it now.
5
+ require "active_record"
6
+ ::PaperTrail::Compatibility.check_activerecord(::ActiveRecord.gem_version)
7
+
8
+ # Now we can load the parts of PT that depend on AR.
9
+ require "paper_trail/has_paper_trail"
10
+ require "paper_trail/reifier"
5
11
  require "paper_trail/frameworks/active_record/models/paper_trail/version"
12
+ ActiveRecord::Base.include PaperTrail::Model
@@ -25,9 +25,7 @@ module PaperTrail
25
25
  # @api public
26
26
  def user_for_paper_trail
27
27
  return unless defined?(current_user)
28
- ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
29
- rescue NoMethodError
30
- current_user
28
+ current_user.try(:id) || current_user
31
29
  end
32
30
 
33
31
  # Returns any information about the controller or request that you
@@ -103,9 +101,3 @@ module PaperTrail
103
101
  end
104
102
  end
105
103
  end
106
-
107
- if defined?(::ActionController)
108
- ::ActiveSupport.on_load(:action_controller) do
109
- include ::PaperTrail::Rails::Controller
110
- end
111
- end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Represents code to load within Rails framework. See documentation in
5
+ # `railties/lib/rails/railtie.rb`.
6
+ # @api private
7
+ class Railtie < ::Rails::Railtie
8
+ # PaperTrail only has one initializer.
9
+ #
10
+ # We specify `before: "load_config_initializers"` to ensure that the PT
11
+ # initializer happens before "app initializers" (those defined in
12
+ # the app's `config/initalizers`).
13
+ initializer "paper_trail", before: "load_config_initializers" do
14
+ # `on_load` is a "lazy load hook". It "declares a block that will be
15
+ # executed when a Rails component is fully loaded". (See
16
+ # `active_support/lazy_load_hooks.rb`)
17
+ ActiveSupport.on_load(:action_controller) do
18
+ require "paper_trail/frameworks/rails/controller"
19
+
20
+ # Mix our extensions into `ActionController::Base`, which is `self`
21
+ # because of the `class_eval` in `lazy_load_hooks.rb`.
22
+ include PaperTrail::Rails::Controller
23
+ end
24
+
25
+ ActiveSupport.on_load(:active_record) do
26
+ require "paper_trail/frameworks/active_record"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "paper_trail/frameworks/rails/controller"
4
- require "paper_trail/frameworks/rails/engine"
3
+ require "paper_trail/frameworks/rails/railtie"
@@ -12,7 +12,7 @@ module PaperTrail
12
12
  # `.paper_trail` and `#paper_trail`.
13
13
  module Model
14
14
  def self.included(base)
15
- base.send :extend, ClassMethods
15
+ base.extend ClassMethods
16
16
  end
17
17
 
18
18
  # :nodoc:
@@ -23,18 +23,18 @@ module PaperTrail
23
23
  # Options:
24
24
  #
25
25
  # - :on - The events to track (optional; defaults to all of them). Set
26
- # to an array of `:create`, `:update`, `:destroy` as desired.
27
- # - :class_name - The name of a custom Version class. This class should
28
- # inherit from `PaperTrail::Version`.
26
+ # to an array of `:create`, `:update`, `:destroy` and `:touch` as desired.
27
+ # - :class_name (deprecated) - The name of a custom Version class that
28
+ # includes `PaperTrail::VersionConcern`.
29
29
  # - :ignore - An array of attributes for which a new `Version` will not be
30
- # created if only they change. It can also aceept a Hash as an
30
+ # created if only they change. It can also accept a Hash as an
31
31
  # argument where the key is the attribute to ignore (a `String` or
32
32
  # `Symbol`), which will only be ignored if the value is a `Proc` which
33
33
  # returns truthily.
34
34
  # - :if, :unless - Procs that allow to specify conditions when to save
35
35
  # versions for an object.
36
36
  # - :only - Inverse of `ignore`. A new `Version` will be created only
37
- # for these attributes if supplied it can also aceept a Hash as an
37
+ # for these attributes if supplied it can also accept a Hash as an
38
38
  # argument where the key is the attribute to track (a `String` or
39
39
  # `Symbol`), which will only be counted if the value is a `Proc` which
40
40
  # returns truthily.
@@ -47,22 +47,25 @@ module PaperTrail
47
47
  # are called with `self`, i.e. the model with the paper trail). See
48
48
  # `PaperTrail::Controller.info_for_paper_trail` for how to store data
49
49
  # from the controller.
50
- # - :versions - The name to use for the versions association. Default
51
- # is `:versions`.
50
+ # - :versions - Either,
51
+ # - A String (deprecated) - The name to use for the versions
52
+ # association. Default is `:versions`.
53
+ # - A Hash - options passed to `has_many`, plus `name:` and `scope:`.
52
54
  # - :version - The name to use for the method which returns the version
53
55
  # the instance was reified from. Default is `:version`.
54
- # - :save_changes - Whether or not to save changes to the object_changes
55
- # column if it exists. Default is true
56
- # - :join_tables - If the model has a has_and_belongs_to_many relation
57
- # with an unpapertrailed model, passing the name of the association to
58
- # the join_tables option will paper trail the join table but not save
59
- # the other model, allowing reification of the association but with the
60
- # other models latest state (if the other model is paper trailed, this
61
- # option does nothing)
56
+ #
57
+ # Plugins like the experimental `paper_trail-association_tracking` gem
58
+ # may accept additional options.
59
+ #
60
+ # You can define a default set of options via the configurable
61
+ # `PaperTrail.config.has_paper_trail_defaults` hash in your applications
62
+ # initializer. The hash can contain any of the following options and will
63
+ # provide an overridable default for all models.
62
64
  #
63
65
  # @api public
64
66
  def has_paper_trail(options = {})
65
- paper_trail.setup(options)
67
+ defaults = PaperTrail.config.has_paper_trail_defaults
68
+ paper_trail.setup(defaults.merge(options))
66
69
  end
67
70
 
68
71
  # @api public