paper_trail 10.3.1 → 14.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +25 -7
  4. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +4 -2
  5. data/lib/generators/paper_trail/migration_generator.rb +5 -4
  6. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +4 -2
  7. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  8. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +17 -45
  9. data/lib/paper_trail/compatibility.rb +3 -3
  10. data/lib/paper_trail/config.rb +0 -33
  11. data/lib/paper_trail/errors.rb +33 -0
  12. data/lib/paper_trail/events/base.rb +92 -69
  13. data/lib/paper_trail/events/destroy.rb +1 -1
  14. data/lib/paper_trail/events/update.rb +23 -7
  15. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  16. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  17. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  18. data/lib/paper_trail/frameworks/rails.rb +1 -2
  19. data/lib/paper_trail/has_paper_trail.rb +1 -1
  20. data/lib/paper_trail/model_config.rb +46 -46
  21. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  22. data/lib/paper_trail/queries/versions/where_object.rb +1 -1
  23. data/lib/paper_trail/queries/versions/where_object_changes.rb +9 -14
  24. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  25. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  26. data/lib/paper_trail/record_trail.rb +80 -64
  27. data/lib/paper_trail/reifier.rb +41 -26
  28. data/lib/paper_trail/request.rb +22 -25
  29. data/lib/paper_trail/serializers/json.rb +0 -10
  30. data/lib/paper_trail/serializers/yaml.rb +38 -13
  31. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -14
  32. data/lib/paper_trail/version_concern.rb +86 -41
  33. data/lib/paper_trail/version_number.rb +3 -3
  34. data/lib/paper_trail.rb +22 -40
  35. metadata +106 -45
  36. data/lib/paper_trail/frameworks/rails/engine.rb +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a08a7c456a492933cc36762913040e14bad8f468fc08889580faf9e86ade7fea
4
- data.tar.gz: 19711dc6c6e4a438a0a97819c572df610d52db0f5c9aeba0a39eb1f44f3909c0
3
+ metadata.gz: 8442e35802b28551f1ab7275a3e6a14fed55edcba6c1aa9beeff5e65a2a92626
4
+ data.tar.gz: 3014b42bde912764165fc14a8974b14cc16cdf9643a1f8c9af23083a4479c50d
5
5
  SHA512:
6
- metadata.gz: b39e89605a2b03889976070f0871f9dcd07544b97b6e0105b78035b5fb1ec030c7d5438dc0fe9fb16cccc8e3624cd920d37bd73066f8a4f3892bf356fa088e85
7
- data.tar.gz: 56567a718ea4e605d32ffc2b71835b188f6cefab8dd7111b7abcdfc6d9e5b843e8468312ed4dfbc87f74607c1cd4a197ae2c11a9cb44ee2dd16a37a11303e6ad
6
+ metadata.gz: 57f5248c0e5d448dd20e88530f06e74fddcf641c1f13f6b9d7e33ef54305168e5f0bb34322c4db7b0b9b5fe9c7c988116a72b17605500061c43ffc67f0519f9d
7
+ data.tar.gz: 6d84306539336b774e8b9cd5f1f2412fa59752c6ceb3033af6be9652045e19d5e11f95555f47cdde3742bf18e6165addcde3e3d5a5b2c7b8df7ecc908b16d6eb
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Andy Stewart, AirBlade Software Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -20,25 +20,43 @@ module PaperTrail
20
20
  default: false,
21
21
  desc: "Store changeset (diff) with each version"
22
22
  )
23
+ class_option(
24
+ :uuid,
25
+ type: :boolean,
26
+ default: false,
27
+ desc: "Use uuid instead of bigint for item_id type (use only if tables use UUIDs)"
28
+ )
23
29
 
24
30
  desc "Generates (but does not run) a migration to add a versions table." \
25
31
  " See section 5.c. Generators in README.md for more information."
26
32
 
27
33
  def create_migration_file
28
- add_paper_trail_migration("create_versions",
34
+ add_paper_trail_migration(
35
+ "create_versions",
29
36
  item_type_options: item_type_options,
30
- versions_table_options: versions_table_options)
31
- add_paper_trail_migration("add_object_changes_to_versions") if options.with_changes?
37
+ versions_table_options: versions_table_options,
38
+ item_id_type_options: item_id_type_options
39
+ )
40
+ if options.with_changes?
41
+ add_paper_trail_migration("add_object_changes_to_versions")
42
+ end
32
43
  end
33
44
 
34
45
  private
35
46
 
