paper_trail 11.1.0 → 15.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/paper_trail/install/install_generator.rb +29 -5
  3. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +11 -6
  4. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +4 -2
  5. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  6. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +10 -10
  7. data/lib/paper_trail/attribute_serializers/object_attribute.rb +13 -3
  8. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +13 -3
  9. data/lib/paper_trail/compatibility.rb +3 -3
  10. data/lib/paper_trail/errors.rb +33 -0
  11. data/lib/paper_trail/events/base.rb +84 -64
  12. data/lib/paper_trail/events/destroy.rb +1 -1
  13. data/lib/paper_trail/events/update.rb +23 -7
  14. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  15. data/lib/paper_trail/frameworks/rails/controller.rb +0 -6
  16. data/lib/paper_trail/frameworks/rails/railtie.rb +34 -0
  17. data/lib/paper_trail/frameworks/rails.rb +1 -2
  18. data/lib/paper_trail/has_paper_trail.rb +4 -0
  19. data/lib/paper_trail/model_config.rb +51 -45
  20. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  21. data/lib/paper_trail/queries/versions/where_object.rb +1 -1
  22. data/lib/paper_trail/queries/versions/where_object_changes.rb +9 -14
  23. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  24. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  25. data/lib/paper_trail/record_trail.rb +81 -64
  26. data/lib/paper_trail/reifier.rb +27 -10
  27. data/lib/paper_trail/request.rb +22 -25
  28. data/lib/paper_trail/serializers/json.rb +0 -10
  29. data/lib/paper_trail/serializers/yaml.rb +38 -13
  30. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -14
  31. data/lib/paper_trail/version_concern.rb +74 -22
  32. data/lib/paper_trail/version_number.rb +2 -2
  33. data/lib/paper_trail.rb +30 -40
  34. metadata +98 -45
  35. data/Gemfile +0 -4
  36. data/lib/paper_trail/frameworks/rails/engine.rb +0 -45
  37. data/paper_trail.gemspec +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c312ab701b8ab7b26df37957b03b9f01b6ce6cbe0e5e25d4c478258f5bec1d6f
4
- data.tar.gz: 62e94f6c2fa657d4c24f0fe6ecdf5abf3c8ae785e7b25f88611fc18e6d4324a7
3
+ metadata.gz: '0972b7805c1dea218a2eb15d46e68c76650fe4d8752e00a64080c38360544ede'
4
+ data.tar.gz: f242c006d46c1af35e67dcbf667458356372fba3d90238d56d29989f257f5f34
5
5
  SHA512:
6
- metadata.gz: b59d91302dde736a2476240eadb9306938db2521594cf55de792c9678324762c4a1ffedfa5a53c3fc0fd9e37a87bc1b6ea4e74e68535b80fe70342d48a221532
7
- data.tar.gz: b4aba2c107556fd6fbf758d9373f147c9b9bb0d1e6059bb3e2ad28b77906e980c559bd3c888fe9734665182cbe294bf470e77527a5536e202b8f32a5b7993516
6
+ metadata.gz: 91e251f425f7389ffdd39e7793f6665f53dec8f1f967f0befbd00df0fd2aef423f7c074520b4f5f4410c3bfbca1f1c0beb2afe61c6d19825e1818209d36e8c45
7
+ data.tar.gz: f2eeac74231a4b51fd2e0b699c2591e0b3de1aad6f8d96bd8863375b37c78d96f6ca6161d7e552a856ee92821380d3f457f111322a686254a964117c908bf5b0
@@ -20,6 +20,12 @@ 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."
@@ -28,7 +34,9 @@ module PaperTrail
28
34
  add_paper_trail_migration(
29
35
  "create_versions",
30
36
  item_type_options: item_type_options,
31
- versions_table_options: versions_table_options
37
+ versions_table_options: versions_table_options,
38
+ item_id_type_options: item_id_type_options,
39
+ version_table_primary_key_type: version_table_primary_key_type
32
40
  )
33
41
  if options.with_changes?
34
42
  add_paper_trail_migration("add_object_changes_to_versions")
@@ -37,12 +45,28 @@ module PaperTrail
37
45
 
38
46
  private
39
47
 
