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
@@ -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
- included do
16
- if ::ActiveRecord.gem_version >= Gem::Version.new("5.0")
17
- belongs_to :item, polymorphic: true, optional: true
18
- else
19
- belongs_to :item, polymorphic: true
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 "event <> ?", "create"
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 can't be called without an object column"
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 (see `sibling_versions`). The "create" event,
278
- # for example, has an index of 0.
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&.respond_to?(:load_changeset)
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 # TODO: Rescue something more specific
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
- if self.class.item_subtype_column_present?
353
- klass = (item_subtype || item_type).constantize
354
- if klass&.paper_trail_options&.key?(:limit)
355
- return klass.paper_trail_options[:limit]
356
- end
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
- PaperTrail.config.version_limit
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 = 10
11
- MINOR = 3
12
- TINY = 1
10
+ MAJOR = 14
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,34 +8,24 @@
8
8
  # can revisit this decision.
9
9
  require "active_support/all"
10
10
 
11
- # AR is required for, eg. has_paper_trail.rb, so we could put this `require` in
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"
11
+ # We used to `require "active_record"` here, but that was [replaced with a
12
+ # Railtie](https://github.com/paper-trail-gem/paper_trail/pull/1281) in PT 12.
13
+ # As a result, we cannot reference `ActiveRecord` in this file (ie. until our
14
+ # Railtie has loaded). If we did, it would cause [problems with non-Rails
15
+ # projects](https://github.com/paper-trail-gem/paper_trail/pull/1401).
16
16
 
17
- require "request_store"
17
+ require "paper_trail/errors"
18
18
  require "paper_trail/cleaner"
19
19
  require "paper_trail/compatibility"
20
20
  require "paper_trail/config"
21
- require "paper_trail/has_paper_trail"
22
21
  require "paper_trail/record_history"
23
- require "paper_trail/reifier"
24
22
  require "paper_trail/request"
25
- require "paper_trail/version_concern"
26
23
  require "paper_trail/version_number"
27
24
  require "paper_trail/serializers/json"
28
- require "paper_trail/serializers/yaml"
29
25
 
30
26
  # An ActiveRecord extension that tracks changes to your models, for auditing or
31
27
  # versioning.
32
28
  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
29
  E_TIMESTAMP_FIELD_CONFIG = <<-EOS.squish.freeze
40
30
  PaperTrail.timestamp_field= has been removed, without replacement. It is no
41
31
  longer configurable. The timestamp column in the versions table must now be
@@ -85,7 +75,7 @@ module PaperTrail
85
75
  #
86
76
  # @api public
87
77
  def request(options = nil, &block)
88
- if options.nil? && !block_given?
78
+ if options.nil? && !block
89
79
  Request
90
80
  else
91
81
  Request.with(options, &block)
@@ -95,7 +85,7 @@ module PaperTrail
95
85
  # Set the field which records when a version was created.
96
86
  # @api public
97
87
  def timestamp_field=(_field_name)
98
- raise(E_TIMESTAMP_FIELD_CONFIG)
88
+ raise Error, E_TIMESTAMP_FIELD_CONFIG
99
89
  end
100
90
 
101
91
  # Set the PaperTrail serializer. This setting affects all threads.
@@ -112,7 +102,7 @@ module PaperTrail
112
102
 
113
103
  # Returns PaperTrail's global configuration object, a singleton. These
114
104
  # settings affect all threads.
115
- # @api private
105
+ # @api public
116
106
  def config
117
107
  @config ||= PaperTrail::Config.instance
118
108
  yield @config if block_given?
@@ -120,33 +110,25 @@ module PaperTrail
120
110
  end
121
111
  alias configure config
122
112
 
113
+ # @api public
123
114
  def version
124
115
  VERSION::STRING
125
116
  end
126
117
  end
127
118
  end
128
119
 
129
- # We use the `on_load` "hook" instead of `ActiveRecord::Base.include` because we
130
- # don't want to cause all of AR to be autloaded yet. See
131
- # https://guides.rubyonrails.org/engines.html#what-are-on-load-hooks-questionmark
132
- # to learn more about `on_load`.
133
- ActiveSupport.on_load(:active_record) do
134
- include PaperTrail::Model
135
- end
136
-
137
- # Require frameworks
138
- if defined?(::Rails)
139
- # Rails module is sometimes defined by gems like rails-html-sanitizer
140
- # so we check for presence of Rails.application.
141
- if defined?(::Rails.application)
142
- require "paper_trail/frameworks/rails"
143
- else
144
- ::Kernel.warn(::PaperTrail::E_RAILS_NOT_LOADED)
145
- end
120
+ # PT is built on ActiveRecord, but does not require Rails. If Rails is defined,
121
+ # our Railtie makes sure not to load the AR-dependent parts of PT until AR is
122
+ # ready. A typical Rails `application.rb` has:
123
+ #
124
+ # ```
125
+ # require 'rails/all' # Defines `Rails`
126
+ # Bundler.require(*Rails.groups) # require 'paper_trail' (this file)
127
+ # ```
128
+ #
129
+ # Non-rails applications should take similar care to load AR before PT.
130
+ if defined?(Rails)
131
+ require "paper_trail/frameworks/rails"
146
132
  else
147
133
  require "paper_trail/frameworks/active_record"
148
134
  end
149
-
150
- if defined?(::ActiveRecord)
151
- ::PaperTrail::Compatibility.check_activerecord(::ActiveRecord.gem_version)
152
- end