paper_trail 9.2.0 → 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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/lib/generators/paper_trail/install/USAGE +3 -0
  4. data/lib/generators/paper_trail/{install_generator.rb → install/install_generator.rb} +27 -38
  5. data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +5 -3
  6. data/lib/generators/paper_trail/migration_generator.rb +38 -0
  7. data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
  8. data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
  9. data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
  10. data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
  11. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +17 -45
  12. data/lib/paper_trail/compatibility.rb +51 -0
  13. data/lib/paper_trail/config.rb +9 -2
  14. data/lib/paper_trail/errors.rb +33 -0
  15. data/lib/paper_trail/events/base.rb +343 -0
  16. data/lib/paper_trail/events/create.rb +32 -0
  17. data/lib/paper_trail/events/destroy.rb +42 -0
  18. data/lib/paper_trail/events/update.rb +76 -0
  19. data/lib/paper_trail/frameworks/active_record.rb +9 -2
  20. data/lib/paper_trail/frameworks/rails/controller.rb +1 -9
  21. data/lib/paper_trail/frameworks/rails/railtie.rb +30 -0
  22. data/lib/paper_trail/frameworks/rails.rb +1 -2
  23. data/lib/paper_trail/has_paper_trail.rb +20 -17
  24. data/lib/paper_trail/model_config.rb +124 -87
  25. data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
  26. data/lib/paper_trail/queries/versions/where_object.rb +4 -1
  27. data/lib/paper_trail/queries/versions/where_object_changes.rb +9 -14
  28. data/lib/paper_trail/queries/versions/where_object_changes_from.rb +57 -0
  29. data/lib/paper_trail/queries/versions/where_object_changes_to.rb +57 -0
  30. data/lib/paper_trail/record_trail.rb +137 -436
  31. data/lib/paper_trail/reifier.rb +41 -25
  32. data/lib/paper_trail/request.rb +22 -25
  33. data/lib/paper_trail/serializers/json.rb +0 -10
  34. data/lib/paper_trail/serializers/yaml.rb +41 -11
  35. data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
  36. data/lib/paper_trail/version_concern.rb +152 -62
  37. data/lib/paper_trail/version_number.rb +2 -2
  38. data/lib/paper_trail.rb +23 -123
  39. metadata +152 -61
  40. data/lib/generators/paper_trail/USAGE +0 -2
  41. data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
  42. /data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
@@ -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,20 +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
  #
129
+ # You can see these particular examples in action in
130
+ # `spec/models/animal_spec.rb` and `spec/models/plant_spec.rb`
123
131
  def version_reification_class(version, attrs)
124
- inheritance_column_name = version.item_type.constantize.inheritance_column
132
+ clazz = version.item_type.constantize
133
+ inheritance_column_name = clazz.inheritance_column
125
134
  inher_col_value = attrs[inheritance_column_name]
126
- class_name = inher_col_value.blank? ? version.item_type : inher_col_value
127
- 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)
128
144
  end
129
145
  end
130
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,10 +9,25 @@ 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
 