47
+ # To use uuid instead of integer for primary key
48
+ def item_id_type_options
49
+ options.uuid? ? "string" : "bigint"
50
+ end
51
+
36
52
  # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
37
53
  # See https://github.com/paper-trail-gem/paper_trail/issues/651
38
54
  def item_type_options
39
- opt = { null: false }
40
- opt[:limit] = 191 if mysql?
41
- ", #{opt}"
55
+ if mysql?
56
+ ", null: false, limit: 191"
57
+ else
58
+ ", null: false"
59
+ end
42
60
  end
43
61
 
44
62
  def mysql?
@@ -62,7 +80,7 @@ module PaperTrail
62
80
  #
63
81
  def versions_table_options
64
82
  if mysql?
65
- ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }'
83
+ ', options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"'
66
84
  else
67
85
  ""
68
86
  end
@@ -11,7 +11,7 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
11
11
  def change
12
12
  create_table :versions<%= versions_table_options %> do |t|
13
13
  t.string :item_type<%= item_type_options %>
14
- t.integer :item_id, null: false, limit: 8
14
+ t.<%= item_id_type_options %> :item_id, null: false
15
15
  t.string :event, null: false
16
16
  t.string :whodunnit
17
17
  t.text :object, limit: TEXT_BYTES
@@ -29,8 +29,10 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
29
29
  # version of ActiveRecord with support for fractional seconds in MySQL.
30
30
  # (https://github.com/rails/rails/pull/14359)
31
31
  #
32
+ # MySQL users should use the following line for `created_at`
33
+ # t.datetime :created_at, limit: 6
32
34
  t.datetime :created_at
33
35
  end
34
- add_index :versions, %i(item_type item_id)
36
+ add_index :versions, %i[item_type item_id]
35
37
  end
36
38
  end
@@ -28,10 +28,11 @@ module PaperTrail
28
28
  end
29
29
 
30
30
  def migration_version
31
- major = ActiveRecord::VERSION::MAJOR
32
- if major >= 5
33
- "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
34
- end
31
+ format(
32
+ "[%d.%d]",
33
+ ActiveRecord::VERSION::MAJOR,
34
+ ActiveRecord::VERSION::MINOR
35
+ )
35
36
  end
36
37
  end
37
38
  end
@@ -7,8 +7,10 @@ module PaperTrail
7
7
  class UpdateItemSubtypeGenerator < MigrationGenerator
8
8
  source_root File.expand_path("templates", __dir__)
9
9
 
10
- desc "Generates (but does not run) a migration to update item_subtype for STI entries in an "\
11
- "existing versions table."
10
+ desc(
11
+ "Generates (but does not run) a migration to update item_subtype for "\
12
+ "STI entries in an existing versions table."
13
+ )
12
14
 
13
15
  def create_migration_file
14
16
  add_paper_trail_migration("update_versions_for_item_subtype", sti_type_options: options)
@@ -8,18 +8,32 @@ module PaperTrail
8
8
  # not suited for writing JSON to a text column. This factory
9
9
  # replaces certain default Active Record serializers
10
10
  # with custom PaperTrail ones.
11
+ #
12
+ # @api private
11
13
  module AttributeSerializerFactory
12
- AR_PG_ARRAY_CLASS = "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array"
14
+ class << self
15
+ # @api private
16
+ def for(klass, attr)
17
+ active_record_serializer = klass.type_for_attribute(attr)
18
+ if ar_pg_array?(active_record_serializer)
19
+ TypeSerializers::PostgresArraySerializer.new(
20
+ active_record_serializer.subtype,
21
+ active_record_serializer.delimiter
22
+ )
23
+ else
24
+ active_record_serializer
25
+ end
26
+ end
27
+
28
+ private
13
29
 
14
- def self.for(klass, attr)
15
- active_record_serializer = klass.type_for_attribute(attr)
16
- if active_record_serializer.class.name == AR_PG_ARRAY_CLASS
17
- TypeSerializers::PostgresArraySerializer.new(
18
- active_record_serializer.subtype,
19
- active_record_serializer.delimiter
20
- )
21
- else
22
- active_record_serializer
30
+ # @api private
31
+ def ar_pg_array?(obj)
32
+ if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array)
33
+ obj.instance_of?(::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array)
34
+ else
35
+ false
36
+ end
23
37
  end
24
38
  end
25
39
  end
@@ -8,9 +8,6 @@ module PaperTrail
8
8
  # The `CastAttributeSerializer` (de)serializes model attribute values. For
9
9
  # example, the string "1.99" serializes into the integer `1` when assigned
