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.
@@ -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