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
@@ -7,8 +7,6 @@ require "paper_trail/events/update"
7
7
  module PaperTrail
8
8
  # Represents the "paper trail" for a single record.
9
9
  class RecordTrail
10
- RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
11
-
12
10
  def initialize(record)
13
11
  @record = record
14
12
  end
@@ -27,14 +25,6 @@ module PaperTrail
27
25
  @record.send("#{@record.class.version_association_name}=", nil)
28
26
  end
29
27
 
30
- # Is PT enabled for this particular record?
31
- # @api private
32
- def enabled?
33
- PaperTrail.enabled? &&
34
- PaperTrail.request.enabled? &&
35
- PaperTrail.request.enabled_for_model?(@record.class)
36
- end
37
-
38
28
  # Returns true if this instance is the current, live one;
39
29
  # returns false if this instance came from a previous version.
40
30
  def live?
@@ -77,13 +67,6 @@ module PaperTrail
77
67
  end
78
68
  end
79
69
 
80
- # PT-AT extends this method to add its transaction id.
81
- #
82
- # @api private
83
- def data_for_create
84
- {}
85
- end
86
-
87
70
  # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
88
71
  #
89
72
  # @api private
@@ -107,14 +90,12 @@ module PaperTrail
107
90
  end
108
91
  end
109
92
 
110
- # PT-AT extends this method to add its transaction id.
111
- #
112
- # @api private
113
- def data_for_destroy
114
- {}
115
- end
116
-
117
93
  # @api private
94
+ # @param force [boolean] Insert a `Version` even if `@record` has not
95
+ # `changed_notably?`.
96
+ # @param in_after_callback [boolean] True when called from an `after_update`
97
+ # or `after_touch` callback.
98
+ # @param is_touch [boolean] True when called from an `after_touch` callback.
118
99
  # @return - The created version object, so that plugins can use it, e.g.
119
100
  # paper_trail-association_tracking
120
101
  def record_update(force:, in_after_callback:, is_touch:)
@@ -138,40 +119,6 @@ module PaperTrail
138
119
  end
139
120
  end
140
121
 
141
- # PT-AT extends this method to add its transaction id.
142
- #
143
- # @api private
144
- def data_for_update
145
- {}
146
- end
147
-
148
- # @api private
149
- # @return - The created version object, so that plugins can use it, e.g.
150
- # paper_trail-association_tracking
151
- def record_update_columns(changes)
152
- return unless enabled?
153
- event = Events::Update.new(@record, false, false, changes)
154
-
155
- # Merge data from `Event` with data from PT-AT. We no longer use
156
- # `data_for_update_columns` but PT-AT still does.
157
- data = event.data.merge(data_for_update_columns)
158
-
159
- versions_assoc = @record.send(@record.class.versions_association_name)
160
- version = versions_assoc.create(data)
161
- if version.errors.any?
162
- log_version_errors(version, :update)
163
- else
164
- version
165
- end
166
- end
167
-
168
- # PT-AT extends this method to add its transaction id.
169
- #
170
- # @api private
171
- def data_for_update_columns
172
- {}
173
- end
174
-
175
122
  # Invoked via callback when a user attempts to persist a reified
176
123
  # `Version`.
177
124
  def reset_timestamp_attrs_for_update_if_needed
@@ -196,15 +143,17 @@ module PaperTrail
196
143
  # Save, and create a version record regardless of options such as `:on`,
197
144
  # `:if`, or `:unless`.
198
145
  #
199
- # Arguments are passed to `save`.
146
+ # `in_after_callback`: Indicates if this method is being called within an
147
+ # `after` callback. Defaults to `false`.
148
+ # `options`: Optional arguments passed to `save`.
200
149
  #
201
150
  # This is an "update" event. That is, we record the same data we would in
202
151
  # the case of a normal AR `update`.
203
- def save_with_version(*args)
152
+ def save_with_version(in_after_callback: false, **options)
204
153
  ::PaperTrail.request(enabled: false) do
205
- @record.save(*args)
154
+ @record.save(**options)
206
155
  end
207
- record_update(force: true, in_after_callback: false, is_touch: false)
156
+ record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
208
157
  end
209
158
 
210
159
  # Like the `update_column` method from `ActiveRecord::Persistence`, but also
@@ -269,11 +218,22 @@ module PaperTrail
269
218
  def build_version_on_update(force:, in_after_callback:, is_touch:)
270
219
  event = Events::Update.new(@record, in_after_callback, is_touch, nil)
271
220
  return unless force || event.changed_notably?
221
+ data = event.data
222
+
223
+ # Copy the (recently set) `updated_at` from the record to the `created_at`
224
+ # of the `Version`. Without this feature, these two timestamps would
225
+ # differ by a few milliseconds. To some people, it seems a little
226
+ # unnatural to tamper with creation timestamps in this way. But, this
227
+ # feature has existed for a long time, almost a decade now, and some users
228
+ # may rely on it now.
229
+ if @record.respond_to?(:updated_at)
230
+ data[:created_at] = @record.updated_at
231
+ end
272
232
 
