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