paper_trail 1.4.0 → 17.0.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/lib/generators/paper_trail/install/USAGE +31 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +101 -0
  4. data/lib/generators/paper_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
  5. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +41 -0
  6. data/lib/generators/paper_trail/migration_generator.rb +65 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +86 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +40 -0
  10. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  11. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +41 -0
  12. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
  13. data/lib/paper_trail/attribute_serializers/object_attribute.rb +48 -0
  14. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +51 -0
  15. data/lib/paper_trail/cleaner.rb +60 -0
  16. data/lib/paper_trail/compatibility.rb +51 -0
  17. data/lib/paper_trail/config.rb +41 -0
  18. data/lib/paper_trail/errors.rb +33 -0
  19. data/lib/paper_trail/events/base.rb +343 -0
  20. data/lib/paper_trail/events/create.rb +32 -0
  21. data/lib/paper_trail/events/destroy.rb +42 -0
  22. data/lib/paper_trail/events/update.rb +76 -0
  23. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +16 -0
  24. data/lib/paper_trail/frameworks/active_record.rb +12 -0
  25. data/lib/paper_trail/frameworks/cucumber.rb +33 -0
  26. data/lib/paper_trail/frameworks/rails/controller.rb +103 -0
  27. data/lib/paper_trail/frameworks/rails/railtie.rb +34 -0
  28. data/lib/paper_trail/frameworks/rails.rb +3 -0
  29. data/lib/paper_trail/frameworks/rspec/helpers.rb +29 -0
  30. data/lib/paper_trail/frameworks/rspec.rb +42 -0
  31. data/lib/paper_trail/has_paper_trail.rb +79 -82
  32. data/lib/paper_trail/model_config.rb +257 -0
  33. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  34. data/lib/paper_trail/queries/versions/where_object.rb +65 -0
  35. data/lib/paper_trail/queries/versions/where_object_changes.rb +70 -0
  36. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  37. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  38. data/lib/paper_trail/record_history.rb +51 -0
  39. data/lib/paper_trail/record_trail.rb +342 -0
  40. data/lib/paper_trail/reifier.rb +147 -0
  41. data/lib/paper_trail/request.rb +163 -0
  42. data/lib/paper_trail/serializers/json.rb +36 -0
  43. data/lib/paper_trail/serializers/yaml.rb +68 -0
  44. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +35 -0
  45. data/lib/paper_trail/version_concern.rb +406 -0
  46. data/lib/paper_trail/version_number.rb +23 -0
  47. data/lib/paper_trail.rb +128 -19
  48. metadata +444 -70
  49. data/.gitignore +0 -3
  50. data/README.md +0 -225
  51. data/Rakefile +0 -50
  52. data/VERSION +0 -1
  53. data/generators/paper_trail/USAGE +0 -2
  54. data/generators/paper_trail/paper_trail_generator.rb +0 -9
  55. data/generators/paper_trail/templates/create_versions.rb +0 -18
  56. data/init.rb +0 -1
  57. data/install.rb +0 -1
  58. data/lib/paper_trail/version.rb +0 -59
  59. data/paper_trail.gemspec +0 -67
  60. data/rails/init.rb +0 -1
  61. data/tasks/paper_trail_tasks.rake +0 -0
  62. data/test/database.yml +0 -18
  63. data/test/paper_trail_controller_test.rb +0 -70
  64. data/test/paper_trail_model_test.rb +0 -448
  65. data/test/paper_trail_schema_test.rb +0 -15
  66. data/test/schema.rb +0 -48
  67. data/test/schema_change.rb +0 -3
  68. data/test/test_helper.rb +0 -43
  69. data/uninstall.rb +0 -1
  70. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Rails
