paper_trail 11.1.0 → 15.2.0

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