273
233
  # Merge data from `Event` with data from PT-AT. We no longer use
274
234
  # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
275
235
  # instead of `merge`.
276
- data = event.data.merge!(data_for_update)
236
+ data.merge!(data_for_update)
277
237
 
278
238
  # Using `version_class.new` reduces memory usage compared to
279
239
  # `versions_assoc.build`. It's a trade-off though. We have to clear
@@ -282,13 +242,69 @@ module PaperTrail
282
242
  @record.class.paper_trail.version_class.new(data)
283
243
  end
284
244
 
245
+ # PT-AT extends this method to add its transaction id.
246
+ #
247
+ # @api public
248
+ def data_for_create
249
+ {}
250
+ end
251
+
252
+ # PT-AT extends this method to add its transaction id.
253
+ #
254
+ # @api public
255
+ def data_for_destroy
256
+ {}
257
+ end
258
+
259
+ # PT-AT extends this method to add its transaction id.
260
+ #
261
+ # @api public
262
+ def data_for_update
263
+ {}
264
+ end
265
+
266
+ # PT-AT extends this method to add its transaction id.
267
+ #
268
+ # @api public
269
+ def data_for_update_columns
270
+ {}
271
+ end
272
+
273
+ # Is PT enabled for this particular record?
274
+ # @api private
275
+ def enabled?
276
+ PaperTrail.enabled? &&
277
+ PaperTrail.request.enabled? &&
278
+ PaperTrail.request.enabled_for_model?(@record.class)
279
+ end
280
+
285
281
  def log_version_errors(version, action)
286
282
  version.logger&.warn(
287
283
  "Unable to create version for #{action} of #{@record.class.name}" \
288
- "##{@record.id}: " + version.errors.full_messages.join(", ")
284
+ "##{@record.id}: " + version.errors.full_messages.join(", ")
289
285
  )
290
286
  end
291
287
 
288
+ # @api private
289
+ # @return - The created version object, so that plugins can use it, e.g.
290
+ # paper_trail-association_tracking
291
+ def record_update_columns(changes)
292
+ return unless enabled?
293
+ data = Events::Update.new(@record, false, false, changes).data
294
+
295
+ # Merge data from `Event` with data from PT-AT. We no longer use
296
+ # `data_for_update_columns` but PT-AT still does.
297
+ data.merge!(data_for_update_columns)
298
+
299
+ versions_assoc = @record.send(@record.class.versions_association_name)
300
+ version = versions_assoc.create(data)
301
+ if version.errors.any?
302
+ log_version_errors(version, :update)
303
+ else
304
+ version
305
+ end
306
+ end
307
+
292
308
  def version
293
309
  @record.public_send(@record.class.version_association_name)
294
310
  end
@@ -52,26 +52,29 @@ module PaperTrail
52
52
  # not the actual subclass. If `type` is present but empty, the class is
53
53
  # the base class.
54
54
  def init_model(attrs, options, version)
55
- if options[:dup] != true && version.item
56
- model = version.item
57
- if options[:unversioned_attributes] == :nil
58
- init_unversioned_attrs(attrs, model)
59
- end
60
- else
61
- klass = version_reification_class(version, attrs)
62
- # The `dup` option always returns a new object, otherwise we should
63
- # attempt to look for the item outside of default scope(s).
64
- find_cond = { klass.primary_key => version.item_id }
65
- if options[:dup] || (item_found = klass.unscoped.where(find_cond).first).nil?
66
- model = klass.new
67
- elsif options[:unversioned_attributes] == :nil
68
- model = item_found
69
- init_unversioned_attrs(attrs, model)
70
- end
55
+ klass = version_reification_class(version, attrs)
56
+
57
+ # The `dup` option and destroyed version always returns a new object,
58
+ # otherwise we should attempt to load item or to look for the item
59
+ # outside of default scope(s).
60
+ model = if options[:dup] == true || version.event == "destroy"
61
+ klass.new
62
+ else
63
+ version.item || init_model_by_finding_item_id(klass, version) || klass.new
64
+ end
65
+
66
+ if options[:unversioned_attributes] == :nil && !model.new_record?
67
+ init_unversioned_attrs(attrs, model)
71
68
  end
69
+
72
70
  model
73
71
  end
74
72
 
73
+ # @api private
74
+ def init_model_by_finding_item_id(klass, version)
75
+ klass.unscoped.where(klass.primary_key => version.item_id).first
76
+ end
77
+
75
78
  # Look for attributes that exist in `model` and not in this version.
