diff --git a/CHANGELOG.md b/CHANGELOG.md index c8912a4d..eb023c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ ### Fixed +- [#857](https://github.com/airblade/paper_trail/pull/857) - + Fix deserialization of enums written by PT 4. - [#798](https://github.com/airblade/paper_trail/issues/798) - Fix a rare bug with serialization of enums in rails 4.2 only when using `touch_with_version`. diff --git a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb index 7b6ffb60..c597f869 100644 --- a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +++ b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb @@ -8,19 +8,41 @@ module PaperTrail # This implementation depends on the `type_for_attribute` method, which was # introduced in rails 4.2. In older versions of rails, we shim this method # with `LegacyActiveRecordShim`. + class CastAttributeSerializer + def initialize(klass) + @klass = klass + end + + private + + # Returns a hash mapping attributes to hashes that map strings to + # integers. Example: + # + # ``` + # { "status" => { "draft"=>0, "published"=>1, "archived"=>2 } } + # ``` + # + # ActiveRecord::Enum was added in AR 4.1 + # http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums + def defined_enums + @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {}) + end + end + if ::ActiveRecord::VERSION::MAJOR >= 5 # This implementation uses AR 5's `serialize` and `deserialize`. class CastAttributeSerializer - def initialize(klass) - @klass = klass - end - def serialize(attr, val) @klass.type_for_attribute(attr).serialize(val) end def deserialize(attr, val) - @klass.type_for_attribute(attr).deserialize(val) + if defined_enums[attr] && val.is_a?(::String) + # Because PT 4 used to save the string version of enums to `object_changes` + val + else + @klass.type_for_attribute(attr).deserialize(val) + end end end else @@ -29,10 +51,6 @@ module PaperTrail # `type_cast_for_database` in our shim attribute type classes, # `NoOpAttribute` and `SerializedAttribute`. class CastAttributeSerializer - def initialize(klass) - @klass = klass - end - def serialize(attr, val) castable_val = val if defined_enums[attr] @@ -44,21 +62,18 @@ module PaperTrail end def deserialize(attr, val) - val = @klass.type_for_attribute(attr).type_cast_from_database(val) - if defined_enums[attr] - defined_enums[attr].key(val) - else + if defined_enums[attr] && val.is_a?(::String) + # Because PT 4 used to save the string version of enums to `object_changes` val + else + val = @klass.type_for_attribute(attr).type_cast_from_database(val) + if defined_enums[attr] + defined_enums[attr].key(val) + else + val + end end end - - private - - # ActiveRecord::Enum was added in AR 4.1 - # http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums - def defined_enums - @defined_enums ||= (@klass.respond_to?(:defined_enums) ? @klass.defined_enums : {}) - end end end end diff --git a/spec/models/post_with_status_spec.rb b/spec/models/post_with_status_spec.rb index 35679e96..982ffe1d 100644 --- a/spec/models/post_with_status_spec.rb +++ b/spec/models/post_with_status_spec.rb @@ -5,7 +5,7 @@ require "rails_helper" describe PostWithStatus, type: :model do if defined?(ActiveRecord::Enum) with_versioning do - let(:post) { PostWithStatus.create!(status: "draft") } + let(:post) { described_class.create!(status: "draft") } it "should stash the enum value properly in versions" do post.published! @@ -13,6 +13,16 @@ describe PostWithStatus, type: :model do expect(post.paper_trail.previous_version.published?).to be true end + it "can read enums in version records written by PT 4" do + post = described_class.create(status: "draft") + post.published! + version = post.versions.last + # Simulate behavior PT 4, which used to save the string version of + # enums to `object_changes` + version.update(object_changes: "---\nid:\n- \n- 1\nstatus:\n- draft\n- published\n") + assert_equal %w(draft published), version.changeset["status"] + end + context "storing enum object_changes" do subject { post.versions.last }