paper_trail 10.3.1 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
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