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.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/lib/generators/paper_trail/install/USAGE +3 -0
- data/lib/generators/paper_trail/{install_generator.rb → install/install_generator.rb} +27 -38
- data/lib/generators/paper_trail/{templates → install/templates}/create_versions.rb.erb +5 -3
- data/lib/generators/paper_trail/migration_generator.rb +38 -0
- data/lib/generators/paper_trail/update_item_subtype/USAGE +4 -0
- data/lib/generators/paper_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
- data/lib/generators/paper_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +24 -10
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +17 -45
- data/lib/paper_trail/compatibility.rb +51 -0
- data/lib/paper_trail/config.rb +9 -2
- data/lib/paper_trail/errors.rb +33 -0
- data/lib/paper_trail/events/base.rb +343 -0
- data/lib/paper_trail/events/create.rb +32 -0
- data/lib/paper_trail/events/destroy.rb +42 -0
- data/lib/paper_trail/events/update.rb +76 -0
- 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 +20 -17
- data/lib/paper_trail/model_config.rb +124 -87
- data/lib/paper_trail/queries/versions/where_attribute_changes.rb +50 -0
- data/lib/paper_trail/queries/versions/where_object.rb +4 -1
- data/lib/paper_trail/queries/versions/where_object_changes.rb +9 -14
- 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 +137 -436
- data/lib/paper_trail/reifier.rb +41 -25
- data/lib/paper_trail/request.rb +22 -25
- data/lib/paper_trail/serializers/json.rb +0 -10
- data/lib/paper_trail/serializers/yaml.rb +41 -11
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +1 -15
- data/lib/paper_trail/version_concern.rb +152 -62
- data/lib/paper_trail/version_number.rb +2 -2
- data/lib/paper_trail.rb +23 -123
- metadata +152 -61
- data/lib/generators/paper_trail/USAGE +0 -2
- data/lib/paper_trail/frameworks/rails/engine.rb +0 -14
- /data/lib/generators/paper_trail/{templates → install/templates}/add_object_changes_to_versions.rb.erb +0 -0
data/lib/paper_trail/reifier.rb
CHANGED
@@ -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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
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`.
|
121
|
-
#
|
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
|
-
|
132
|
+
clazz = version.item_type.constantize
|
133
|
+
inheritance_column_name = clazz.inheritance_column
|
125
134
|
inher_col_value = attrs[inheritance_column_name]
|
126
|
-
|
127
|
-
|
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
|
data/lib/paper_trail/request.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
26
|
-
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
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
|
-
|
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
|
@@ -42,40 +46,7 @@ module PaperTrail
|
|
42
46
|
end
|
43
47
|
|
44
48
|
def not_creates
|
45
|
-
where
|
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
|
249
|
-
#
|
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
|
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 =
|
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 =
|
11
|
-
MINOR =
|
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.)
|