paper_trail 9.2.0 → 12.1.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 +2 -2
  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 +8 -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 +305 -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 +60 -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 +103 -71
  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 +5 -12
  36. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  37. data/lib/paper_trail/version_concern.rb +141 -61
  38. data/lib/paper_trail/version_number.rb +2 -2
  39. data/lib/paper_trail.rb +16 -123
  40. metadata +159 -56
  41. data/lib/generators/paper_trail/USAGE +0 -2
  42. data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
@@ -0,0 +1,305 @@
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
+ # An attributed is "ignored" if it is listed in the `:ignore` option
120
+ # and/or the `:skip` option. Returns true if an ignored attribute has
121
+ # changed.
122
+ #
123
+ # @api private
124
+ def ignored_attr_has_changed?
125
+ ignored = calculated_ignored_array + @record.paper_trail_options[:skip]
126
+ ignored.any? && (changed_in_latest_version & ignored).any?
127
+ end
128
+
129
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
130
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
131
+ #
132
+ # @api private
133
+ def load_changes_in_latest_version
134
+ if @in_after_callback
135
+ @record.saved_changes
136
+ else
137
+ @record.changes
138
+ end
139
+ end
140
+
141
+ # PT 10 has a new optional column, `item_subtype`
142
+ #
143
+ # @api private
144
+ def merge_item_subtype_into(data)
145
+ if @record.class.paper_trail.version_class.columns_hash.key?("item_subtype")
146
+ data.merge!(item_subtype: @record.class.name)
147
+ end
148
+ end
149
+
150
+ # Updates `data` from the model's `meta` option and from `controller_info`.
151
+ # Metadata is always recorded; that means all three events (create, update,
152
+ # destroy) and `update_columns`.
153
+ #
154
+ # @api private
155
+ def merge_metadata_into(data)
156
+ merge_metadata_from_model_into(data)
157
+ merge_metadata_from_controller_into(data)
158
+ end
159
+
160
+ # Updates `data` from `controller_info`.
161
+ #
162
+ # @api private
163
+ def merge_metadata_from_controller_into(data)
164
+ data.merge(PaperTrail.request.controller_info || {})
165
+ end
166
+
167
+ # Updates `data` from the model's `meta` option.
168
+ #
169
+ # @api private
170
+ def merge_metadata_from_model_into(data)
171
+ @record.paper_trail_options[:meta].each do |k, v|
172
+ data[k] = model_metadatum(v, data[:event])
173
+ end
174
+ end
175
+
176
+ # Given a `value` from the model's `meta` option, returns an object to be
177
+ # persisted. The `value` can be a simple scalar value, but it can also
178
+ # be a symbol that names a model method, or even a Proc.
179
+ #
180
+ # @api private
181
+ def model_metadatum(value, event)
182
+ if value.respond_to?(:call)
183
+ value.call(@record)
184
+ elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
185
+ # If it is an attribute that is changing in an existing object,
186
+ # be sure to grab the current version.
187
+ if event != "create" &&
188
+ @record.has_attribute?(value) &&
189
+ attribute_changed_in_latest_version?(value)
190
+ attribute_in_previous_version(value, false)
191
+ else
192
+ @record.send(value)
193
+ end
194
+ else
195
+ value
196
+ end
197
+ end
198
+
199
+ # @api private
200
+ def notable_changes
201
+ changes_in_latest_version.delete_if { |k, _v|
202
+ !notably_changed.include?(k)
203
+ }
204
+ end
205
+
206
+ # @api private
207
+ def notably_changed
208
+ # Memoized to reduce memory usage
209
+ @notably_changed ||= begin
210
+ only = @record.paper_trail_options[:only].dup
211
+ # Remove Hash arguments and then evaluate whether the attributes (the
212
+ # keys of the hash) should also get pushed into the collection.
213
+ only.delete_if do |obj|
214
+ obj.is_a?(Hash) &&
215
+ obj.each { |attr, condition|
216
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
217
+ }
218
+ end
219
+ only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
220
+ end
221
+ end
222
+
223
+ # Returns hash of attributes (with appropriate attributes serialized),
224
+ # omitting attributes to be skipped.
225
+ #
226
+ # @api private
227
+ def object_attrs_for_paper_trail(is_touch)
228
+ attrs = nonskipped_attributes_before_change(is_touch)
229
+ AttributeSerializers::ObjectAttribute.new(@record.class).serialize(attrs)
230
+ attrs
231
+ end
232
+
233
+ # @api private
234
+ def prepare_object_changes(changes)
235
+ changes = serialize_object_changes(changes)
236
+ recordable_object_changes(changes)
237
+ end
238
+
239
+ # Returns an object which can be assigned to the `object_changes`
240
+ # attribute of a nascent version record. If the `object_changes` column is
241
+ # a postgres `json` column, then a hash can be used in the assignment,
242
+ # otherwise the column is a `text` column, and we must perform the
243
+ # serialization here, using `PaperTrail.serializer`.
244
+ #
245
+ # @api private
246
+ # @param changes HashWithIndifferentAccess
247
+ def recordable_object_changes(changes)
248
+ if PaperTrail.config.object_changes_adapter.respond_to?(:diff)
249
+ # We'd like to avoid the `to_hash` here, because it increases memory
250
+ # usage, but that would be a breaking change because
251
+ # `object_changes_adapter` expects a plain `Hash`, not a
252
+ # `HashWithIndifferentAccess`.
253
+ changes = PaperTrail.config.object_changes_adapter.diff(changes.to_hash)
254
+ end
255
+
256
+ if @record.class.paper_trail.version_class.object_changes_col_is_json?
257
+ changes
258
+ else
259
+ PaperTrail.serializer.dump(changes)
260
+ end
261
+ end
262
+
263
+ # Returns a boolean indicating whether to store serialized version diffs
264
+ # in the `object_changes` column of the version record.
265
+ #
266
+ # @api private
267
+ def record_object_changes?
268
+ @record.class.paper_trail.version_class.column_names.include?("object_changes")
269
+ end
270
+
271
+ # Returns a boolean indicating whether to store the original object during save.
272
+ #
273
+ # @api private
274
+ def record_object?
275
+ @record.class.paper_trail.version_class.column_names.include?("object")
276
+ end
277
+
278
+ # Returns an object which can be assigned to the `object` attribute of a
279
+ # nascent version record. If the `object` column is a postgres `json`
280
+ # column, then a hash can be used in the assignment, otherwise the column
281
+ # is a `text` column, and we must perform the serialization here, using
282
+ # `PaperTrail.serializer`.
283
+ #
284
+ # @api private
285
+ def recordable_object(is_touch)
286
+ if @record.class.paper_trail.version_class.object_col_is_json?
287
+ object_attrs_for_paper_trail(is_touch)
288
+ else
289
+ PaperTrail.serializer.dump(object_attrs_for_paper_trail(is_touch))
290
+ end
291
+ end
292
+
293
+ # @api private
294
+ def serialize_object_changes(changes)
295
+ AttributeSerializers::ObjectChangesAttribute.
296
+ new(@record.class).
297
+ serialize(changes)
298
+
299
+ # We'd like to convert this `HashWithIndifferentAccess` to a plain
300
+ # `Hash`, but we don't, to save memory.
301
+ changes
302
+ end
303
+ end
304
+ end
305
+ 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,60 @@
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
+ if record_object_changes?
39
+ changes = @force_changes.nil? ? notable_changes : @force_changes
40
+ data[:object_changes] = prepare_object_changes(changes)
41
+ end
42
+ merge_item_subtype_into(data)
43
+ merge_metadata_into(data)
44
+ end
45
+
46
+ private
47
+
48
+ # `touch` cannot record `object_changes` because rails' `touch` does not
49
+ # perform dirty-tracking. Specifically, methods from `Dirty`, like
50
+ # `saved_changes`, return the same values before and after `touch`.
51
+ #
52
+ # See https://github.com/rails/rails/issues/33429
53
+ #
54
+ # @api private
55
+ def record_object_changes?
56
+ !@is_touch && super
57
+ end
58
+ end
59
+ end
60
+ 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