10
10
  # to an attribute of type `ActiveRecord::Type::Integer`.
11
- #
12
- # This implementation depends on the `type_for_attribute` method, which was
13
- # introduced in rails 4.2. As of PT 8, we no longer support rails < 4.2.
14
11
  class CastAttributeSerializer
15
12
  def initialize(klass)
16
13
  @klass = klass
@@ -30,53 +27,28 @@ module PaperTrail
30
27
  def defined_enums
31
28
  @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
32
29
  end
33
- end
34
30
 
35
- if ::ActiveRecord::VERSION::MAJOR >= 5
36
- # This implementation uses AR 5's `serialize` and `deserialize`.
37
- class CastAttributeSerializer
38
- def serialize(attr, val)
39
- AttributeSerializerFactory.for(@klass, attr).serialize(val)
31
+ def deserialize(attr, val)
32
+ if defined_enums[attr] && val.is_a?(::String)
33
+ # Because PT 4 used to save the string version of enums to `object_changes`
34
+ val
35
+ elsif rails_gte_7_0? && val.is_a?(ActiveRecord::Type::Time::Value)
36
+ # Because Rails 7 time attribute throws a delegation error when you deserialize
37
+ # it with the factory.
38
+ # See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
39
+ # https://github.com/rails/rails/issues/43966
40
+ val.instance_variable_get(:@time)
41
+ else
42
+ AttributeSerializerFactory.for(@klass, attr).deserialize(val)
40
43
  end
44
+ end
41
45
 
42
- def deserialize(attr, val)
43
- if defined_enums[attr] && val.is_a?(::String)
44
- # Because PT 4 used to save the string version of enums to `object_changes`
45
- val
46
- else
47
- AttributeSerializerFactory.for(@klass, attr).deserialize(val)
48
- end
49
- end
46
+ def rails_gte_7_0?
47
+ ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
50
48
  end
51
- else
52
- # This implementation uses AR 4.2's `type_cast_for_database`. For
53
- # versions of AR < 4.2 we provide an implementation of
54
- # `type_cast_for_database` in our shim attribute type classes,
55
- # `NoOpAttribute` and `SerializedAttribute`.
56
- class CastAttributeSerializer
57
- def serialize(attr, val)
58
- castable_val = val
59
- if defined_enums[attr]
60
- # `attr` is an enum. Find the number that corresponds to `val`. If `val` is
61
- # a number already, there won't be a corresponding entry, just use `val`.
62
- castable_val = defined_enums[attr][val] || val
63
- end
64
- @klass.type_for_attribute(attr).type_cast_for_database(castable_val)
65
- end
66
49
 
67
- def deserialize(attr, val)
68
- if defined_enums[attr] && val.is_a?(::String)
69
- # Because PT 4 used to save the string version of enums to `object_changes`
70
- val
71
- else
72
- val = @klass.type_for_attribute(attr).type_cast_from_database(val)
73
- if defined_enums[attr]
74
- defined_enums[attr].key(val)
75
- else
76
- val
77
- end
78
- end
79
- end
50
+ def serialize(attr, val)
51
+ AttributeSerializerFactory.for(@klass, attr).serialize(val)
80
52
  end
81
53
  end
82
54
  end
@@ -8,7 +8,7 @@ module PaperTrail
8
8
  #
9
9
  # It is not safe to assume that a new version of rails will be compatible with
10
10
  # PaperTrail. PT is only compatible with the versions of rails that it is
11
- # tested against. See `.travis.yml`.
11
+ # tested against. See `.github/workflows/test.yml`.
12
12
  #
13
13
  # However, as of
14
14
  # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
@@ -17,8 +17,8 @@ module PaperTrail
17
17
  # newer rails versions. Most PT users should avoid incompatible rails
18
18
  # versions.
19
19
  module Compatibility
20
- ACTIVERECORD_GTE = ">= 4.2"
21
- ACTIVERECORD_LT = "< 6.1"
20
+ ACTIVERECORD_GTE = ">= 6.0" # enforced in gemspec
21
+ ACTIVERECORD_LT = "< 7.1" # not enforced in gemspec
22
22
 
23
23
  E_INCOMPATIBLE_AR = <<-EOS
24
24
  PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
@@ -9,14 +9,6 @@ module PaperTrail
9
9
  class Config
10
10
  include Singleton
11
11
 
