paper_trail 5.0.1 → 5.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.
- checksums.yaml +4 -4
- data/.github/CONTRIBUTING.md +2 -1
- data/.github/ISSUE_TEMPLATE.md +1 -1
- data/.rubocop_todo.yml +1 -1
- data/Appraisals +6 -10
- data/CHANGELOG.md +33 -0
- data/README.md +133 -94
- data/doc/warning_about_not_setting_whodunnit.md +32 -0
- data/lib/paper_trail.rb +0 -1
- data/lib/paper_trail/attribute_serializers/README.md +10 -0
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +58 -0
- data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +4 -4
- data/lib/paper_trail/has_paper_trail.rb +41 -22
- data/lib/paper_trail/reifier.rb +4 -3
- data/lib/paper_trail/serializers/json.rb +4 -4
- data/lib/paper_trail/serializers/yaml.rb +4 -4
- data/lib/paper_trail/version_concern.rb +10 -2
- data/lib/paper_trail/version_number.rb +2 -2
- data/paper_trail.gemspec +1 -1
- data/spec/models/widget_spec.rb +2 -1
- data/spec/modules/version_number_spec.rb +2 -1
- data/test/dummy/app/models/foo_habtm.rb +1 -0
- data/test/unit/associations_test.rb +24 -0
- data/test/unit/serializers/json_test.rb +11 -3
- data/test/unit/serializers/yaml_test.rb +4 -1
- metadata +10 -5
- data/lib/paper_trail/attributes_serialization.rb +0 -161
@@ -0,0 +1,32 @@
|
|
1
|
+
# The warning about not setting whodunnit
|
2
|
+
|
3
|
+
After upgrading to PaperTrail 5, you see this warning:
|
4
|
+
|
5
|
+
> user_for_paper_trail is present, but whodunnit has not been set. PaperTrail no
|
6
|
+
> longer adds the set_paper_trail_whodunnit before_filter for you. Please add this
|
7
|
+
> before_filter to your ApplicationController to continue recording whodunnit.
|
8
|
+
|
9
|
+
## You want to track whodunnit
|
10
|
+
|
11
|
+
Add the set_paper_trail_whodunnit before_filter to your ApplicationController.
|
12
|
+
See the PaperTrail readme for an example (https://git.io/vrsbt).
|
13
|
+
|
14
|
+
## You don't want to track whodunnit
|
15
|
+
|
16
|
+
If you no longer want to track whodunnit, you may disable this
|
17
|
+
warning by overriding user_for_paper_trail to return nil.
|
18
|
+
|
19
|
+
```
|
20
|
+
# in application_controller.rb
|
21
|
+
def user_for_paper_trail
|
22
|
+
nil # disable whodunnit tracking
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
## You just want the warning to go away
|
27
|
+
|
28
|
+
To disable this warning for any other reason, use `skip_after_action`.
|
29
|
+
|
30
|
+
```
|
31
|
+
skip_after_action :warn_about_not_setting_whodunnit
|
32
|
+
```
|
data/lib/paper_trail.rb
CHANGED
@@ -0,0 +1,10 @@
|
|
1
|
+
Attribute Serializers
|
2
|
+
=====================
|
3
|
+
|
4
|
+
"Serialization" here refers to the preparation of data for insertion into a
|
5
|
+
database, particularly the `object` and `object_changes` columns in the
|
6
|
+
`versions` table.
|
7
|
+
|
8
|
+
Likewise, "deserialization" refers to any processing of data after they
|
9
|
+
have been read from the database, for example preparing the result of
|
10
|
+
`VersionConcern#changeset`.
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
# :nodoc:
|
3
|
+
module AttributeSerializers
|
4
|
+
# The `CastAttributeSerializer` (de)serializes model attribute values. For
|
5
|
+
# example, the string "1.99" serializes into the integer `1` when assigned
|
6
|
+
# to an attribute of type `ActiveRecord::Type::Integer`.
|
7
|
+
#
|
8
|
+
# This implementation depends on the `type_for_attribute` method, which was
|
9
|
+
# introduced in rails 4.2. In older versions of rails, we shim this method
|
10
|
+
# with `LegacyActiveRecordShim`.
|
11
|
+
if ::ActiveRecord::VERSION::MAJOR >= 5
|
12
|
+
# This implementation uses AR 5's `serialize` and `deserialize`.
|
13
|
+
class CastAttributeSerializer
|
14
|
+
def initialize(klass)
|
15
|
+
@klass = klass
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize(attr, val)
|
19
|
+
@klass.type_for_attribute(attr).serialize(val)
|
20
|
+
end
|
21
|
+
|
22
|
+
def deserialize(attr, val)
|
23
|
+
@klass.type_for_attribute(attr).deserialize(val)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
else
|
27
|
+
# This implementation uses AR 4.2's `type_cast_for_database`. For
|
28
|
+
# versions of AR < 4.2 we provide an implementation of
|
29
|
+
# `type_cast_for_database` in our shim attribute type classes,
|
30
|
+
# `NoOpAttribute` and `SerializedAttribute`.
|
31
|
+
class CastAttributeSerializer
|
32
|
+
def initialize(klass)
|
33
|
+
@klass = klass
|
34
|
+
end
|
35
|
+
|
36
|
+
def serialize(attr, val)
|
37
|
+
val = defined_enums[attr][val] if defined_enums[attr]
|
38
|
+
@klass.type_for_attribute(attr).type_cast_for_database(val)
|
39
|
+
end
|
40
|
+
|
41
|
+
def deserialize(attr, val)
|
42
|
+
val = @klass.type_for_attribute(attr).type_cast_from_database(val)
|
43
|
+
if defined_enums[attr]
|
44
|
+
defined_enums[attr].key(val)
|
45
|
+
else
|
46
|
+
val
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def defined_enums
|
53
|
+
@defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module AttributeSerializers
|
3
|
+
# Included into model if AR version is < 4.2. Backport Rails 4.2 and later's
|
4
|
+
# `type_for_attribute` so we can build on a common interface.
|
5
|
+
module LegacyActiveRecordShim
|
6
|
+
# An attribute which needs no processing. It is part of our backport (shim)
|
7
|
+
# of rails 4.2's attribute API. See `type_for_attribute` below.
|
8
|
+
class NoOpAttribute
|
9
|
+
def type_cast_for_database(value)
|
10
|
+
value
|
11
|
+
end
|
12
|
+
|
13
|
+
def type_cast_from_database(data)
|
14
|
+
data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
NO_OP_ATTRIBUTE = NoOpAttribute.new
|
18
|
+
|
19
|
+
# An attribute which requires manual (de)serialization to/from what we get
|
20
|
+
# from the database. It is part of our backport (shim) of rails 4.2's
|
21
|
+
# attribute API. See `type_for_attribute` below.
|
22
|
+
class SerializedAttribute
|
23
|
+
def initialize(coder)
|
24
|
+
@coder = coder.respond_to?(:dump) ? coder : PaperTrail.serializer
|
25
|
+
end
|
26
|
+
|
27
|
+
def type_cast_for_database(value)
|
28
|
+
@coder.dump(value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def type_cast_from_database(data)
|
32
|
+
@coder.load(data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def type_for_attribute(attr_name)
|
37
|
+
serialized_attribute_types[attr_name.to_s] || NO_OP_ATTRIBUTE
|
38
|
+
end
|
39
|
+
|
40
|
+
def serialized_attribute_types
|
41
|
+
@attribute_types ||= Hash[serialized_attributes.map do |attr_name, coder|
|
42
|
+
[attr_name, SerializedAttribute.new(coder)]
|
43
|
+
end]
|
44
|
+
end
|
45
|
+
private :serialized_attribute_types
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "paper_trail/attribute_serializers/cast_attribute_serializer"
|
2
|
+
|
3
|
+
module PaperTrail
|
4
|
+
module AttributeSerializers
|
5
|
+
# Serialize or deserialize the `version.object` column.
|
6
|
+
class ObjectAttribute
|
7
|
+
def initialize(model_class)
|
8
|
+
@model_class = model_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize(attributes)
|
12
|
+
alter(attributes, :serialize)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize(attributes)
|
16
|
+
alter(attributes, :deserialize)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Modifies `attributes` in place.
|
22
|
+
# TODO: Return a new hash instead.
|
23
|
+
def alter(attributes, serialization_method)
|
24
|
+
# Don't serialize before values before inserting into columns of type
|
25
|
+
# `JSON` on `PostgreSQL` databases.
|
26
|
+
return attributes if object_col_is_json?
|
27
|
+
|
28
|
+
serializer = CastAttributeSerializer.new(@model_class)
|
29
|
+
attributes.each do |key, value|
|
30
|
+
attributes[key] = serializer.send(serialization_method, key, value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def object_col_is_json?
|
35
|
+
@model_class.paper_trail_version_class.object_col_is_json?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "paper_trail/attribute_serializers/cast_attribute_serializer"
|
2
|
+
|
3
|
+
module PaperTrail
|
4
|
+
module AttributeSerializers
|
5
|
+
# Serialize or deserialize the `version.object_changes` column.
|
6
|
+
class ObjectChangesAttribute
|
7
|
+
def initialize(item_class)
|
8
|
+
@item_class = item_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize(changes)
|
12
|
+
alter(changes, :serialize)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize(changes)
|
16
|
+
alter(changes, :deserialize)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Modifies `changes` in place.
|
22
|
+
# TODO: Return a new hash instead.
|
23
|
+
def alter(changes, serialization_method)
|
24
|
+
# Don't serialize before values before inserting into columns of type
|
25
|
+
# `JSON` on `PostgreSQL` databases.
|
26
|
+
return changes if object_changes_col_is_json?
|
27
|
+
|
28
|
+
serializer = CastAttributeSerializer.new(@item_class)
|
29
|
+
changes.clone.each do |key, change|
|
30
|
+
# `change` is an Array with two elements, representing before and after.
|
31
|
+
changes[key] = Array(change).map do |value|
|
32
|
+
serializer.send(serialization_method, key, value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def object_changes_col_is_json?
|
38
|
+
@item_class.paper_trail_version_class.object_changes_col_is_json?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -96,10 +96,10 @@ module PaperTrail
|
|
96
96
|
if enabled && user_present && whodunnit_blank && !@set_paper_trail_whodunnit_called
|
97
97
|
::Kernel.warn <<-EOS.strip_heredoc
|
98
98
|
user_for_paper_trail is present, but whodunnit has not been set.
|
99
|
-
PaperTrail no longer adds the set_paper_trail_whodunnit
|
100
|
-
|
101
|
-
|
102
|
-
|
99
|
+
PaperTrail no longer adds the set_paper_trail_whodunnit callback for
|
100
|
+
you. To continue recording whodunnit, please add this before_action
|
101
|
+
callback to your ApplicationController . For more information,
|
102
|
+
please see https://git.io/vrTsk
|
103
103
|
EOS
|
104
104
|
end
|
105
105
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require "active_support/core_ext/object" # provides the `try` method
|
2
|
-
require "paper_trail/
|
2
|
+
require "paper_trail/attribute_serializers/legacy_active_record_shim"
|
3
|
+
require "paper_trail/attribute_serializers/object_attribute"
|
4
|
+
require "paper_trail/attribute_serializers/object_changes_attribute"
|
3
5
|
|
4
6
|
module PaperTrail
|
5
7
|
# Extensions to `ActiveRecord::Base`. See `frameworks/active_record.rb`.
|
@@ -110,7 +112,10 @@ module PaperTrail
|
|
110
112
|
# Lazily include the instance methods so we don't clutter up
|
111
113
|
# any more ActiveRecord models than we have to.
|
112
114
|
send :include, InstanceMethods
|
113
|
-
|
115
|
+
|
116
|
+
if ::ActiveRecord::VERSION::STRING < "4.2"
|
117
|
+
send :extend, AttributeSerializers::LegacyActiveRecordShim
|
118
|
+
end
|
114
119
|
|
115
120
|
class_attribute :version_association_name
|
116
121
|
self.version_association_name = options[:version] || :version
|
@@ -309,7 +314,6 @@ module PaperTrail
|
|
309
314
|
|
310
315
|
# Utility method for reifying. Anything executed inside the block will
|
311
316
|
# appear like a new record.
|
312
|
-
# rubocop: disable Style/Alias
|
313
317
|
def appear_as_new_record
|
314
318
|
instance_eval {
|
315
319
|
alias :old_new_record? :new_record?
|
@@ -318,7 +322,6 @@ module PaperTrail
|
|
318
322
|
yield
|
319
323
|
instance_eval { alias :new_record? :old_new_record? }
|
320
324
|
end
|
321
|
-
# rubocop: enable Style/Alias
|
322
325
|
|
323
326
|
# Temporarily overwrites the value of whodunnit and then executes the
|
324
327
|
# provided block.
|
@@ -377,9 +380,7 @@ module PaperTrail
|
|
377
380
|
if pt_record_object_changes? && changed_notably?
|
378
381
|
data[:object_changes] = pt_recordable_object_changes
|
379
382
|
end
|
380
|
-
|
381
|
-
data[:transaction_id] = PaperTrail.transaction_id
|
382
|
-
end
|
383
|
+
add_transaction_id_to(data)
|
383
384
|
version = send(self.class.versions_association_name).create! merge_metadata(data)
|
384
385
|
update_transaction_id(version)
|
385
386
|
save_associations(version)
|
@@ -399,12 +400,14 @@ module PaperTrail
|
|
399
400
|
if pt_record_object_changes?
|
400
401
|
data[:object_changes] = pt_recordable_object_changes
|
401
402
|
end
|
402
|
-
|
403
|
-
data[:transaction_id] = PaperTrail.transaction_id
|
404
|
-
end
|
403
|
+
add_transaction_id_to(data)
|
405
404
|
version = send(self.class.versions_association_name).create merge_metadata(data)
|
406
|
-
|
407
|
-
|
405
|
+
if version.errors.any?
|
406
|
+
log_version_errors(version, :update)
|
407
|
+
else
|
408
|
+
update_transaction_id(version)
|
409
|
+
save_associations(version)
|
410
|
+
end
|
408
411
|
end
|
409
412
|
end
|
410
413
|
|
@@ -446,7 +449,9 @@ module PaperTrail
|
|
446
449
|
|
447
450
|
def changes_for_paper_trail
|
448
451
|
notable_changes = changes.delete_if { |k, _v| !notably_changed.include?(k) }
|
449
|
-
|
452
|
+
AttributeSerializers::ObjectChangesAttribute.
|
453
|
+
new(self.class).
|
454
|
+
serialize(notable_changes)
|
450
455
|
notable_changes.to_hash
|
451
456
|
end
|
452
457
|
|
@@ -480,14 +485,16 @@ module PaperTrail
|
|
480
485
|
object: pt_recordable_object,
|
481
486
|
whodunnit: PaperTrail.whodunnit
|
482
487
|
}
|
483
|
-
|
484
|
-
data[:transaction_id] = PaperTrail.transaction_id
|
485
|
-
end
|
488
|
+
add_transaction_id_to(data)
|
486
489
|
version = self.class.paper_trail_version_class.create(merge_metadata(data))
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
490
|
+
if version.errors.any?
|
491
|
+
log_version_errors(version, :destroy)
|
492
|
+
else
|
493
|
+
send("#{self.class.version_association_name}=", version)
|
494
|
+
send(self.class.versions_association_name).reset
|
495
|
+
update_transaction_id(version)
|
496
|
+
save_associations(version)
|
497
|
+
end
|
491
498
|
end
|
492
499
|
end
|
493
500
|
|
@@ -528,7 +535,7 @@ module PaperTrail
|
|
528
535
|
self.class.paper_trail_save_join_tables.include?(a.name) ||
|
529
536
|
a.klass.paper_trail_enabled_for_model?
|
530
537
|
assoc_version_args = {
|
531
|
-
version_id: version.
|
538
|
+
version_id: version.transaction_id,
|
532
539
|
foreign_key_name: a.name
|
533
540
|
}
|
534
541
|
assoc_ids =
|
@@ -577,7 +584,7 @@ module PaperTrail
|
|
577
584
|
# ommitting attributes to be skipped.
|
578
585
|
def object_attrs_for_paper_trail
|
579
586
|
attrs = attributes_before_change.except(*paper_trail_options[:skip])
|
580
|
-
self.class.
|
587
|
+
AttributeSerializers::ObjectAttribute.new(self.class).serialize(attrs)
|
581
588
|
attrs
|
582
589
|
end
|
583
590
|
|
@@ -640,6 +647,11 @@ module PaperTrail
|
|
640
647
|
(if_condition.blank? || if_condition.call(self)) && !unless_condition.try(:call, self)
|
641
648
|
end
|
642
649
|
|
650
|
+
def add_transaction_id_to(data)
|
651
|
+
return unless self.class.paper_trail_version_class.column_names.include?("transaction_id")
|
652
|
+
data[:transaction_id] = PaperTrail.transaction_id
|
653
|
+
end
|
654
|
+
|
643
655
|
# @api private
|
644
656
|
def update_transaction_id(version)
|
645
657
|
return unless self.class.paper_trail_version_class.column_names.include?("transaction_id")
|
@@ -649,6 +661,13 @@ module PaperTrail
|
|
649
661
|
version.save
|
650
662
|
end
|
651
663
|
end
|
664
|
+
|
665
|
+
def log_version_errors(version, action)
|
666
|
+
version.logger.warn(
|
667
|
+
"Unable to create version for #{action} of #{self.class.name}##{id}: " +
|
668
|
+
version.errors.full_messages.join(", ")
|
669
|
+
)
|
670
|
+
end
|
652
671
|
end
|
653
672
|
end
|
654
673
|
end
|
data/lib/paper_trail/reifier.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "paper_trail/attribute_serializers/object_attribute"
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
# Given a version record and some options, builds a new model object.
|
3
5
|
# @api private
|
@@ -164,10 +166,9 @@ module PaperTrail
|
|
164
166
|
# Set all the attributes in this version on the model.
|
165
167
|
def reify_attributes(model, version, attrs)
|
166
168
|
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
|
167
|
-
model.class.
|
168
|
-
|
169
|
+
AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
|
169
170
|
attrs.each do |k, v|
|
170
|
-
# `
|
171
|
+
# `ObjectAttribute#deserialize` will return the mapped enum value
|
171
172
|
# and in Rails < 5, the []= uses the integer type caster from the column
|
172
173
|
# definition (in general) and thus will turn a (usually) string to 0 instead
|
173
174
|
# of the correct value
|
@@ -14,8 +14,8 @@ module PaperTrail
|
|
14
14
|
ActiveSupport::JSON.encode object
|
15
15
|
end
|
16
16
|
|
17
|
-
# Returns a SQL condition to be used to match the given field and
|
18
|
-
# in the serialized object
|
17
|
+
# Returns a SQL LIKE condition to be used to match the given field and
|
18
|
+
# value in the serialized object.
|
19
19
|
def where_object_condition(arel_field, field, value)
|
20
20
|
# Convert to JSON to handle strings and nulls correctly.
|
21
21
|
json_value = value.to_json
|
@@ -32,8 +32,8 @@ module PaperTrail
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
# Returns a SQL condition to be used to match the given field and
|
36
|
-
# in the serialized object_changes
|
35
|
+
# Returns a SQL LIKE condition to be used to match the given field and
|
36
|
+
# value in the serialized `object_changes`.
|
37
37
|
def where_object_changes_condition(arel_field, field, value)
|
38
38
|
# Convert to JSON to handle strings and nulls correctly.
|
39
39
|
json_value = value.to_json
|
@@ -14,14 +14,14 @@ module PaperTrail
|
|
14
14
|
::YAML.dump object
|
15
15
|
end
|
16
16
|
|
17
|
-
# Returns a SQL condition to be used to match the given field and
|
18
|
-
# in the serialized object
|
17
|
+
# Returns a SQL LIKE condition to be used to match the given field and
|
18
|
+
# value in the serialized object.
|
19
19
|
def where_object_condition(arel_field, field, value)
|
20
20
|
arel_field.matches("%\n#{field}: #{value}\n%")
|
21
21
|
end
|
22
22
|
|
23
|
-
# Returns a SQL condition to be used to match the given field and
|
24
|
-
# in the serialized object_changes
|
23
|
+
# Returns a SQL LIKE condition to be used to match the given field and
|
24
|
+
# value in the serialized `object_changes`.
|
25
25
|
def where_object_changes_condition(arel_field, field, value)
|
26
26
|
# Need to check first (before) and secondary (after) fields
|
27
27
|
m1 = nil
|