48
+ # To use uuid instead of integer for primary key
49
+ def item_id_type_options
50
+ options.uuid? ? "string" : "bigint"
51
+ end
52
+
53
+ # To use uuid for version table primary key
54
+ def version_table_primary_key_type
55
+ if options.uuid?
56
+ ", id: :uuid"
57
+ else
58
+ ""
59
+ end
60
+ end
61
+
40
62
  # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
41
63
  # See https://github.com/paper-trail-gem/paper_trail/issues/651
42
64
  def item_type_options
43
- opt = { null: false }
44
- opt[:limit] = 191 if mysql?
45
- ", #{opt}"
65
+ if mysql?
66
+ ", null: false, limit: 191"
67
+ else
68
+ ", null: false"
69
+ end
46
70
  end
47
71
 
48
72
  def mysql?
@@ -66,7 +90,7 @@ module PaperTrail
66
90
  #
67
91
  def versions_table_options
68
92
  if mysql?
69
- ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }'
93
+ ', options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"'
70
94
  else
71
95
  ""
72
96
  end
@@ -9,12 +9,10 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
9
9
  TEXT_BYTES = 1_073_741_823
10
10
 
11
11
  def change
12
- create_table :versions<%= versions_table_options %> do |t|
13
- t.string :item_type<%= item_type_options %>
14
- t.bigint :item_id, null: false
15
- t.string :event, null: false
12
+ create_table :versions<%= versions_table_options %><%= version_table_primary_key_type %> do |t|
13
+ # Consider using bigint type for performance if you are going to store only numeric ids.
14
+ # t.bigint :whodunnit
16
15
  t.string :whodunnit
17
- t.text :object, limit: TEXT_BYTES
18
16
 
19
17
  # Known issue in MySQL: fractional second precision
20
18
  # -------------------------------------------------
@@ -29,8 +27,15 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
29
27
  # version of ActiveRecord with support for fractional seconds in MySQL.
30
28
  # (https://github.com/rails/rails/pull/14359)
31
29
  #
30
+ # MySQL users should use the following line for `created_at`
31
+ # t.datetime :created_at, limit: 6
32
32
  t.datetime :created_at
33
+
34
+ t.<%= item_id_type_options %> :item_id, null: false
35
+ t.string :item_type<%= item_type_options %>
36
+ t.string :event, null: false
37
+ t.text :object, limit: TEXT_BYTES
33
38
  end
34
- add_index :versions, %i(item_type item_id)
39
+ add_index :versions, %i[item_type item_id]
35
40
  end
36
41
  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,22 +27,25 @@ 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
-
35
- # Uses AR 5's `serialize` and `deserialize`.
36
- class CastAttributeSerializer
37
- def serialize(attr, val)
38
- AttributeSerializerFactory.for(@klass, attr).serialize(val)
39
- end
40
30
 
41
31
  def deserialize(attr, val)
42
32
  if defined_enums[attr] && val.is_a?(::String)
43
33
  # Because PT 4 used to save the string version of enums to `object_changes`
44
34
  val
35
+ elsif PaperTrail.active_record_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)
45
41
  else
46
42
  AttributeSerializerFactory.for(@klass, attr).deserialize(val)
47
43
  end
48
44
  end
45
+
46
+ def serialize(attr, val)
47
+ AttributeSerializerFactory.for(@klass, attr).serialize(val)
48
+ end
49
49
  end
50
50
  end
51
51
  end
@@ -8,6 +8,12 @@ module PaperTrail
8
8
  class ObjectAttribute
9
9
  def initialize(model_class)
10
10
  @model_class = model_class
11
+
12
+ # ActiveRecord since 7.0 has a built-in encryption mechanism
13
+ @encrypted_attributes =
14
+ if PaperTrail.active_record_gte_7_0?
15
+ @model_class.encrypted_attributes&.map(&:to_s)
16
+ end
11
17
  end
12
18
 
13
19
  def serialize(attributes)
@@ -23,14 +29,18 @@ module PaperTrail
23
29
  # Modifies `attributes` in place.
24
30
  # TODO: Return a new hash instead.
25
31
  def alter(attributes, serialization_method)
