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.
@@ -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
+ ```
@@ -1,5 +1,4 @@
1
1
  require "request_store"
2
- require "paper_trail/attributes_serialization"
3
2
  require "paper_trail/cleaner"
4
3
  require "paper_trail/config"
5
4
  require "paper_trail/has_paper_trail"
@@ -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
- before_filter for you. Please add this before_filter to your
101
- ApplicationController to continue recording whodunnit. See the
102
- PaperTrail readme for an example.
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/attributes_serialization"
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
- send :extend, AttributesSerialization
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
- if self.class.paper_trail_version_class.column_names.include?("transaction_id")
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
- if self.class.paper_trail_version_class.column_names.include?("transaction_id")
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
- update_transaction_id(version)
407
- save_associations(version)
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
- self.class.serialize_attribute_changes_for_paper_trail!(notable_changes)
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
- if self.class.paper_trail_version_class.column_names.include?("transaction_id")
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
- send("#{self.class.version_association_name}=", version)
488
- send(self.class.versions_association_name).send :load_target
489
- update_transaction_id(version)
490
- save_associations(version)
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.id,
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.serialize_attributes_for_paper_trail!(attrs)
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
@@ -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.unserialize_attributes_for_paper_trail! attrs
168
-
169
+ AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
169
170
  attrs.each do |k, v|
170
- # `unserialize_attributes_for_paper_trail!` will return the mapped enum value
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 value
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 value
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 value
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 value
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