76
79
  # These attributes should be set to nil. Modifies `attrs`.
77
80
  # @api private
@@ -88,9 +91,7 @@ module PaperTrail
88
91
  #
89
92
  # @api private
90
93
  def reify_attribute(k, v, model, version)
91
- enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
92
- is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
93
- if model.has_attribute?(k) && !is_enum_without_type_caster
94
+ if model.has_attribute?(k)
94
95
  model[k.to_sym] = v
95
96
  elsif model.respond_to?("#{k}=")
96
97
  model.send("#{k}=", v)
@@ -111,21 +112,35 @@ module PaperTrail
111
112
  end
112
113
 
113
114
  # Given a `version`, return the class to reify. This method supports
114
- # Single Table Inheritance (STI) with custom inheritance columns.
115
+ # Single Table Inheritance (STI) with custom inheritance columns and
116
+ # custom inheritance column values.
115
117
  #
116
118
  # For example, imagine a `version` whose `item_type` is "Animal". The
117
119
  # `animals` table is an STI table (it has cats and dogs) and it has a
118
120
  # custom inheritance column, `species`. If `attrs["species"]` is "Dog",
119
121
  # this method returns the constant `Dog`. If `attrs["species"]` is blank,
120
- # this method returns the constant `Animal`. You can see this particular
121
- # example in action in `spec/models/animal_spec.rb`.
122
+ # this method returns the constant `Animal`.
123
+ #
124
+ # The values contained in the inheritance columns may be non-camelized
125
+ # strings (e.g. 'dog' instead of 'Dog'). To reify classes in this case
126
+ # we need to call the parents class `sti_class_for` method to retrieve
127
+ # the correct record class.
122
128
  #
123
- # TODO: Duplication: similar `constantize` in VersionConcern#version_limit
129
+ # You can see these particular examples in action in
130
+ # `spec/models/animal_spec.rb` and `spec/models/plant_spec.rb`
124
131
  def version_reification_class(version, attrs)
125
- inheritance_column_name = version.item_type.constantize.inheritance_column
132
+ clazz = version.item_type.constantize
133
+ inheritance_column_name = clazz.inheritance_column
126
134
  inher_col_value = attrs[inheritance_column_name]
127
- class_name = inher_col_value.blank? ? version.item_type : inher_col_value
128
- class_name.constantize
135
+ return clazz if inher_col_value.blank?
136
+
137
+ # Rails 6.1 adds a public method for clients to use to customize STI classes. If that
138
+ # method is not available, fall back to using the private one
139
+ if clazz.public_methods.include?(:sti_class_for)
140
+ return clazz.sti_class_for(inher_col_value)
141
+ end
142
+
143
+ clazz.send(:find_sti_class, inher_col_value)
129
144
  end
130
145
  end
131
146
  end
@@ -12,9 +12,6 @@ module PaperTrail
12
12
  #
13
13
  # @api private
14
14
  module Request
15
- class InvalidOption < RuntimeError
16
- end
17
-
18
15
  class << self
19
16
  # Sets any data from the controller that you want PaperTrail to store.
20
17
  # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
@@ -78,28 +75,6 @@ module PaperTrail
78
75
  !!store.fetch(:"enabled_for_#{model}", true)
79
76
  end
80
77
 
81
- # @api private
82
- def merge(options)
83
- options.to_h.each do |k, v|
84
- store[k] = v
85
- end
86
- end
87
-
88
- # @api private
89
- def set(options)
90
- store.clear
91
- merge(options)
92
- end
93
-
94
- # Returns a deep copy of the internal hash from our RequestStore. Keys are
95
- # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
96
- # We cannot use Marshal.dump here because it doesn't support Proc. It is
97
- # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
98
- # @api private
99
- def to_h
100
- store.deep_dup
101
- end
102
-
103
78
  # Temporarily set `options` and execute a block.
104
79
  # @api private
105
80
  def with(options)
@@ -136,6 +111,19 @@ module PaperTrail
136
111
 
137
112
  private
138
113
 
114
+ # @api private
115
+ def merge(options)
116
+ options.to_h.each do |k, v|
117
+ store[k] = v
118
+ end
119
+ end
120
+
121
+ # @api private
122
+ def set(options)
123
+ store.clear
124
+ merge(options)
125
+ end
126
+
139
127
  # Returns a Hash, initializing with default values if necessary.
140
128
  # @api private
141
129
  def store
@@ -144,6 +132,15 @@ module PaperTrail
144
132
  }
145
133
  end
146
134
 
135
+ # Returns a deep copy of the internal hash from our RequestStore. Keys are
136
+ # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
137
+ # We cannot use Marshal.dump here because it doesn't support Proc. It is
138
+ # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
139
+ # @api private
140
+ def to_h
141
+ store.deep_dup
142
+ end
143
+
147
144
  # Provide a helpful error message if someone has a typo in one of their