5
+ # Extensions to rails controllers. Provides convenient ways to pass certain
6
+ # information to the model layer, with `controller_info` and `whodunnit`.
7
+ # Also includes a convenient on/off switch,
8
+ # `paper_trail_enabled_for_controller`.
9
+ module Controller
10
+ def self.included(controller)
11
+ controller.before_action(
12
+ :set_paper_trail_enabled_for_controller,
13
+ :set_paper_trail_controller_info
14
+ )
15
+ end
16
+
17
+ protected
18
+
19
+ # Returns the user who is responsible for any changes that occur.
20
+ # By default this calls `current_user` and returns the result.
21
+ #
22
+ # Override this method in your controller to call a different
23
+ # method, e.g. `current_person`, or anything you like.
24
+ #
25
+ # @api public
26
+ def user_for_paper_trail
27
+ return unless defined?(current_user)
28
+ current_user.try(:id) || current_user
29
+ end
30
+
31
+ # Returns any information about the controller or request that you
32
+ # want PaperTrail to store alongside any changes that occur. By
33
+ # default this returns an empty hash.
34
+ #
35
+ # Override this method in your controller to return a hash of any
36
+ # information you need. The hash's keys must correspond to columns
37
+ # in your `versions` table, so don't forget to add any new columns
38
+ # you need.
39
+ #
40
+ # For example:
41
+ #
42
+ # {:ip => request.remote_ip, :user_agent => request.user_agent}
43
+ #
44
+ # The columns `ip` and `user_agent` must exist in your `versions` # table.
45
+ #
46
+ # Use the `:meta` option to
47
+ # `PaperTrail::Model::ClassMethods.has_paper_trail` to store any extra
48
+ # model-level data you need.
49
+ #
50
+ # @api public
51
+ def info_for_paper_trail
52
+ {}
53
+ end
54
+
55
+ # Returns `true` (default) or `false` depending on whether PaperTrail
56
+ # should be active for the current request.
57
+ #
58
+ # Override this method in your controller to specify when PaperTrail
59
+ # should be off.
60
+ #
61
+ # ```
62
+ # def paper_trail_enabled_for_controller
63
+ # # Don't omit `super` without a good reason.
64
+ # super && request.user_agent != 'Disable User-Agent'
65
+ # end
66
+ # ```
67
+ #
68
+ # @api public
69
+ def paper_trail_enabled_for_controller
70
+ ::PaperTrail.enabled?
71
+ end
72
+
73
+ private
74
+
75
+ # Tells PaperTrail whether versions should be saved in the current
76
+ # request.
77
+ #
78
+ # @api public
79
+ def set_paper_trail_enabled_for_controller
80
+ ::PaperTrail.request.enabled = paper_trail_enabled_for_controller
81
+ end
82
+
83
+ # Tells PaperTrail who is responsible for any changes that occur.
84
+ #
85
+ # @api public
86
+ def set_paper_trail_whodunnit
87
+ if ::PaperTrail.request.enabled?
88
+ ::PaperTrail.request.whodunnit = user_for_paper_trail
89
+ end
90
+ end
91
+
92
+ # Tells PaperTrail any information from the controller you want to store
93
+ # alongside any changes that occur.
94
+ #
95
+ # @api public
96
+ def set_paper_trail_controller_info
97
+ if ::PaperTrail.request.enabled?
98
+ ::PaperTrail.request.controller_info = info_for_paper_trail
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,34 @@
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 |app|
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
+
29
+ if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("7.1")
30
+ app.deprecators[:paper_trail] = PaperTrail.deprecator
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paper_trail/frameworks/rails/railtie"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module RSpec
5
+ module Helpers
6
+ # Included in the RSpec configuration in `frameworks/rspec.rb`
7
+ module InstanceMethods
8
+ # enable versioning for specific blocks (at instance-level)
9
+ def with_versioning
10
+ was_enabled = ::PaperTrail.enabled?
11
+ ::PaperTrail.enabled = true
12
+ yield
13
+ ensure
14
+ ::PaperTrail.enabled = was_enabled
15
+ end
16
+ end
17
+
18
+ # Extended by the RSpec configuration in `frameworks/rspec.rb`
19
+ module ClassMethods
20
+ # enable versioning for specific blocks (at class-level)
21
+ def with_versioning(&block)
22
+ context "with versioning", versioning: true do
23
+ class_exec(&block)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "rspec/matchers"
5
+ require "paper_trail/frameworks/rspec/helpers"
6
+
7
+ RSpec.configure do |config|
8
+ config.include PaperTrail::RSpec::Helpers::InstanceMethods
9
+ config.extend PaperTrail::RSpec::Helpers::ClassMethods
10
+
11
+ config.before(:each) do
12
+ PaperTrail.enabled = false
13
+ PaperTrail.request.enabled = true
14
+ PaperTrail.request.whodunnit = nil
15
+ PaperTrail.request.controller_info = {} if defined?(Rails) && defined?(RSpec::Rails)
16
+ end
17
+
18
+ config.before(:each, versioning: true) do
19
+ PaperTrail.enabled = true
20
+ end
21
+ end
22
+
23
+ RSpec::Matchers.define :be_versioned do
24
+ # check to see if the model has `has_paper_trail` declared on it
25
+ match { |actual| actual.is_a?(PaperTrail::Model::InstanceMethods) }
26
+ end
27
+
28
+ RSpec::Matchers.define :have_a_version_with do |attributes|
29
+ # check if the model has a version with the specified attributes
30
+ match do |actual|
31
+ versions_association = actual.class.versions_association_name
32
+ actual.send(versions_association).where_object(attributes).any?
33
+ end
34
+ end
35
+
36
+ RSpec::Matchers.define :have_a_version_with_changes do |attributes|
37
+ # check if the model has a version changes with the specified attributes
38
+ match do |actual|
39
+ versions_association = actual.class.versions_association_name
40
+ actual.send(versions_association).where_object_changes(attributes).any?
41
+ end
42
+ end
@@ -1,95 +1,92 @@
1
- module PaperTrail
2
-
3
- def self.included(base)
4
- base.send :extend, ClassMethods
5
- end
6
-
7
-
8
- module ClassMethods
9
- # Options:
10
- # :ignore an array of attributes for which a new +Version+ will not be created if only they change.
11
- # :meta a hash of extra data to store. You must add a column to the versions table for each key.
12
- # Values are objects or procs (which are called with +self+, i.e. the model with the paper
13
- # trail).
14
- def has_paper_trail(options = {})
15
- send :include, InstanceMethods
16
-
17
- cattr_accessor :ignore
18
- self.ignore = (options[:ignore] || []).map &:to_s
19
-
20
- cattr_accessor :meta
21
- self.meta = options[:meta] || {}
22
-
23
- cattr_accessor :paper_trail_active
24
- self.paper_trail_active = true
25
-
26
- has_many :versions, :as => :item, :order => 'created_at ASC, id ASC'
27
-
28
- after_create :record_create
29
- before_update :record_update
30
- after_destroy :record_destroy
31
- end
32
-
33
- def paper_trail_off
34
- self.paper_trail_active = false
35
- end
36
-
37
- def paper_trail_on
38
- self.paper_trail_active = true
39
- end
40
- end
41
-
1
+ # frozen_string_literal: true
42
2
 