12
- E_PT_AT_REMOVED = <<-EOS.squish
13
- Association Tracking for PaperTrail has been extracted to a separate gem.
14
- To use it, please add `paper_trail-association_tracking` to your Gemfile.
15
- If you don't use it (most people don't, that's the default) and you set
16
- `track_associations = false` somewhere (probably a rails initializer) you
17
- can remove that line now.
18
- EOS
19
-
20
12
  attr_accessor(
21
13
  :association_reify_error_behaviour,
22
14
  :object_changes_adapter,
@@ -43,30 +35,5 @@ module PaperTrail
43
35
  def enabled=(enable)
44
36
  @mutex.synchronize { @enabled = enable }
45
37
  end
46
-
47
- # In PT 10, the paper_trail-association_tracking gem was changed from a
48
- # runtime dependency to a development dependency. We raise an error about
49
- # this for the people who don't read changelogs.
50
- #
51
- # We raise a generic RuntimeError instead of a specific PT error class
52
- # because there is no known use case where someone would want to rescue
53
- # this. If we think of such a use case in the future we can revisit this
54
- # decision.
55
- #
56
- # @override If PT-AT is `require`d, it will replace this method with its
57
- # own implementation.
58
- def track_associations=(value)
59
- if value
60
- raise E_PT_AT_REMOVED
61
- else
62
- ::Kernel.warn(E_PT_AT_REMOVED)
63
- end
64
- end
65
-
66
- # @override If PT-AT is `require`d, it will replace this method with its
67
- # own implementation.
68
- def track_associations?
69
- raise E_PT_AT_REMOVED
70
- end
71
38
  end
72
39
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Generic PaperTrail exception.
5
+ # @api public
6
+ class Error < StandardError
7
+ end
8
+
9
+ # An unexpected option, perhaps a typo, was passed to a public API method.
10
+ # @api public
11
+ class InvalidOption < Error
12
+ end
13
+
14
+ # The application's database schema is not supported.
15
+ # @api public
16
+ class UnsupportedSchema < Error
17
+ end
18
+
19
+ # The application's database column type is not supported.
20
+ # @api public
21
+ class UnsupportedColumnType < UnsupportedSchema
22
+ def initialize(method:, expected:, actual:)
23
+ super(
24
+ format(
25
+ "%s expected %s column, got %s",
26
+ method,
27
+ expected,
28
+ actual
29
+ )
30
+ )
31
+ end
32
+ end
33
+ end
@@ -22,7 +22,18 @@ module PaperTrail
22
22
  #
23
23
  # @api private
24
24
  class Base
25
- RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
25
+ E_FORBIDDEN_METADATA_KEY = <<-EOS.squish
26
+ Forbidden metadata key: %s. As of PT 14, the following metadata keys are
27
+ forbidden: %s
28
+ EOS
29
+ FORBIDDEN_METADATA_KEYS = %i[
30
+ created_at
31
+ id
32
+ item_id
33
+ item_subtype
34
+ item_type
35
+ updated_at
36
+ ].freeze
26
37
 
27
38
  # @api private
28
39
  def initialize(record, in_after_callback)
@@ -46,12 +57,19 @@ module PaperTrail
46
57
 
47
58
  private
48
59
 
60
+ # @api private
61
+ def assert_metadatum_key_is_permitted(key)
62
+ return unless FORBIDDEN_METADATA_KEYS.include?(key.to_sym)
63
+ raise PaperTrail::InvalidOption,
64
+ format(E_FORBIDDEN_METADATA_KEY, key, FORBIDDEN_METADATA_KEYS)
65
+ end
66
+
49
67
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
50
68
  # https://github.com/paper-trail-gem/paper_trail/pull/899
51
69
  #
52
70
  # @api private
53
71
  def attribute_changed_in_latest_version?(attr_name)
54
- if @in_after_callback && RAILS_GTE_5_1
72
+ if @in_after_callback
55
73
  @record.saved_change_to_attribute?(attr_name.to_s)
56
74
  else
57
75
  @record.attribute_changed?(attr_name.to_s)
@@ -60,30 +78,14 @@ module PaperTrail
60
78
 
61
79
  # @api private
62
80
  def nonskipped_attributes_before_change(is_touch)
63
- cache_changed_attributes do
64
- record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip])
65
-
66
- record_attributes.each_key do |k|
67
- if @record.class.column_names.include?(k)
68
- record_attributes[k] = attribute_in_previous_version(k, is_touch)
69
- end
81
+ record_attributes = @record.attributes.except(*@record.paper_trail_options[:skip])
82
+ record_attributes.each_key do |k|
83
+ if @record.class.column_names.include?(k)
84
+ record_attributes[k] = attribute_in_previous_version(k, is_touch)
70
85
  end