25
+ # @param object (Hash | HashWithIndifferentAccess) - Coming from
26
+ # `recordable_object` `object` will be a plain `Hash`. However, due to
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`.
15
29
  def dump(object)
30
+ object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
16
31
  ::YAML.dump object
17
32
  end
18
33
 
@@ -22,16 +37,31 @@ module PaperTrail
22
37
  arel_field.matches("%\n#{field}: #{value}\n%")
23
38
  end
24
39
 
25
- # Returns a SQL LIKE condition to be used to match the given field and
26
- # value in the serialized `object_changes`.
27
- def where_object_changes_condition(*)
28
- raise <<-STR.squish.freeze
29
- where_object_changes no longer supports reading YAML from a text
30
- column. The old implementation was inaccurate, returning more records
31
- than you wanted. This feature was deprecated in 8.1.0 and removed in
32
- 9.0.0. The json and jsonb datatypes are still supported. See
33
- discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
34
- 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
35
65
  end
36
66
  end
37
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,17 +25,6 @@ module PaperTrail
28
25
 
29
26
  private
30
27
 
31
- def active_record_pre_502?
32
- ::ActiveRecord::VERSION::MAJOR < 5 ||
33
- (::ActiveRecord::VERSION::MINOR.zero? && ::ActiveRecord::VERSION::TINY < 2)
34
- end
35
-
36
- def serialize_with_ar(array)
37
- ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
38
- new(@subtype, @delimiter).
39
- serialize(array)
40
- end
41
-
42
28
  def deserialize_with_ar(array)
43
29
  ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
44
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,13 +15,14 @@ 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
@@ -42,40 +46,7 @@ module PaperTrail
42
46
  end
43
47
 
44
48
  def not_creates
45
- where "event <> ?", "create"
46
- end
47
-
48
- # Returns versions after `obj`.
49
- #
50
- # @param obj - a `Version` or a timestamp
51
- # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
52
- # Default: false.
53
- # @return `ActiveRecord::Relation`
54
- # @api public
55
- def subsequent(obj, timestamp_arg = false)
56
- if timestamp_arg != true && primary_key_is_int?
57
- return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
58
- end
59
-
60
- obj = obj.send(:created_at) if obj.is_a?(self)
61
- where(arel_table[:created_at].gt(obj)).order(timestamp_sort_order)
62
- end
63
-
64
- # Returns versions before `obj`.
65
- #
66
- # @param obj - a `Version` or a timestamp
67
- # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
68
- # Default: false.
69
- # @return `ActiveRecord::Relation`
70
- # @api public
71
- def preceding(obj, timestamp_arg = false)
72
- if timestamp_arg != true && primary_key_is_int?
73
- return where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
74
- end
75
-
76
- obj = obj.send(:created_at) if obj.is_a?(self)
77
- where(arel_table[:created_at].lt(obj)).
78
- order(timestamp_sort_order("desc"))
49
+ where.not(event: "create")
79
50
  end
80
51
 
81
52
  def between(start_time, end_time)
@@ -93,6 +64,18 @@ module PaperTrail
93
64
  end
94
65
  end
95
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
+
96
79
  # Given a hash of attributes like `name: 'Joan'`, query the
97
80
  # `versions.objects` column.
98
81
  #
@@ -149,6 +132,36 @@ module PaperTrail
149
132
  Queries::Versions::WhereObjectChanges.new(self, args).execute
150
133
  end
151
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
+
152
165
  def primary_key_is_int?
153
166
  @primary_key_is_int ||= columns_hash[primary_key].type == :integer
154
167
  rescue StandardError # TODO: Rescue something more specific
@@ -166,6 +179,65 @@ module PaperTrail
166
179
  def object_changes_col_is_json?
167
180
  %i[json jsonb].include?(columns_hash["object_changes"].try(:type))
168
181
  end
182
+
183
+ # Returns versions before `obj`.
184
+ #
185
+ # @param obj - a `Version` or a timestamp
186
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
187
+ # Default: false.
188
+ # @return `ActiveRecord::Relation`
189
+ # @api public
190
+ # rubocop:disable Style/OptionalBooleanParameter
191
+ def preceding(obj, timestamp_arg = false)
192
+ if timestamp_arg != true && primary_key_is_int?
193
+ preceding_by_id(obj)
194
+ else
195
+ preceding_by_timestamp(obj)
196
+ end
197
+ end
198
+ # rubocop:enable Style/OptionalBooleanParameter
199
+
200
+ # Returns versions after `obj`.
201
+ #
202
+ # @param obj - a `Version` or a timestamp
203
+ # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
204
+ # Default: false.
205
+ # @return `ActiveRecord::Relation`
206
+ # @api public
207
+ # rubocop:disable Style/OptionalBooleanParameter
208
+ def subsequent(obj, timestamp_arg = false)
209
+ if timestamp_arg != true && primary_key_is_int?
210
+ subsequent_by_id(obj)
211
+ else
212
+ subsequent_by_timestamp(obj)
213
+ end
214
+ end
215
+ # rubocop:enable Style/OptionalBooleanParameter
216
+
217
+ private
218
+
219
+ # @api private
220
+ def preceding_by_id(obj)
221
+ where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
222
+ end
223
+
224
+ # @api private
225
+ def preceding_by_timestamp(obj)
226
+ obj = obj.send(:created_at) if obj.is_a?(self)
227
+ where(arel_table[:created_at].lt(obj)).
228
+ order(timestamp_sort_order("desc"))
229
+ end
230
+
231
+ # @api private
232
+ def subsequent_by_id(version)
233
+ where(arel_table[primary_key].gt(version.id)).order(arel_table[primary_key].asc)
234
+ end
235
+
236
+ # @api private
237
+ def subsequent_by_timestamp(obj)
238
+ obj = obj.send(:created_at) if obj.is_a?(self)
239
+ where(arel_table[:created_at].gt(obj)).order(timestamp_sort_order)
240
+ end
169
241
  end
170
242
 
171
243
  # @api private
@@ -179,18 +251,8 @@ module PaperTrail
179
251
 
180
252
  # Restore the item from this version.
181
253
  #
182
- # Optionally this can also restore all :has_one and :has_many (including
183
- # has_many :through) associations as they were "at the time", if they are
184
- # also being versioned by PaperTrail.
185
- #
186
254
  # Options:
187
255
  #
188
- # - :has_one
189
- # - `true` - Also reify has_one associations.
190
- # - `false - Default.
191
- # - :has_many
192
- # - `true` - Also reify has_many and has_many :through associations.
193
- # - `false` - Default.
194
256
  # - :mark_for_destruction
