paper_trail 10.3.1 → 12.1.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/install_generator.rb +13 -7
  4. data/lib/generators/paper_trail/install/templates/create_versions.rb.erb +1 -1
  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 +8 -46
  9. data/lib/paper_trail/compatibility.rb +2 -2
  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 +35 -50
  13. data/lib/paper_trail/events/destroy.rb +1 -1
  14. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  15. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  16. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  17. data/lib/paper_trail/frameworks/rails.rb +1 -2
  18. data/lib/paper_trail/has_paper_trail.rb +1 -1
  19. data/lib/paper_trail/model_config.rb +25 -30
  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 +8 -13
  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 +3 -5
  26. data/lib/paper_trail/reifier.rb +41 -26
  27. data/lib/paper_trail/request.rb +0 -3
  28. data/lib/paper_trail/serializers/json.rb +0 -10
  29. data/lib/paper_trail/serializers/yaml.rb +0 -12
  30. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -14
  31. data/lib/paper_trail/version_concern.rb +75 -40
  32. data/lib/paper_trail/version_number.rb +3 -3
  33. data/lib/paper_trail.rb +15 -40
  34. metadata +113 -40
  35. data/lib/paper_trail/frameworks/rails/engine.rb +0 -45
@@ -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`.
@@ -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
@@ -26,18 +26,6 @@ module PaperTrail
26
26
  def where_object_condition(arel_field, field, value)
27
27
  arel_field.matches("%\n#{field}: #{value}\n%")
28
28
  end
29
-
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
- end
41
29
  end
42
30
  end
43
31
  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).
@@ -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
@@ -13,22 +16,13 @@ module PaperTrail
13
16
  extend ::ActiveSupport::Concern
14
17
 
15
18
  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
21
-
19
+ belongs_to :item, polymorphic: true, optional: true
22
20
  validates_presence_of :event
23
21
  after_create :enforce_version_limit!
24
22
  end
25
23
 
26
24
  # :nodoc:
27
25
  module ClassMethods
28
- def item_subtype_column_present?
29
- column_names.include?("item_subtype")
30
- end
31
-
32
26
  def with_item_keys(item_type, item_id)
33
27
  where item_type: item_type, item_id: item_id
34
28
  end
@@ -46,7 +40,7 @@ module PaperTrail
46
40
  end
47
41
 
48
42
  def not_creates
49
- where "event <> ?", "create"
43
+ where.not(event: "create")
50
44
  end
51
45
 
52
46
  def between(start_time, end_time)
@@ -64,6 +58,18 @@ module PaperTrail
64
58
  end
65
59
  end
66
60
 
61
+ # Given an attribute like `"name"`, query the `versions.object_changes`
62
+ # column for any changes that modified the provided attribute.
63
+ #
64
+ # @api public
65
+ def where_attribute_changes(attribute)
66
+ unless attribute.is_a?(String) || attribute.is_a?(Symbol)
67
+ raise ArgumentError, "expected to receive a String or Symbol"
68
+ end
69
+
70
+ Queries::Versions::WhereAttributeChanges.new(self, attribute).execute
71
+ end
72
+
67
73
  # Given a hash of attributes like `name: 'Joan'`, query the
68
74
  # `versions.objects` column.
69
75
  #
@@ -120,6 +126,36 @@ module PaperTrail
120
126
  Queries::Versions::WhereObjectChanges.new(self, args).execute
121
127
  end
122
128
 
129
+ # Given a hash of attributes like `name: 'Joan'`, query the
130
+ # `versions.objects_changes` column for changes where the version changed
131
+ # from the hash of attributes to other values.
132
+ #
133
+ # This is useful for finding versions where the attribute started with a
134
+ # known value and changed to something else. This is in comparison to
135
+ # `where_object_changes` which will find both the changes before and
136
+ # after.
137
+ #
138
+ # @api public
139
+ def where_object_changes_from(args = {})
140
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
141
+ Queries::Versions::WhereObjectChangesFrom.new(self, args).execute
142
+ end
143
+
144
+ # Given a hash of attributes like `name: 'Joan'`, query the
145
+ # `versions.objects_changes` column for changes where the version changed
146
+ # to the hash of attributes from other values.
147
+ #
148
+ # This is useful for finding versions where the attribute started with an
149
+ # unknown value and changed to a known value. This is in comparison to
150
+ # `where_object_changes` which will find both the changes before and
151
+ # after.
152
+ #
153
+ # @api public
154
+ def where_object_changes_to(args = {})
155
+ raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
156
+ Queries::Versions::WhereObjectChangesTo.new(self, args).execute
157
+ end
158
+
123
159
  def primary_key_is_int?
124
160
  @primary_key_is_int ||= columns_hash[primary_key].type == :integer
125
161
  rescue StandardError # TODO: Rescue something more specific
@@ -145,6 +181,7 @@ module PaperTrail
145
181
  # Default: false.
146
182
  # @return `ActiveRecord::Relation`
147
183
  # @api public
184
+ # rubocop:disable Style/OptionalBooleanParameter
148
185
  def preceding(obj, timestamp_arg = false)
149
186
  if timestamp_arg != true && primary_key_is_int?
150
187
  preceding_by_id(obj)
@@ -152,6 +189,7 @@ module PaperTrail
152
189
  preceding_by_timestamp(obj)
153
190
  end
154
191
  end
192
+ # rubocop:enable Style/OptionalBooleanParameter
155
193
 
156
194
  # Returns versions after `obj`.
157
195
  #
@@ -160,6 +198,7 @@ module PaperTrail
160
198
  # Default: false.
161
199
  # @return `ActiveRecord::Relation`
162
200
  # @api public
201
+ # rubocop:disable Style/OptionalBooleanParameter
163
202
  def subsequent(obj, timestamp_arg = false)
164
203
  if timestamp_arg != true && primary_key_is_int?
165
204
  subsequent_by_id(obj)
@@ -167,6 +206,7 @@ module PaperTrail
167
206
  subsequent_by_timestamp(obj)
168
207
  end
169
208
  end
209
+ # rubocop:enable Style/OptionalBooleanParameter
170
210
 
171
211
  private
172
212
 
@@ -205,18 +245,8 @@ module PaperTrail
205
245
 
206
246
  # Restore the item from this version.
207
247
  #
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
248
  # Options:
213
249
  #
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
250
  # - :mark_for_destruction
221
251
  # - `true` - Mark the has_one/has_many associations that did not exist in
222
252
  # the reified version for destruction, instead of removing them.
@@ -232,7 +262,7 @@ module PaperTrail
232
262
  #
233
263
  def reify(options = {})
234
264
  unless self.class.column_names.include? "object"
235
- raise "reify can't be called without an object column"
265
+ raise Error, "reify requires an object column"
236
266
  end
237
267
  return nil if object.nil?
238
268
  ::PaperTrail::Reifier.reify(self, options)
@@ -258,13 +288,6 @@ module PaperTrail
258
288
  end
259
289
  alias version_author terminator
260
290
 
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
291
  def next
269
292
  @next ||= sibling_versions.subsequent(self).first
270
293
  end
@@ -274,8 +297,9 @@ module PaperTrail
274
297
  end
275
298
 
276
299
  # 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.
300
+ # version among its siblings. The "create" event, for example, has an index
301
+ # of 0.
302
+ #
279
303
  # @api public
280
304
  def index
281
305
  @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
@@ -285,7 +309,7 @@ module PaperTrail
285
309
 
286
310
  # @api private
287
311
  def load_changeset
288
- if PaperTrail.config.object_changes_adapter&.respond_to?(:load_changeset)
312
+ if PaperTrail.config.object_changes_adapter.respond_to?(:load_changeset)
289
313
  return PaperTrail.config.object_changes_adapter.load_changeset(self)
290
314
  end
291
315
 
@@ -342,20 +366,31 @@ module PaperTrail
342
366
  excess_versions.map(&:destroy)
343
367
  end
344
368
 
369
+ # @api private
370
+ def sibling_versions
371
+ @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
372
+ end
373
+
345
374
  # See docs section 2.e. Limiting the Number of Versions Created.
346
375
  # The version limit can be global or per-model.
347
376
  #
348
377
  # @api private
349
- #
350
- # TODO: Duplication: similar `constantize` in Reifier#version_reification_class
351
378
  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
379
+ if limit_option?(item.class)
380
+ item.class.paper_trail_options[:limit]
381
+ elsif base_class_limit_option?(item.class)
382
+ item.class.base_class.paper_trail_options[:limit]
383
+ else
384
+ PaperTrail.config.version_limit
357
385
  end
358
- PaperTrail.config.version_limit
386
+ end
387
+
388
+ def limit_option?(klass)
389
+ klass.respond_to?(:paper_trail_options) && klass.paper_trail_options.key?(:limit)
390
+ end
391
+
392
+ def base_class_limit_option?(klass)
393
+ klass.respond_to?(:base_class) && limit_option?(klass.base_class)
359
394
  end
360
395
  end
361
396
  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 = 12
11
+ MINOR = 1
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,18 @@
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"
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
@@ -85,7 +69,7 @@ module PaperTrail
85
69
  #
86
70
  # @api public
87
71
  def request(options = nil, &block)
88
- if options.nil? && !block_given?
72
+ if options.nil? && !block
89
73
  Request
90
74
  else
91
75
  Request.with(options, &block)
@@ -95,7 +79,7 @@ module PaperTrail
95
79
  # Set the field which records when a version was created.
96
80
  # @api public
97
81
  def timestamp_field=(_field_name)
98
- raise(E_TIMESTAMP_FIELD_CONFIG)
82
+ raise Error, E_TIMESTAMP_FIELD_CONFIG
99
83
  end
100
84
 
101
85
  # Set the PaperTrail serializer. This setting affects all threads.
@@ -126,27 +110,18 @@ module PaperTrail
126
110
  end
127
111
  end
128
112
 
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
113
+ # PT is built on ActiveRecord, but does not require Rails. If Rails is defined,
114
+ # our Railtie makes sure not to load the AR-dependent parts of PT until AR is
115
+ # ready. A typical Rails `application.rb` has:
116
+ #
117
+ # ```
118
+ # require 'rails/all' # Defines `Rails`
119
+ # Bundler.require(*Rails.groups) # require 'paper_trail' (this file)
120
+ # ```
121
+ #
122
+ # Non-rails applications should take similar care to load AR before PT.
123
+ if defined?(Rails)
124
+ require "paper_trail/frameworks/rails"
146
125
  else
147
126
  require "paper_trail/frameworks/active_record"
148
127
  end
149
-
150
- if defined?(::ActiveRecord)
151
- ::PaperTrail::Compatibility.check_activerecord(::ActiveRecord.gem_version)
152
- end