26
- # Don't serialize before values before inserting into columns of type
32
+ # Don't serialize non-encrypted before values before inserting into columns of type
27
33
  # `JSON` on `PostgreSQL` databases.
28
- return attributes if object_col_is_json?
34
+ attributes_to_serialize =
35
+ object_col_is_json? ? attributes.slice(*@encrypted_attributes) : attributes
36
+ return attributes if attributes_to_serialize.blank?
29
37
 
30
38
  serializer = CastAttributeSerializer.new(@model_class)
31
- attributes.each do |key, value|
39
+ attributes_to_serialize.each do |key, value|
32
40
  attributes[key] = serializer.send(serialization_method, key, value)
33
41
  end
42
+
43
+ attributes
34
44
  end
35
45
 
36
46
  def object_col_is_json?
@@ -8,6 +8,12 @@ module PaperTrail
8
8
  class ObjectChangesAttribute
9
9
  def initialize(item_class)
10
10
  @item_class = item_class
11
+
12
+ # ActiveRecord since 7.0 has a built-in encryption mechanism
13
+ @encrypted_attributes =
14
+ if PaperTrail.active_record_gte_7_0?
15
+ @item_class.encrypted_attributes&.map(&:to_s)
16
+ end
11
17
  end
12
18
 
13
19
  def serialize(changes)
@@ -23,17 +29,21 @@ module PaperTrail
23
29
  # Modifies `changes` in place.
24
30
  # TODO: Return a new hash instead.
25
31
  def alter(changes, serialization_method)
26
- # Don't serialize before values before inserting into columns of type
32
+ # Don't serialize non-encrypted before values before inserting into columns of type
27
33
  # `JSON` on `PostgreSQL` databases.
28
- return changes if object_changes_col_is_json?
34
+ changes_to_serialize =
35
+ object_changes_col_is_json? ? changes.slice(*@encrypted_attributes) : changes.clone
36
+ return changes if changes_to_serialize.blank?
29
37
 
30
38
  serializer = CastAttributeSerializer.new(@item_class)
31
- changes.clone.each do |key, change|
39
+ changes_to_serialize.each do |key, change|
32
40
  # `change` is an Array with two elements, representing before and after.
33
41
  changes[key] = Array(change).map do |value|
34
42
  serializer.send(serialization_method, key, value)
35
43
  end
36
44
  end
45
+
46
+ changes
37
47
  end
38
48
 
39
49
  def object_changes_col_is_json?
@@ -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 = ">= 5.2" # enforced in gemspec
21
- ACTIVERECORD_LT = "< 6.2" # not enforced in gemspec
20
+ ACTIVERECORD_GTE = ">= 6.1" # enforced in gemspec
21
+ ACTIVERECORD_LT = "< 7.3" # 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
@@ -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(&block)
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, &block)
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,18 +93,14 @@ 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
 
@@ -131,19 +129,25 @@ module PaperTrail
131
129
  @changed_in_latest_version ||= changes_in_latest_version.keys
132
130
  end
133
131
 
134
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
135
- # https://github.com/paper-trail-gem/paper_trail/pull/899
132
+ # Memoized to reduce memory usage
136
133
  #
137
134
  # @api private
138
135
  def changes_in_latest_version
139
- # Memoized to reduce memory usage
140
- @changes_in_latest_version ||= begin
141
- if @in_after_callback && RAILS_GTE_5_1
142
- @record.saved_changes
143
- else
144
- @record.changes
145
- 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
+ }
146
149
  end
150
+ only
147
151
  end
148
152
 
149
153
  # An attributed is "ignored" if it is listed in the `:ignore` option
@@ -156,6 +160,18 @@ module PaperTrail
156
160
  ignored.any? && (changed_in_latest_version & ignored).any?
157
161
  end
158
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
+
159
175
  # PT 10 has a new optional column, `item_subtype`
160
176
  #
161
177
  # @api private
@@ -179,7 +195,9 @@ module PaperTrail
179
195
  #
180
196
  # @api private
181
197
  def merge_metadata_from_controller_into(data)
182
- 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)
183
201
  end
184
202
 
185
203
  # Updates `data` from the model's `meta` option.
@@ -187,6 +205,7 @@ module PaperTrail
187
205
  # @api private
188
206
  def merge_metadata_from_model_into(data)
