Merge pull request #667 from kaspth/revive-serialized-attributes

Use Active Record's type system from 4.2 onwards.
This commit is contained in:
Jared Beck 2015-12-21 15:42:54 -05:00
commit 6df3532b7f
9 changed files with 202 additions and 184 deletions

View File

@ -1181,22 +1181,6 @@ class ConvertVersionsObjectToJson < ActiveRecord::Migration
end
```
## SerializedAttributes support
PaperTrail has a config option that can be used to enable/disable whether
PaperTrail attempts to utilize `ActiveRecord`'s `serialized_attributes` feature.
Note: This is enabled by default when PaperTrail is used with `ActiveRecord`
version < `4.2`, and disabled by default when used with ActiveRecord `4.2.x`.
Since `serialized_attributes` will be removed in `ActiveRecord` version `5.0`,
this configuration value does nothing when PaperTrail is used with
version `5.0` or greater.
```ruby
PaperTrail.config.serialized_attributes = true # enable
PaperTrail.config.serialized_attributes = false # disable
PaperTrail.serialized_attributes? # get current setting
```
## Testing
You may want to turn PaperTrail off to speed up your tests. See the [Turning

View File

@ -0,0 +1,89 @@
module PaperTrail
module AttributesSerialization
class NoOpAttribute
def type_cast_for_database(value)
value
end
def type_cast_from_database(data)
data
end
end
NO_OP_ATTRIBUTE = NoOpAttribute.new
class SerializedAttribute
def initialize(coder)
@coder = coder.respond_to?(:dump) ? coder : PaperTrail.serializer
end
def type_cast_for_database(value)
@coder.dump(value)
end
def type_cast_from_database(data)
@coder.load(data)
end
end
SERIALIZE, DESERIALIZE =
if ::ActiveRecord::VERSION::MAJOR >= 5
[:serialize, :deserialize]
else
[:type_cast_for_database, :type_cast_from_database]
end
if ::ActiveRecord::VERSION::STRING < '4.2'
# Backport Rails 4.2 and later's `type_for_attribute` to build
# on a common interface.
def type_for_attribute(attr_name)
serialized_attribute_types[attr_name.to_s] || NO_OP_ATTRIBUTE
end
def serialized_attribute_types
@attribute_types ||= Hash[serialized_attributes.map do |attr_name, coder|
[attr_name, SerializedAttribute.new(coder)]
end]
end
private :serialized_attribute_types
end
# Used for `Version#object` attribute.
def serialize_attributes_for_paper_trail!(attributes)
alter_attributes_for_paper_trail!(SERIALIZE, attributes)
end
def unserialize_attributes_for_paper_trail!(attributes)
alter_attributes_for_paper_trail!(DESERIALIZE, attributes)
end
def alter_attributes_for_paper_trail!(serializer, attributes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return attributes if paper_trail_version_class.object_col_is_json?
attributes.each do |key, value|
attributes[key] = type_for_attribute(key).send(serializer, value)
end
end
# Used for Version#object_changes attribute.
def serialize_attribute_changes_for_paper_trail!(changes)
alter_attribute_changes_for_paper_trail!(SERIALIZE, changes)
end
def unserialize_attribute_changes_for_paper_trail!(changes)
alter_attribute_changes_for_paper_trail!(DESERIALIZE, changes)
end
def alter_attribute_changes_for_paper_trail!(serializer, changes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return changes if paper_trail_version_class.object_changes_col_is_json?
changes.clone.each do |key, change|
type = type_for_attribute(key)
changes[key] = Array(change).map { |value| type.send(serializer, value) }
end
end
end
end

View File

@ -5,30 +5,25 @@ module PaperTrail
class Config
include Singleton
attr_accessor :timestamp_field, :serializer, :version_limit
attr_reader :serialized_attributes
attr_writer :track_associations
def initialize
@timestamp_field = :created_at
@serializer = PaperTrail::Serializers::YAML
# This setting only defaults to false on AR 4.2+, because that's when
# it was deprecated. We want it to function with older versions of
# ActiveRecord by default.
if ::ActiveRecord::VERSION::STRING < '4.2'
@serialized_attributes = true
end
end
def serialized_attributes=(value)
if ::ActiveRecord::VERSION::MAJOR >= 5
::ActiveSupport::Deprecation.warn(
"ActiveRecord 5.0 deprecated `serialized_attributes` " +
"without replacement, so this PaperTrail config setting does " +
"nothing with this version, and is always turned off"
def serialized_attributes
ActiveSupport::Deprecation.warn(
"PaperTrail.config.serialized_attributes is deprecated without " \
"replacement and no longer has any effect."
)
end
@serialized_attributes = value
def serialized_attributes=(_)
ActiveSupport::Deprecation.warn(
"PaperTrail.config.serialized_attributes= is deprecated without " \
"replacement and no longer has any effect."
)
end
def track_associations

View File

@ -1,4 +1,5 @@
require 'active_support/core_ext/object' # provides the `try` method
require 'paper_trail/attributes_serialization'
module PaperTrail
module Model
@ -61,6 +62,7 @@ module PaperTrail
# Lazily include the instance methods so we don't clutter up
# any more ActiveRecord models than we have to.
send :include, InstanceMethods
send :extend, AttributesSerialization
class_attribute :version_association_name
self.version_association_name = options[:version] || :version
@ -161,74 +163,6 @@ module PaperTrail
def paper_trail_version_class
@paper_trail_version_class ||= version_class_name.constantize
end
# Used for `Version#object` attribute.
def serialize_attributes_for_paper_trail!(attributes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return attributes if self.paper_trail_version_class.object_col_is_json?
serialized_attributes.each do |key, coder|
if attributes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method.
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
attributes[key] = coder.dump(attributes[key])
end
end
end
# TODO: There is a lot of duplication between this and
# `serialize_attributes_for_paper_trail!`.
def unserialize_attributes_for_paper_trail!(attributes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return attributes if self.paper_trail_version_class.object_col_is_json?
serialized_attributes.each do |key, coder|
if attributes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method.
# TODO: Shouldn't this be `:load`?
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
attributes[key] = coder.load(attributes[key])
end
end
end
# Used for Version#object_changes attribute.
def serialize_attribute_changes_for_paper_trail!(changes)
# Don't serialize before values before inserting into columns of type `JSON`
# on `PostgreSQL` databases.
return changes if self.paper_trail_version_class.object_changes_col_is_json?
serialized_attributes.each do |key, coder|
if changes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method.
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
old_value, new_value = changes[key]
changes[key] = [coder.dump(old_value),
coder.dump(new_value)]
end
end
end
# TODO: There is a lot of duplication between this and
# `serialize_attribute_changes_for_paper_trail!`.
def unserialize_attribute_changes_for_paper_trail!(changes)
# Don't serialize before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
return changes if self.paper_trail_version_class.object_changes_col_is_json?
serialized_attributes.each do |key, coder|
if changes.key?(key)
# Fall back to current serializer if `coder` has no `dump` method.
# TODO: Shouldn't this be `:load`?
coder = PaperTrail.serializer unless coder.respond_to?(:dump)
old_value, new_value = changes[key]
changes[key] = [coder.load(old_value),
coder.load(new_value)]
end
end
end
end
# Wrap the following methods in a module so we can include them only in the
@ -440,9 +374,7 @@ module PaperTrail
def changes_for_paper_trail
_changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
if PaperTrail.serialized_attributes?
self.class.serialize_attribute_changes_for_paper_trail!(_changes)
end
_changes.to_hash
end
@ -562,9 +494,7 @@ module PaperTrail
# ommitting attributes to be skipped.
def object_attrs_for_paper_trail(attributes_hash)
attrs = attributes_hash.except(*self.paper_trail_options[:skip])
if PaperTrail.serialized_attributes?
self.class.serialize_attributes_for_paper_trail!(attrs)
end
attrs
end

View File

@ -59,9 +59,7 @@ module PaperTrail
end
end
if PaperTrail.serialized_attributes?
model.class.unserialize_attributes_for_paper_trail! attrs
end
# Set all the attributes in this version on the model.
attrs.each do |k, v|

View File

@ -260,9 +260,7 @@ module PaperTrail
# @api private
def load_changeset
changes = HashWithIndifferentAccess.new(object_changes_deserialized)
if PaperTrail.serialized_attributes?
item_type.constantize.unserialize_attribute_changes_for_paper_trail!(changes)
end
changes
rescue # TODO: Rescue something specific
{}

View File

@ -43,6 +43,36 @@ class ActiveSupport::TestCase
DatabaseCleaner.clean if using_mysql?
Thread.current[:paper_trail] = nil
end
private
def assert_attributes_equal(expected, actual)
if using_mysql?
expected, actual = expected.dup, actual.dup
# Adjust timestamps for missing fractional seconds precision.
%w(created_at updated_at).each do |timestamp|
expected[timestamp] = expected[timestamp].change(usec: 0)
actual[timestamp] = actual[timestamp].change(usec: 0)
end
end
assert_equal expected, actual
end
def assert_changes_equal(expected, actual)
if using_mysql?
expected, actual = expected.dup, actual.dup
# Adjust timestamps for missing fractional seconds precision.
%w(created_at updated_at).each do |timestamp|
expected[timestamp][1] = expected[timestamp][1].change(usec: 0)
actual[timestamp][1] = actual[timestamp][1].change(usec: 0)
end
end
assert_equal expected, actual
end
end
#

View File

@ -228,8 +228,8 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
'id' => [nil, @widget.id]
}
assert_equal Time, @widget.versions.last.changeset['updated_at'][1].class
assert_equal changes, @widget.versions.last.changeset
assert_kind_of Time, @widget.versions.last.changeset['updated_at'][1]
assert_changes_equal changes, @widget.versions.last.changeset
end
context 'and then updated without any changes' do
@ -375,7 +375,7 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
should 'be available in its previous version' do
assert_equal @widget.id, @reified_widget.id
assert_equal @widget.attributes, @reified_widget.attributes
assert_attributes_equal @widget.attributes, @reified_widget.attributes
end
should 'be re-creatable from its previous version' do
@ -978,16 +978,11 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
end
end
# `serialized_attributes` is deprecated in ActiveRecord 5.0
if ::ActiveRecord::VERSION::MAJOR < 5
context 'When an attribute has a custom serializer' do
setup do
PaperTrail.config.serialized_attributes = true
@person = Person.new(:time_zone => "Samoa")
end
teardown { PaperTrail.config.serialized_attributes = false }
should "be an instance of ActiveSupport::TimeZone" do
assert_equal ActiveSupport::TimeZone, @person.time_zone.class
end
@ -1074,7 +1069,6 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
end
end
end
end
context 'A new model instance which uses a custom PaperTrail::Version class' do

View File

@ -38,7 +38,7 @@ class ProtectedAttrsTest < ActiveSupport::TestCase
# For some reason this test seems to be broken in JRuby 1.9 mode in the
# test env even though it works in the console. WTF?
unless ActiveRecord::VERSION::MAJOR >= 4 && defined?(JRUBY_VERSION)
assert_equal @widget.previous_version.attributes, @initial_attributes
assert_attributes_equal @widget.previous_version.attributes, @initial_attributes
end
end
end