71
86
  end
72
87
  end
73
88
 
74
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`.
75
- # @api private
76
- def cache_changed_attributes
77
- if RAILS_GTE_5_1
78
- # Everything works fine as it is
79
- yield
80
- else
81
- # Any particular call to `changed_attributes` produces the huge memory allocation.
82
- # Lets use the generic AR workaround for that.
83
- @record.send(:cache_changed_attributes) { yield }
84
- end
85
- end
86
-
87
89
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
88
90
  # https://github.com/paper-trail-gem/paper_trail/pull/899
89
91
  #
@@ -91,23 +93,19 @@ module PaperTrail
91
93
  #
92
94
  # @api private
93
95
  def attribute_in_previous_version(attr_name, is_touch)
94
- if RAILS_GTE_5_1
95
- if @in_after_callback && !is_touch
96
- # For most events, we want the original value of the attribute, before
97
- # the last save.
98
- @record.attribute_before_last_save(attr_name.to_s)
99
- else
100
- # We are either performing a `record_destroy` or a
101
- # `record_update(is_touch: true)`.
102
- @record.attribute_in_database(attr_name.to_s)
103
- end
96
+ if @in_after_callback && !is_touch
97
+ # For most events, we want the original value of the attribute, before
98
+ # the last save.
99
+ @record.attribute_before_last_save(attr_name.to_s)
104
100
  else
105
- @record.attribute_was(attr_name.to_s)
101
+ # We are either performing a `record_destroy` or a
102
+ # `record_update(is_touch: true)`.
103
+ @record.attribute_in_database(attr_name.to_s)
106
104
  end
107
105
  end
108
106
 
109
107
  # @api private
110
- def changed_and_not_ignored
108
+ def calculated_ignored_array
111
109
  ignore = @record.paper_trail_options[:ignore].dup
112
110
  # Remove Hash arguments and then evaluate whether the attributes (the
113
111
  # keys of the hash) should also get pushed into the collection.
@@ -117,8 +115,12 @@ module PaperTrail
117
115
  ignore << attr if condition.respond_to?(:call) && condition.call(@record)
118
116
  }
119
117
  end
118
+ end
119
+
120
+ # @api private
121
+ def changed_and_not_ignored
120
122
  skip = @record.paper_trail_options[:skip]
121
- (changed_in_latest_version - ignore) - skip
123
+ (changed_in_latest_version - calculated_ignored_array) - skip
122
124
  end
123
125
 
124
126
  # @api private
@@ -127,19 +129,25 @@ module PaperTrail
127
129
  @changed_in_latest_version ||= changes_in_latest_version.keys
128
130
  end
129
131
 
130
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
131
- # https://github.com/paper-trail-gem/paper_trail/pull/899
132
+ # Memoized to reduce memory usage
132
133
  #
133
134
  # @api private
134
135
  def changes_in_latest_version
135
- # Memoized to reduce memory usage
136
- @changes_in_latest_version ||= begin
137
- if @in_after_callback && RAILS_GTE_5_1
138
- @record.saved_changes
139
- else
140
- @record.changes
141
- end
136
+ @changes_in_latest_version ||= load_changes_in_latest_version
137
+ end
138
+
139
+ # @api private
140
+ def evaluate_only
141
+ only = @record.paper_trail_options[:only].dup
142
+ # Remove Hash arguments and then evaluate whether the attributes (the
143
+ # keys of the hash) should also get pushed into the collection.
144
+ only.delete_if do |obj|
145
+ obj.is_a?(Hash) &&
146
+ obj.each { |attr, condition|
147
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
148
+ }
142
149
  end
150
+ only
143
151
  end
144
152
 
145
153
  # An attributed is "ignored" if it is listed in the `:ignore` option
@@ -148,10 +156,22 @@ module PaperTrail
148
156
  #
149
157
  # @api private
150
158
  def ignored_attr_has_changed?
151
- ignored = @record.paper_trail_options[:ignore] + @record.paper_trail_options[:skip]
159
+ ignored = calculated_ignored_array + @record.paper_trail_options[:skip]
152
160
  ignored.any? && (changed_in_latest_version & ignored).any?
153
161
  end
154
162
 
163
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
164
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
165
+ #
166
+ # @api private
167
+ def load_changes_in_latest_version
168
+ if @in_after_callback
169
+ @record.saved_changes
170
+ else
171
+ @record.changes
172
+ end
173
+ end
174
+
155
175
  # PT 10 has a new optional column, `item_subtype`
156
176
  #
157
177
  # @api private
@@ -175,7 +195,9 @@ module PaperTrail
175
195
  #
176
196
  # @api private
177
197
  def merge_metadata_from_controller_into(data)
178
- data.merge(PaperTrail.request.controller_info || {})
198
+ metadata = PaperTrail.request.controller_info || {}
199
+ metadata.keys.each { |k| assert_metadatum_key_is_permitted(k) }
200
+ data.merge(metadata)
179
201
  end
180
202
 
181
203
  # Updates `data` from the model's `meta` option.
@@ -183,6 +205,7 @@ module PaperTrail
183
205
  # @api private
184
206
  def merge_metadata_from_model_into(data)
185
207
  @record.paper_trail_options[:meta].each do |k, v|
208
+ assert_metadatum_key_is_permitted(k)
186
209
  data[k] = model_metadatum(v, data[:event])
187
210
  end
188
211
  end
@@ -196,24 +219,32 @@ module PaperTrail
196
219
  if value.respond_to?(:call)
197
220
  value.call(@record)
198
221
  elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
199
- # If it is an attribute that is changing in an existing object,
200
- # be sure to grab the current version.
201
- if event != "create" &&
202
- @record.has_attribute?(value) &&
203
- attribute_changed_in_latest_version?(value)
204
- attribute_in_previous_version(value, false)
205
- else
206
- @record.send(value)
207
- end
222
+ metadatum_from_model_method(event, value)
208
223
  else
209
224
  value
210
225
  end
211
226
  end
212
227
 
228
+ # The model method can either be an attribute or a non-attribute method.
229
+ #
230
+ # If it is an attribute that is changing in an existing object,
231
+ # be sure to grab the correct version.
232
+ #
233
+ # @api private
234
+ def metadatum_from_model_method(event, method)
235
+ if event != "create" &&
236
+ @record.has_attribute?(method) &&
237
+ attribute_changed_in_latest_version?(method)
238
+ attribute_in_previous_version(method, false)
239
+ else
240
+ @record.send(method)
241
+ end
242
+ end
243
+
213
244
  # @api private
214
245
  def notable_changes
215
246
  changes_in_latest_version.delete_if { |k, _v|
216
- !notably_changed.include?(k)
247
+ notably_changed.exclude?(k)
217
248
  }
218
249
  end
219
250
 
@@ -221,16 +252,9 @@ module PaperTrail
221
252
  def notably_changed
222
253
  # Memoized to reduce memory usage
223
254
  @notably_changed ||= begin
224
- only = @record.paper_trail_options[:only].dup
225
- # Remove Hash arguments and then evaluate whether the attributes (the
226
- # keys of the hash) should also get pushed into the collection.
227
- only.delete_if do |obj|
228
- obj.is_a?(Hash) &&
229
- obj.each { |attr, condition|
230
- only << attr if condition.respond_to?(:call) && condition.call(@record)
231
- }
232
- end
233
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
255
+ only = evaluate_only
256
+ cani = changed_and_not_ignored
257
+ only.empty? ? cani : (cani & only)
234
258
  end
235
259
  end
236
260
 
@@ -247,8 +271,7 @@ module PaperTrail
247
271
  # @api private
248
272
  def prepare_object_changes(changes)
249
273
  changes = serialize_object_changes(changes)
250
- changes = recordable_object_changes(changes)
251
- changes
274
+ recordable_object_changes(changes)
252
275
  end
253
276
 
254
277
  # Returns an object which can be assigned to the `object_changes`
@@ -260,7 +283,7 @@ module PaperTrail
260
283
  # @api private
261
284
  # @param changes HashWithIndifferentAccess
262
285
  def recordable_object_changes(changes)
263
- if PaperTrail.config.object_changes_adapter&.respond_to?(:diff)
286
+ if PaperTrail.config.object_changes_adapter.respond_to?(:diff)
264
287
  # We'd like to avoid the `to_hash` here, because it increases memory
265
288
  # usage, but that would be a breaking change because
266
289
  # `object_changes_adapter` expects a plain `Hash`, not a
@@ -35,7 +35,7 @@ module PaperTrail
35
35
  #
36
36
  # @override
37
37
  def changes_in_latest_version
38
- @record.attributes.map { |attr, value| [attr, [value, nil]] }.to_h
38
+ @record.attributes.transform_values { |value| [value, nil] }
39
39
  end
40
40
  end
41
41
  end