189
207
  @record.paper_trail_options[:meta].each do |k, v|
208
+ assert_metadatum_key_is_permitted(k)
190
209
  data[k] = model_metadatum(v, data[:event])
191
210
  end
192
211
  end
@@ -200,24 +219,32 @@ module PaperTrail
200
219
  if value.respond_to?(:call)
201
220
  value.call(@record)
202
221
  elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
203
- # If it is an attribute that is changing in an existing object,
204
- # be sure to grab the current version.
205
- if event != "create" &&
206
- @record.has_attribute?(value) &&
207
- attribute_changed_in_latest_version?(value)
208
- attribute_in_previous_version(value, false)
209
- else
210
- @record.send(value)
211
- end
222
+ metadatum_from_model_method(event, value)
212
223
  else
213
224
  value
214
225
  end
215
226
  end
216
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
+
217
244
  # @api private
218
245
  def notable_changes
219
246
  changes_in_latest_version.delete_if { |k, _v|
220
- !notably_changed.include?(k)
247
+ notably_changed.exclude?(k)
221
248
  }
222
249
  end
223
250
 
@@ -225,16 +252,9 @@ module PaperTrail
225
252
  def notably_changed
226
253
  # Memoized to reduce memory usage
227
254
  @notably_changed ||= begin
228
- only = @record.paper_trail_options[:only].dup
229
- # Remove Hash arguments and then evaluate whether the attributes (the
230
- # keys of the hash) should also get pushed into the collection.
231
- only.delete_if do |obj|
232
- obj.is_a?(Hash) &&
233
- obj.each { |attr, condition|
234
- only << attr if condition.respond_to?(:call) && condition.call(@record)
235
- }
236
- end
237
- 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)
238
258
  end
239
259
  end
240
260
 
@@ -263,7 +283,7 @@ module PaperTrail
263
283
  # @api private
264
284
  # @param changes HashWithIndifferentAccess
265
285
  def recordable_object_changes(changes)
266
- if PaperTrail.config.object_changes_adapter&.respond_to?(:diff)
286
+ if PaperTrail.config.object_changes_adapter.respond_to?(:diff)
267
287
  # We'd like to avoid the `to_hash` here, because it increases memory
268
288
  # usage, but that would be a breaking change because
269
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
@@ -29,22 +29,38 @@ module PaperTrail
29
29
  event: @record.paper_trail_event || "update",
30
30
  whodunnit: PaperTrail.request.whodunnit
31
31
  }
32
- if @record.respond_to?(:updated_at)
33
- data[:created_at] = @record.updated_at
34
- end
35
32
  if record_object?
36
33
  data[:object] = recordable_object(@is_touch)
37
34
  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
35
+ merge_object_changes_into(data)
42
36
  merge_item_subtype_into(data)
43
37
  merge_metadata_into(data)
44
38
  end
45
39
 
40
+ # If it is a touch event, and changed are empty, it is assumed to be
41
+ # implicit `touch` mutation, and will a version is created.
42
+ #
43
+ # See https://github.com/rails/rails/commit/dcb825902d79d0f6baba956f7c6ec5767611353e
44
+ #
45
+ # @api private
46
+ def changed_notably?
47
+ if @is_touch && changes_in_latest_version.empty?
48
+ true
49
+ else
50
+ super
51
+ end
52
+ end
53
+
46
54
  private
47
55
 
56
+ # @api private
57
+ def merge_object_changes_into(data)
58
+ if record_object_changes?
59
+ changes = @force_changes.nil? ? notable_changes : @force_changes
60
+ data[:object_changes] = prepare_object_changes(changes)
61
+ end
62
+ end
63
+
48
64
  # `touch` cannot record `object_changes` because rails' `touch` does not
49
65
  # perform dirty-tracking. Specifically, methods from `Dirty`, like
50
66
  # `saved_changes`, return the same values before and after `touch`.
@@ -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
@@ -101,9 +101,3 @@ module PaperTrail
101
101
  end
102
102
  end
103
103
  end
104
-
105
- if defined?(::ActionController)
106
- ::ActiveSupport.on_load(:action_controller) do
107
- include ::PaperTrail::Rails::Controller
108
- end
109
- end