148
145
  # option keys. We don't validate option values here. That's traditionally
149
146
  # been handled with casting (`to_s`, `!!`) in the accessor method.
@@ -31,16 +31,6 @@ module PaperTrail
31
31
  arel_field.matches("%\"#{field}\":#{json_value}%")
32
32
  end
33
33
  end
34
-
35
- def where_object_changes_condition(*)
36
- raise <<-STR.squish.freeze
37
- where_object_changes no longer supports reading JSON from a text
38
- column. The old implementation was inaccurate, returning more records
39
- than you wanted. This feature was deprecated in 7.1.0 and removed in
40
- 8.0.0. The json and jsonb datatypes are still supported. See the
41
- discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
42
- STR
43
- end
44
34
  end
45
35
  end
46
36
  end
@@ -9,13 +9,23 @@ module PaperTrail
9
9
  extend self # makes all instance methods become module methods as well
10
10
 
11
11
  def load(string)
12
- ::YAML.load string
12
+ if use_safe_load?
13
+ ::YAML.safe_load(
14
+ string,
15
+ permitted_classes: yaml_column_permitted_classes,
16
+ aliases: true
17
+ )
18
+ elsif ::YAML.respond_to?(:unsafe_load)
19
+ ::YAML.unsafe_load(string)
20
+ else
21
+ ::YAML.load(string)
22
+ end
13
23
  end
14
24
 
15
25
  # @param object (Hash | HashWithIndifferentAccess) - Coming from
16
26
  # `recordable_object` `object` will be a plain `Hash`. However, due to
17
- # recent [memory optimizations](https://git.io/fjeYv), when coming from
18
- # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
27
+ # recent [memory optimizations](https://github.com/paper-trail-gem/paper_trail/pull/1189),
28
+ # when coming from `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
19
29
  def dump(object)
20
30
  object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
21
31
  ::YAML.dump object
@@ -27,16 +37,31 @@ module PaperTrail
27
37
  arel_field.matches("%\n#{field}: #{value}\n%")
28
38
  end
29
39
 
30
- # Returns a SQL LIKE condition to be used to match the given field and
31
- # value in the serialized `object_changes`.
32
- def where_object_changes_condition(*)
33
- raise <<-STR.squish.freeze
34
- where_object_changes no longer supports reading YAML from a text
35
- column. The old implementation was inaccurate, returning more records
36
- than you wanted. This feature was deprecated in 8.1.0 and removed in
37
- 9.0.0. The json and jsonb datatypes are still supported. See
38
- discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
39
- STR
40
+ private
41
+
42
+ def use_safe_load?
43
+ if ::ActiveRecord.gem_version >= Gem::Version.new("7.0.3.1")
44
+ # `use_yaml_unsafe_load` may be removed in the future, at which point
45
+ # safe loading will be the default.
46
+ !defined?(ActiveRecord.use_yaml_unsafe_load) || !ActiveRecord.use_yaml_unsafe_load
47
+ elsif defined?(ActiveRecord::Base.use_yaml_unsafe_load)
48
+ # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
49
+ !ActiveRecord::Base.use_yaml_unsafe_load
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ def yaml_column_permitted_classes
56
+ if defined?(ActiveRecord.yaml_column_permitted_classes)
57
+ # Rails >= 7.0.3.1
58
+ ActiveRecord.yaml_column_permitted_classes
59
+ elsif defined?(ActiveRecord::Base.yaml_column_permitted_classes)
60
+ # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
61
+ ActiveRecord::Base.yaml_column_permitted_classes
62
+ else
63
+ []
64
+ end
40
65
  end
41
66
  end
42
67
  end
@@ -11,15 +11,12 @@ module PaperTrail
11
11
  end
12
12
 
13
13
  def serialize(array)
14
- return serialize_with_ar(array) if active_record_pre_502?
15
14
  array
16
15
  end
17
16
 
18
17
  def deserialize(array)
19
- return deserialize_with_ar(array) if active_record_pre_502?
20
-
21
18
  case array
22
- # Needed for legacy reasons. If serialized array is a string
19
+ # Needed for legacy data. If serialized array is a string
23
20
  # then it was serialized with Rails < 5.0.2.
24
21
  when ::String then deserialize_with_ar(array)
25
22
  else array
@@ -28,16 +25,6 @@ module PaperTrail
28
25
 
29
26
  private
30
27
 
31
- def active_record_pre_502?
32
- ::ActiveRecord.gem_version < Gem::Version.new("5.0.2")
33
- end
34
-
35
- def serialize_with_ar(array)
36
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
37
- new(@subtype, @delimiter).
38
- serialize(array)
39
- end
40
-
41
28
  def deserialize_with_ar(array)
42
29
  ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
43
30
  new(@subtype, @delimiter).