paper_trail 10.3.1 → 12.1.0

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