paper_trail 9.2.0 → 12.2.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 (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