195
257
  # - `true` - Mark the has_one/has_many associations that did not exist in
196
258
  # the reified version for destruction, instead of removing them.
@@ -205,6 +267,9 @@ module PaperTrail
205
267
  # - `:preserve` - Attributes undefined in version record are not modified.
206
268
  #
207
269
  def reify(options = {})
270
+ unless self.class.column_names.include? "object"
271
+ raise Error, "reify requires an object column"
272
+ end
208
273
  return nil if object.nil?
209
274
  ::PaperTrail::Reifier.reify(self, options)
210
275
  end
@@ -229,13 +294,6 @@ module PaperTrail
229
294
  end
230
295
  alias version_author terminator
231
296
 
232
- def sibling_versions(reload = false)
233
- if reload || !defined?(@sibling_versions) || @sibling_versions.nil?
234
- @sibling_versions = self.class.with_item_keys(item_type, item_id)
235
- end
236
- @sibling_versions
237
- end
238
-
239
297
  def next
240
298
  @next ||= sibling_versions.subsequent(self).first
241
299
  end
@@ -245,8 +303,9 @@ module PaperTrail
245
303
  end
246
304
 
247
305
  # Returns an integer representing the chronological position of the
248
- # version among its siblings (see `sibling_versions`). The "create" event,
249
- # for example, has an index of 0.
306
+ # version among its siblings. The "create" event, for example, has an index
307
+ # of 0.
308
+ #
250
309
  # @api public
251
310
  def index
252
311
  @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
@@ -256,7 +315,7 @@ module PaperTrail
256
315
 
257
316
  # @api private
258
317
  def load_changeset
259
- if PaperTrail.config.object_changes_adapter
318
+ if PaperTrail.config.object_changes_adapter.respond_to?(:load_changeset)
260
319
  return PaperTrail.config.object_changes_adapter.load_changeset(self)
261
320
  end
262
321
 
@@ -295,7 +354,10 @@ module PaperTrail
295
354
  else
296
355
  begin
297
356
  PaperTrail.serializer.load(object_changes)
298
- 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
299
361
  {}
300
362
  end
301
363
  end
@@ -304,7 +366,7 @@ module PaperTrail
304
366
  # Enforces the `version_limit`, if set. Default: no limit.
305
367
  # @api private
306
368
  def enforce_version_limit!
307
- limit = PaperTrail.config.version_limit
369
+ limit = version_limit
308
370
  return unless limit.is_a? Numeric
309
371
  previous_versions = sibling_versions.not_creates.
310
372
  order(self.class.timestamp_sort_order("asc"))
@@ -312,5 +374,33 @@ module PaperTrail
312
374
  excess_versions = previous_versions - previous_versions.last(limit)
313
375
  excess_versions.map(&:destroy)
314
376
  end
377
+
378
+ # @api private
379
+ def sibling_versions
380
+ @sibling_versions ||= self.class.with_item_keys(item_type, item_id)
381
+ end
382
+
383
+ # See docs section 2.e. Limiting the Number of Versions Created.
384
+ # The version limit can be global or per-model.
385
+ #
386
+ # @api private
387
+ def version_limit
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
395
+ end
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)
404
+ end
315
405
  end
316
406
  end
@@ -7,8 +7,8 @@ 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 = 9
11
- MINOR = 2
10
+ MAJOR = 14
11
+ MINOR = 0
12
12
  TINY = 0
13
13
 
14
14
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)