43
- module InstanceMethods
44
- def record_create
45
- if self.class.paper_trail_active
46
- versions.create merge_metadata(:event => 'create', :whodunnit => PaperTrail.whodunnit)
47
- end
48
- end
3
+ require "paper_trail/attribute_serializers/object_attribute"
4
+ require "paper_trail/attribute_serializers/object_changes_attribute"
5
+ require "paper_trail/model_config"
6
+ require "paper_trail/record_trail"
49
7
 
50
- def record_update
51
- if changed_and_we_care? and self.class.paper_trail_active
52
- versions.build merge_metadata(:event => 'update',
53
- :object => object_to_string(previous_version),
54
- :whodunnit => PaperTrail.whodunnit)
55
- end
8
+ module PaperTrail
9
+ # Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
10
+ # It is our goal to have the smallest possible footprint here, because
11
+ # `ActiveRecord::Base` is a very crowded namespace! That is why we introduced
12
+ # `.paper_trail` and `#paper_trail`.
13
+ module Model
14
+ def self.included(base)
15
+ base.extend ClassMethods
56
16
  end
57
17
 
58
- def record_destroy
59
- if self.class.paper_trail_active
60
- versions.create merge_metadata(:event => 'destroy',
61
- :object => object_to_string(previous_version),
62
- :whodunnit => PaperTrail.whodunnit)
18
+ # :nodoc:
19
+ module ClassMethods
20
+ # Declare this in your model to track every create, update, and destroy.
21
+ # Each version of the model is available in the `versions` association.
22
+ #
23
+ # Options:
24
+ #
25
+ # - :on - The events to track (optional; defaults to all of them). Set
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
+ # - :ignore - An array of attributes for which a new `Version` will not be
30
+ # created if only they change. It can also accept a Hash as an
31
+ # argument where the key is the attribute to ignore (a `String` or
32
+ # `Symbol`), which will only be ignored if the value is a `Proc` which
33
+ # returns truthily.
34
+ # - :if, :unless - Procs that allow to specify conditions when to save
35
+ # versions for an object.
36
+ # - :only - Inverse of `ignore`. A new `Version` will be created only
37
+ # for these attributes if supplied it can also accept a Hash as an
38
+ # argument where the key is the attribute to track (a `String` or
39
+ # `Symbol`), which will only be counted if the value is a `Proc` which
40
+ # returns truthily.
41
+ # - :skip - Fields to ignore completely. As with `ignore`, updates to
42
+ # these fields will not create a new `Version`. In addition, these
43
+ # fields will not be included in the serialized versions of the object
44
+ # whenever a new `Version` is created.
45
+ # - :meta - A hash of extra data to store. You must add a column to the
46
+ # `versions` table for each key. Values are objects or procs (which
47
+ # are called with `self`, i.e. the model with the paper trail). See
48
+ # `PaperTrail::Controller.info_for_paper_trail` for how to store data
49
+ # from the controller.
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:`.
54
+ # - :version - The name to use for the method which returns the version
55
+ # the instance was reified from. Default is `:version`.
56
+ # - :synchronize_version_creation_timestamp - By default, paper trail
57
+ # sets the `created_at` field for a new Version equal to the `updated_at`
58
+ # column of the model being updated. If you instead want `created_at` to
59
+ # populate with the current timestamp, set this option to `false`.
60
+ #
61
+ # Plugins like the experimental `paper_trail-association_tracking` gem
62
+ # may accept additional options.
63
+ #
64
+ # You can define a default set of options via the configurable
65
+ # `PaperTrail.config.has_paper_trail_defaults` hash in your applications
66
+ # initializer. The hash can contain any of the following options and will
67
+ # provide an overridable default for all models.
68
+ #
69
+ # @api public
70
+ def has_paper_trail(options = {})
71
+ raise Error, "has_paper_trail must be called only once" if self < InstanceMethods
72
+
73
+ defaults = PaperTrail.config.has_paper_trail_defaults
74
+ paper_trail.setup(defaults.merge(options))
63
75
  end
64
- end
65
76
 
66
- private
67
-
68
- def merge_metadata(data)
69
- meta.each do |k,v|
70
- data[k] = v.respond_to?(:call) ? v.call(self) : v
77
+ # @api public
78
+ def paper_trail
79
+ ::PaperTrail::ModelConfig.new(self)
71
80
  end
72
- data
73
81
  end
74
82
 
75
- def previous_version
76
- previous = self.clone
77
- previous.id = id
78
- changes.each do |attr, ary|
79
- previous.send "#{attr}=", ary.first
83
+ # Wrap the following methods in a module so we can include them only in the
84
+ # ActiveRecord models that declare `has_paper_trail`.
85
+ module InstanceMethods
86
+ # @api public
87
+ def paper_trail
88
+ ::PaperTrail::RecordTrail.new(self)
80
89
  end
81
- previous
82
- end
83
-
84
- def object_to_string(object)
85
- object.attributes.to_yaml
86
- end
87
-
88
- def changed_and_we_care?
89
- changed? and !(changed - self.class.ignore).empty?
90
90
  end
91
91
  end
92
-
93
92
  end
94
-
95
- ActiveRecord::Base.send :include, PaperTrail
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Configures an ActiveRecord model, mostly at application boot time, but also
5
+ # sometimes mid-request, with methods like enable/disable.
6
+ class ModelConfig
7
+ E_CANNOT_RECORD_AFTER_DESTROY = <<~STR
8
+ paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
9
+ belongs_to_required_by_default. Use on_destroy(:before)
10
+ or disable belongs_to_required_by_default.
11
+ STR
12
+ E_HPT_ABSTRACT_CLASS = <<~STR.squish.freeze
13
+ An application model (%s) has been configured to use PaperTrail (via
14
+ `has_paper_trail`), but the version model it has been told to use (%s) is
15
+ an `abstract_class`. This could happen when an advanced feature called
16
+ Custom Version Classes (http://bit.ly/2G4ch0G) is misconfigured. When all
17
+ version classes are custom, PaperTrail::Version is configured to be an
18
+ `abstract_class`. This is fine, but all application models must be
19
+ configured to use concrete (not abstract) version models.
20
+ STR
21
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
22
+ Passing versions association name as `has_paper_trail versions: %{versions_name}`
23
+ is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
24
+ The hash you pass to `versions:` is now passed directly to `has_many`.
25
+ STR
26
+ DPR_CLASS_NAME_OPTION = <<~STR.squish
27
+ Passing Version class name as `has_paper_trail class_name: %{class_name}`
28
+ is deprecated. Use `has_paper_trail versions: {class_name: %{class_name}}`
29
+ instead. The hash you pass to `versions:` is now passed directly to `has_many`.
30
+ STR
31
+
32
+ def initialize(model_class)
33
+ @model_class = model_class
34
+ end
35
+
36
+ # Adds a callback that records a version after a "create" event.
37
+ #
38
+ # @api public
39
+ def on_create
40
+ @model_class.after_create { |r|
41
+ r.paper_trail.record_create if r.paper_trail.save_version?
42
+ }
43
+ append_option_uniquely(:on, :create)
44
+ end
45
+
46
+ # Adds a callback that records a version before or after a "destroy" event.
47
+ #
48
+ # @api public
49
+ def on_destroy(recording_order = "before")
50
+ assert_valid_recording_order_for_on_destroy(recording_order)
51
+ @model_class.send(
52
+ :"#{recording_order}_destroy",
53
+ lambda do |r|
54
+ return unless r.paper_trail.save_version?
55
+ r.paper_trail.record_destroy(recording_order)
56
+ end
57
+ )
58
+ append_option_uniquely(:on, :destroy)
59
+ end
60
+
61
+ # Adds a callback that records a version after an "update" event.
62
+ #
63
+ # @api public
64
+ def on_update
65
+ @model_class.before_save { |r|
66
+ r.paper_trail.reset_timestamp_attrs_for_update_if_needed
67
+ }
68
+ @model_class.after_update { |r|
69
+ if r.paper_trail.save_version?
70
+ r.paper_trail.record_update(
71
+ force: false,
72
+ in_after_callback: true,
73
+ is_touch: false
74
+ )
75
+ end
76
+ }
77
+ @model_class.after_update { |r|
78
+ r.paper_trail.clear_version_instance
79
+ }
80
+ append_option_uniquely(:on, :update)
81
+ end
82
+
83
+ # Adds a callback that records a version after a "touch" event.
84
+ #
85
+ # Rails < 6.0 (no longer supported by PT) had a bug where dirty-tracking
86
+ # did not occur during a `touch`.
87
+ # (https://github.com/rails/rails/issues/33429) See also:
88
+ # https://github.com/paper-trail-gem/paper_trail/issues/1121
89
+ # https://github.com/paper-trail-gem/paper_trail/issues/1161
90
+ # https://github.com/paper-trail-gem/paper_trail/pull/1285
91
+ #
92
+ # @api public
93
+ def on_touch
94
+ @model_class.after_touch { |r|
95
+ if r.paper_trail.save_version?
96
+ r.paper_trail.record_update(
97
+ force: false,
98
+ in_after_callback: true,
99
+ is_touch: true
100
+ )
101
+ end
102
+ }
103
+ end
104
+
105
+ # Set up `@model_class` for PaperTrail. Installs callbacks, associations,
106
+ # "class attributes", instance methods, and more.
107
+ # @api private
108
+ def setup(options = {})
109
+ options[:on] ||= %i[create update destroy touch]
110
+ options[:on] = Array(options[:on]) # Support single symbol
111
+ @model_class.send :include, ::PaperTrail::Model::InstanceMethods
112
+ setup_options(options)
113
+ setup_associations(options)
114
+ @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
115
+ setup_callbacks_from_options options[:on]
116
+ end
117
+
118
+ # @api private
119
+ def version_class
120
+ @version_class ||= @model_class.version_class_name.constantize
121
+ end
122
+
123
+ private
124
+
125
+ # @api private
126
+ def append_option_uniquely(option, value)
127
+ collection = @model_class.paper_trail_options.fetch(option)
128
+ return if collection.include?(value)
129
+ collection << value
130
+ end
131
+
132
+ # Raises an error if the provided class is an `abstract_class`.
133
+ # @api private
134
+ def assert_concrete_activerecord_class(class_name)
135
+ if class_name.constantize.abstract_class?
136
+ raise Error, format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
137
+ end
138
+ end
139
+
140
+ # @api private
141
+ def assert_valid_recording_order_for_on_destroy(recording_order)
142
+ unless %w[after before].include?(recording_order.to_s)
143
+ raise ArgumentError, 'recording order can only be "after" or "before"'
144
+ end
145
+
146
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
147
+ raise Error, E_CANNOT_RECORD_AFTER_DESTROY
148
+ end
149
+ end
150
+
151
+ def cannot_record_after_destroy?
152
+ ::ActiveRecord::Base.belongs_to_required_by_default
153
+ end
154
+
155
+ def check_version_class_name(options)
156
+ # @api private - `version_class_name`
157
+ @model_class.class_attribute :version_class_name
158
+ if options[:class_name]
159
+ PaperTrail.deprecator.warn(
160
+ format(
161
+ DPR_CLASS_NAME_OPTION,
162
+ class_name: options[:class_name].inspect
163
+ ),
164
+ caller(1)
165
+ )
166
+ options[:versions][:class_name] = options[:class_name]
167
+ end
168
+ @model_class.version_class_name = options[:versions][:class_name] || "PaperTrail::Version"
169
+ assert_concrete_activerecord_class(@model_class.version_class_name)
170
+ end
171
+
172
+ def check_versions_association_name(options)
173
+ # @api private - versions_association_name
174
+ @model_class.class_attribute :versions_association_name
175
+ @model_class.versions_association_name = options[:versions][:name] || :versions
176
+ end
177
+
178
+ def define_has_many_versions(options)
179
+ options = ensure_versions_option_is_hash(options)
180
+ check_version_class_name(options)
181
+ check_versions_association_name(options)
182
+ scope = get_versions_scope(options)
183
+ @model_class.has_many(
184
+ @model_class.versions_association_name,
185
+ scope,
186
+ class_name: @model_class.version_class_name,
187
+ as: :item,
188
+ **options[:versions].except(:name, :scope)
189
+ )
190
+ end
191
+
192
+ def ensure_versions_option_is_hash(options)
193
+ unless options[:versions].is_a?(Hash)
194
+ if options[:versions]
195
+ PaperTrail.deprecator.warn(
196
+ format(
197
+ DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION,
198
+ versions_name: options[:versions].inspect
199
+ ),
200
+ caller(1)
201
+ )
202
+ end
203
+ options[:versions] = {
204
+ name: options[:versions]
205
+ }
206
+ end
207
+ options
208
+ end
209
+
210
+ # Process an `ignore`, `skip`, or `only` option.
211
+ def event_attribute_option(option_name)
212
+ [@model_class.paper_trail_options[option_name]].
213
+ flatten.
214
+ compact.
215
+ map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
216
+ end
217
+
218
+ def get_versions_scope(options)
219
+ options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
220
+ end
221
+
222
+ def setup_associations(options)
223
+ # @api private - version_association_name
224
+ @model_class.class_attribute :version_association_name
225
+ @model_class.version_association_name = options[:version] || :version
226
+
227
+ # The version this instance was reified from.
228
+ # @api public
229
+ @model_class.send :attr_accessor, @model_class.version_association_name
230
+
231
+ # @api public - paper_trail_event
232
+ @model_class.send :attr_accessor, :paper_trail_event
233
+
234
+ define_has_many_versions(options)
235
+ end
236
+
237
+ def setup_callbacks_from_options(options_on = [])
238
+ options_on.each do |event|
239
+ public_send(:"on_#{event}")
240
+ end
241
+ end
242
+
243
+ def setup_options(options)
244
+ # @api public - paper_trail_options - Let's encourage plugins to use eg.
245
+ # `paper_trail_options[:versions][:class_name]` rather than
246
+ # `version_class_name` because the former is documented and the latter is
247
+ # not.
248
+ @model_class.class_attribute :paper_trail_options
249
+ @model_class.paper_trail_options = options.dup
250
+
251
+ %i[ignore skip only].each do |k|
252
+ @model_class.paper_trail_options[k] = event_attribute_option(k)
253
+ end
254
+ @model_class.paper_trail_options[:meta] ||= {}
255
+ end
256
+ end
257
+ end