paper_trail 10.3.1 → 13.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/lib/generators/paper_trail/install/install_generator.rb +25 -7
- data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +4 -2
- data/lib/generators/paper_trail/migration_generator.rb +5 -4
- data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +4 -2
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +14 -46
- data/lib/paper_trail/compatibility.rb +3 -3
- data/lib/paper_trail/config.rb +0 -33
- data/lib/paper_trail/errors.rb +33 -0
- data/lib/paper_trail/events/base.rb +68 -68
- data/lib/paper_trail/events/destroy.rb +1 -1
- data/lib/paper_trail/events/update.rb +23 -4
- data/lib/paper_trail/frameworks/active_record.rb +9 -2
- data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
- data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
- data/lib/paper_trail/frameworks/rails.rb +1 -2
- data/lib/paper_trail/has_paper_trail.rb +1 -1
- data/lib/paper_trail/model_config.rb +49 -46
- data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
- data/lib/paper_trail/queries/versions/where_object.rb +1 -1
- data/lib/paper_trail/queries/versions/where_object_changes.rb +8 -13
- data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
- data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
- data/lib/paper_trail/record_trail.rb +7 -7
- data/lib/paper_trail/reifier.rb +41 -26
- data/lib/paper_trail/request.rb +0 -3
- data/lib/paper_trail/serializers/json.rb +0 -10
- data/lib/paper_trail/serializers/yaml.rb +19 -13
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -14
- data/lib/paper_trail/version_concern.rb +86 -41
- data/lib/paper_trail/version_number.rb +3 -3
- data/lib/paper_trail.rb +17 -40
- metadata +104 -43
- data/lib/paper_trail/frameworks/rails/engine.rb +0 -45
@@ -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
|
-
|
12
|
+
if use_safe_load?
|
13
|
+
::YAML.safe_load(
|
14
|
+
string,
|
15
|
+
permitted_classes: ::ActiveRecord.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://
|
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,12 @@ module PaperTrail
|
|
27
37
|
arel_field.matches("%\n#{field}: #{value}\n%")
|
28
38
|
end
|
29
39
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
+
# `use_yaml_unsafe_load` was added in 7.0.3.1, will be removed in 7.1.0?
|
43
|
+
def use_safe_load?
|
44
|
+
defined?(ActiveRecord.use_yaml_unsafe_load) &&
|
45
|
+
!ActiveRecord.use_yaml_unsafe_load
|
40
46
|
end
|
41
47
|
end
|
42
48
|
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
|
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).
|
@@ -1,8 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "paper_trail/attribute_serializers/object_changes_attribute"
|
4
|
+
require "paper_trail/queries/versions/where_attribute_changes"
|
4
5
|
require "paper_trail/queries/versions/where_object"
|
5
6
|
require "paper_trail/queries/versions/where_object_changes"
|
7
|
+
require "paper_trail/queries/versions/where_object_changes_from"
|
8
|
+
require "paper_trail/queries/versions/where_object_changes_to"
|
6
9
|
|
7
10
|
module PaperTrail
|
8
11
|
# Originally, PaperTrail did not provide this module, and all of this
|
@@ -12,23 +15,20 @@ module PaperTrail
|
|
12
15
|
module VersionConcern
|
13
16
|
extend ::ActiveSupport::Concern
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
18
|
+
E_YAML_PERMITTED_CLASSES = <<-EOS.squish.freeze
|
19
|
+
PaperTrail encountered a Psych::DisallowedClass error during
|
20
|
+
deserialization of YAML column, indicating that
|
21
|
+
yaml_column_permitted_classes has not been configured correctly. %s
|
22
|
+
EOS
|
21
23
|
|
24
|
+
included do
|
25
|
+
belongs_to :item, polymorphic: true, optional: true, inverse_of: false
|
22
26
|
validates_presence_of :event
|
23
27
|
after_create :enforce_version_limit!
|
24
28
|
end
|
25
29
|
|
26
30
|
# :nodoc:
|
27
31
|
module ClassMethods
|
28
|
-
def item_subtype_column_present?
|
29
|
-
column_names.include?("item_subtype")
|
30
|
-
end
|
31
|
-
|
32
32
|
def with_item_keys(item_type, item_id)
|
33
33
|
where item_type: item_type, item_id: item_id
|
34
34
|
end
|
@@ -46,7 +46,7 @@ module PaperTrail
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def not_creates
|
49
|
-
where
|
49
|
+
where.not(event: "create")
|
50
50
|
end
|
51
51
|
|
52
52
|
def between(start_time, end_time)
|
@@ -64,6 +64,18 @@ module PaperTrail
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
# Given an attribute like `"name"`, query the `versions.object_changes`
|
68
|
+
# column for any changes that modified the provided attribute.
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def where_attribute_changes(attribute)
|
72
|
+
unless attribute.is_a?(String) || attribute.is_a?(Symbol)
|
73
|
+
raise ArgumentError, "expected to receive a String or Symbol"
|
74
|
+
end
|
75
|
+
|
76
|
+
Queries::Versions::WhereAttributeChanges.new(self, attribute).execute
|
77
|
+
end
|
78
|
+
|
67
79
|
# Given a hash of attributes like `name: 'Joan'`, query the
|
68
80
|
# `versions.objects` column.
|
69
81
|
#
|
@@ -120,6 +132,36 @@ module PaperTrail
|
|
120
132
|
Queries::Versions::WhereObjectChanges.new(self, args).execute
|
121
133
|
end
|
122
134
|
|
135
|
+
# Given a hash of attributes like `name: 'Joan'`, query the
|
136
|
+
# `versions.objects_changes` column for changes where the version changed
|
137
|
+
# from the hash of attributes to other values.
|
138
|
+
#
|
139
|
+
# This is useful for finding versions where the attribute started with a
|
140
|
+
# known value and changed to something else. This is in comparison to
|
141
|
+
# `where_object_changes` which will find both the changes before and
|
142
|
+
# after.
|
143
|
+
#
|
144
|
+
# @api public
|
145
|
+
def where_object_changes_from(args = {})
|
146
|
+
raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
|
147
|
+
Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
|
148
|
+
end
|
149
|
+
|
150
|
+
# Given a hash of attributes like `name: 'Joan'`, query the
|
151
|
+
# `versions.objects_changes` column for changes where the version changed
|
152
|
+
# to the hash of attributes from other values.
|
153
|
+
#
|
154
|
+
# This is useful for finding versions where the attribute started with an
|
155
|
+
# unknown value and changed to a known value. This is in comparison to
|
156
|
+
# `where_object_changes` which will find both the changes before and
|
157
|
+
# after.
|
158
|
+
#
|
159
|
+
# @api public
|
160
|
+
def where_object_changes_to(args = {})
|
161
|
+
raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
|
162
|
+
Queries::Versions::WhereObjectChangesTo.new(self, args).execute
|
163
|
+
end
|
164
|
+
|
123
165
|
def primary_key_is_int?
|
124
166
|
@primary_key_is_int ||= columns_hash[primary_key].type == :integer
|
125
167
|
rescue StandardError # TODO: Rescue something more specific
|
@@ -145,6 +187,7 @@ module PaperTrail
|
|
145
187
|
# Default: false.
|
146
188
|
# @return `ActiveRecord::Relation`
|
147
189
|
# @api public
|
190
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
148
191
|
def preceding(obj, timestamp_arg = false)
|
149
192
|
if timestamp_arg != true && primary_key_is_int?
|
150
193
|
preceding_by_id(obj)
|
@@ -152,6 +195,7 @@ module PaperTrail
|
|
152
195
|
preceding_by_timestamp(obj)
|
153
196
|
end
|
154
197
|
end
|
198
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
155
199
|
|
156
200
|
# Returns versions after `obj`.
|
157
201
|
#
|
@@ -160,6 +204,7 @@ module PaperTrail
|
|
160
204
|
# Default: false.
|
161
205
|
# @return `ActiveRecord::Relation`
|
162
206
|
# @api public
|
207
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
163
208
|
def subsequent(obj, timestamp_arg = false)
|
164
209
|
if timestamp_arg != true && primary_key_is_int?
|
165
210
|
subsequent_by_id(obj)
|
@@ -167,6 +212,7 @@ module PaperTrail
|
|
167
212
|
subsequent_by_timestamp(obj)
|
168
213
|
end
|
169
214
|
end
|
215
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
170
216
|
|
171
217
|
private
|
172
218
|
|
@@ -205,18 +251,8 @@ module PaperTrail
|
|
205
251
|
|
206
252
|
# Restore the item from this version.
|
207
253
|
#
|
208
|
-
# Optionally this can also restore all :has_one and :has_many (including
|
209
|
-
# has_many :through) associations as they were "at the time", if they are
|
210
|
-
# also being versioned by PaperTrail.
|
211
|
-
#
|
212
254
|
# Options:
|
213
255
|
#
|
214
|
-
# - :has_one
|
215
|
-
# - `true` - Also reify has_one associations.
|
216
|
-
# - `false - Default.
|
217
|
-
# - :has_many
|
218
|
-
# - `true` - Also reify has_many and has_many :through associations.
|
219
|
-
# - `false` - Default.
|
220
256
|
# - :mark_for_destruction
|
221
257
|
# - `true` - Mark the has_one/has_many associations that did not exist in
|
222
258
|
# the reified version for destruction, instead of removing them.
|
@@ -232,7 +268,7 @@ module PaperTrail
|
|
232
268
|
#
|
233
269
|
def reify(options = {})
|
234
270
|
unless self.class.column_names.include? "object"
|
235
|
-
raise "reify
|
271
|
+
raise Error, "reify requires an object column"
|
236
272
|
end
|
237
273
|
return nil if object.nil?
|
238
274
|
::PaperTrail::Reifier.reify(self, options)
|
@@ -258,13 +294,6 @@ module PaperTrail
|
|
258
294
|
end
|
259
295
|
alias version_author terminator
|
260
296
|
|
261
|
-
def sibling_versions(reload = false)
|
262
|
-
if reload || !defined?(@sibling_versions) || @sibling_versions.nil?
|
263
|
-
@sibling_versions = self.class.with_item_keys(item_type, item_id)
|
264
|
-
end
|
265
|
-
@sibling_versions
|
266
|
-
end
|
267
|
-
|
268
297
|
def next
|
269
298
|
@next ||= sibling_versions.subsequent(self).first
|
270
299
|
end
|
@@ -274,8 +303,9 @@ module PaperTrail
|
|
274
303
|
end
|
275
304
|
|
276
305
|
# Returns an integer representing the chronological position of the
|
277
|
-
# version among its siblings
|
278
|
-
#
|
306
|
+
# version among its siblings. The "create" event, for example, has an index
|
307
|
+
# of 0.
|
308
|
+
#
|
279
309
|
# @api public
|
280
310
|
def index
|
281
311
|
@index ||= RecordHistory.new(sibling_versions, self.class).index(self)
|
@@ -285,7 +315,7 @@ module PaperTrail
|
|
285
315
|
|
286
316
|
# @api private
|
287
317
|
def load_changeset
|
288
|
-
if PaperTrail.config.object_changes_adapter
|
318
|
+
if PaperTrail.config.object_changes_adapter.respond_to?(:load_changeset)
|
289
319
|
return PaperTrail.config.object_changes_adapter.load_changeset(self)
|
290
320
|
end
|
291
321
|
|
@@ -324,7 +354,10 @@ module PaperTrail
|
|
324
354
|
else
|
325
355
|
begin
|
326
356
|
PaperTrail.serializer.load(object_changes)
|
327
|
-
rescue StandardError
|
357
|
+
rescue StandardError => e
|
358
|
+
if defined?(::Psych::Exception) && e.instance_of?(::Psych::Exception)
|
359
|
+
::Kernel.warn format(E_YAML_PERMITTED_CLASSES, e)
|
360
|
+
end
|
328
361
|
{}
|
329
362
|
end
|
330
363
|
end
|
@@ -342,20 +375,32 @@ module PaperTrail
|
|
342
375
|
excess_versions.map(&:destroy)
|
343
376
|
end
|
344
377
|
|
378
|
+
# @api private
|
379
|
+
def sibling_versions
|
380
|
+
@sibling_versions ||= self.class.with_item_keys(item_type, item_id)
|
381
|
+
end
|
382
|
+
|
345
383
|
# See docs section 2.e. Limiting the Number of Versions Created.
|
346
384
|
# The version limit can be global or per-model.
|
347
385
|
#
|
348
386
|
# @api private
|
349
|
-
#
|
350
|
-
# TODO: Duplication: similar `constantize` in Reifier#version_reification_class
|
351
387
|
def version_limit
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
388
|
+
klass = item.class
|
389
|
+
if limit_option?(klass)
|
390
|
+
klass.paper_trail_options[:limit]
|
391
|
+
elsif base_class_limit_option?(klass)
|
392
|
+
klass.base_class.paper_trail_options[:limit]
|
393
|
+
else
|
394
|
+
PaperTrail.config.version_limit
|
357
395
|
end
|
358
|
-
|
396
|
+
end
|
397
|
+
|
398
|
+
def limit_option?(klass)
|
399
|
+
klass.respond_to?(:paper_trail_options) && klass.paper_trail_options.key?(:limit)
|
400
|
+
end
|
401
|
+
|
402
|
+
def base_class_limit_option?(klass)
|
403
|
+
klass.respond_to?(:base_class) && limit_option?(klass.base_class)
|
359
404
|
end
|
360
405
|
end
|
361
406
|
end
|
@@ -7,9 +7,9 @@ module PaperTrail
|
|
7
7
|
# because of this confusion, but it's not worth the breaking change.
|
8
8
|
# People are encouraged to use `PaperTrail.gem_version` instead.
|
9
9
|
module VERSION
|
10
|
-
MAJOR =
|
11
|
-
MINOR =
|
12
|
-
TINY =
|
10
|
+
MAJOR = 13
|
11
|
+
MINOR = 0
|
12
|
+
TINY = 0
|
13
13
|
|
14
14
|
# Set PRE to nil unless it's a pre-release (beta, rc, etc.)
|
15
15
|
PRE = nil
|
data/lib/paper_trail.rb
CHANGED
@@ -8,40 +8,26 @@
|
|
8
8
|
# can revisit this decision.
|
9
9
|
require "active_support/all"
|
10
10
|
|
11
|
-
|
12
|
-
# all of those files, but it seems easier to troubleshoot if we just make sure
|
13
|
-
# AR is loaded here before loading *any* of PT. See discussion of
|
14
|
-
# performance/simplicity tradeoff for activesupport above.
|
15
|
-
require "active_record"
|
16
|
-
|
17
|
-
require "request_store"
|
11
|
+
require "paper_trail/errors"
|
18
12
|
require "paper_trail/cleaner"
|
19
13
|
require "paper_trail/compatibility"
|
20
14
|
require "paper_trail/config"
|
21
|
-
require "paper_trail/has_paper_trail"
|
22
15
|
require "paper_trail/record_history"
|
23
|
-
require "paper_trail/reifier"
|
24
16
|
require "paper_trail/request"
|
25
|
-
require "paper_trail/version_concern"
|
26
17
|
require "paper_trail/version_number"
|
27
18
|
require "paper_trail/serializers/json"
|
28
|
-
require "paper_trail/serializers/yaml"
|
29
19
|
|
30
20
|
# An ActiveRecord extension that tracks changes to your models, for auditing or
|
31
21
|
# versioning.
|
32
22
|
module PaperTrail
|
33
|
-
E_RAILS_NOT_LOADED = <<-EOS.squish.freeze
|
34
|
-
PaperTrail has been loaded too early, before rails is loaded. This can
|
35
|
-
happen when another gem defines the ::Rails namespace, then PT is loaded,
|
36
|
-
all before rails is loaded. You may want to reorder your Gemfile, or defer
|
37
|
-
the loading of PT by using `require: false` and a manual require elsewhere.
|
38
|
-
EOS
|
39
23
|
E_TIMESTAMP_FIELD_CONFIG = <<-EOS.squish.freeze
|
40
24
|
PaperTrail.timestamp_field= has been removed, without replacement. It is no
|
41
25
|
longer configurable. The timestamp column in the versions table must now be
|
42
26
|
named created_at.
|
43
27
|
EOS
|
44
28
|
|
29
|
+
RAILS_GTE_7_0 = ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
|
30
|
+
|
45
31
|
extend PaperTrail::Cleaner
|
46
32
|
|
47
33
|
class << self
|
@@ -85,7 +71,7 @@ module PaperTrail
|
|
85
71
|
#
|
86
72
|
# @api public
|
87
73
|
def request(options = nil, &block)
|
88
|
-
if options.nil? && !
|
74
|
+
if options.nil? && !block
|
89
75
|
Request
|
90
76
|
else
|
91
77
|
Request.with(options, &block)
|
@@ -95,7 +81,7 @@ module PaperTrail
|
|
95
81
|
# Set the field which records when a version was created.
|
96
82
|
# @api public
|
97
83
|
def timestamp_field=(_field_name)
|
98
|
-
raise
|
84
|
+
raise Error, E_TIMESTAMP_FIELD_CONFIG
|
99
85
|
end
|
100
86
|
|
101
87
|
# Set the PaperTrail serializer. This setting affects all threads.
|
@@ -126,27 +112,18 @@ module PaperTrail
|
|
126
112
|
end
|
127
113
|
end
|
128
114
|
|
129
|
-
#
|
130
|
-
#
|
131
|
-
#
|
132
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
if defined?(::Rails.application)
|
142
|
-
require "paper_trail/frameworks/rails"
|
143
|
-
else
|
144
|
-
::Kernel.warn(::PaperTrail::E_RAILS_NOT_LOADED)
|
145
|
-
end
|
115
|
+
# PT is built on ActiveRecord, but does not require Rails. If Rails is defined,
|
116
|
+
# our Railtie makes sure not to load the AR-dependent parts of PT until AR is
|
117
|
+
# ready. A typical Rails `application.rb` has:
|
118
|
+
#
|
119
|
+
# ```
|
120
|
+
# require 'rails/all' # Defines `Rails`
|
121
|
+
# Bundler.require(*Rails.groups) # require 'paper_trail' (this file)
|
122
|
+
# ```
|
123
|
+
#
|
124
|
+
# Non-rails applications should take similar care to load AR before PT.
|
125
|
+
if defined?(Rails)
|
126
|
+
require "paper_trail/frameworks/rails"
|
146
127
|
else
|
147
128
|
require "paper_trail/frameworks/active_record"
|
148
129
|
end
|
149
|
-
|
150
|
-
if defined?(::ActiveRecord)
|
151
|
-
::PaperTrail::Compatibility.check_activerecord(::ActiveRecord.gem_version)
|
152
|
-
end
|