Merge pull request #667 from kaspth/revive-serialized-attributes
Use Active Record's type system from 4.2 onwards.
This commit is contained in:
commit
6df3532b7f
16
README.md
16
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
{}
|
||||
|
|
|
@ -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
|
||||
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue