Last active
August 29, 2015 14:08
-
-
Save reggieb/518315ed0cc83c4f6471 to your computer and use it in GitHub Desktop.
Paper Trail: restoring associated objects with main object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class VersionStruct | |
| class << self | |
| def build_from(version, args = {}) | |
| object = deserialize(version.object) | |
| args[:changes] = deserialize(version.object_changes) | |
| args[:version] = version | |
| version_struct = new(object, args) | |
| version_struct.mimic | |
| end | |
| def deserialize(text) | |
| PaperTrail.serializer.load(text) if text | |
| end | |
| end | |
| attr_reader :object, :changes, :version, :serialized, :associations, :args, :object_class | |
| def initialize(object, args = {}) | |
| @object = ihash(object) | |
| @changes = ihash(args[:changes] || {}) | |
| @version = args[:version] | |
| @serialized = args[:serialized] || [] | |
| @associations = ihash(args[:associations] || {}) | |
| @object_class = self.class.const_get(args[:object_class]) if args[:object_class] | |
| @args = args | |
| end | |
| def mimic | |
| @mimic ||= build_mimic | |
| end | |
| private | |
| class MimicStruct < OpenStruct | |
| def human_attribute_name(name) | |
| object_class.human_attribute_name(name) if object_class | |
| end | |
| end | |
| def empty_item | |
| MimicStruct.new( | |
| object_class: object_class.name | |
| ) | |
| end | |
| def build_mimic | |
| MimicStruct.new( | |
| mimic_attributes | |
| ) | |
| end | |
| def mimic_attributes | |
| attrs = object_or_before_change | |
| attrs[:changes] = deserialized_changes | |
| attrs[:version] = version if version | |
| add_associations(attrs) | |
| attrs[:object_class] = object_class | |
| attrs | |
| end | |
| def deserialized_changes | |
| return changes if serialized.empty? | |
| deserialized_changes = changes.clone | |
| serialized.each do |attribute| | |
| next unless changes[attribute] | |
| deserialized_changes[attribute] = changes[attribute].collect{|a| YAML.load(a) if a} | |
| end | |
| deserialized_changes | |
| end | |
| def deserialized_object | |
| deserialized_object = object.clone | |
| return deserialized_object if serialized.empty? | |
| serialized.each do |attribute| | |
| deserialized_object[attribute] = YAML.load(object[attribute]) if object[attribute] | |
| end | |
| deserialized_object | |
| end | |
| def object_or_before_change | |
| before_change.merge(deserialized_object) | |
| end | |
| def before_change | |
| deserialized_changes.inject({}) do |hash, key_value| | |
| attribute, changes = key_value | |
| hash[attribute] = changes.first | |
| hash | |
| end | |
| end | |
| def add_associations(attrs) | |
| associations.each do |association, objects| | |
| mimics = objects.keys.collect do |object_id| | |
| version_id = objects[object_id] | |
| object_version = PaperTrail::Version.find_by(id: version_id) | |
| if version_id and object_version | |
| self.class.build_from(object_version, object_class: object_version.item_type) | |
| else | |
| get_item_or_restore_deleted(association, object_id) | |
| end | |
| end | |
| attrs[association] = single_association?(association) ? mimics.first : mimics.compact | |
| end | |
| end | |
| def single_association?(association) | |
| [:belongs_to, :has_one].include? reflection_on(association).try(:macro) | |
| end | |
| def get_item_or_restore_deleted(association, object_id) | |
| get_item(association, object_id) || rebuild_deleted(association, object_id) | |
| end | |
| def get_item(association, object_id) | |
| object_association = version.item.send(association) | |
| return object_association.reify if object_association.respond_to?(:reify) | |
| current = object_association.find_by(id: object_id) | |
| return unless current | |
| at_change = current.version_at(version.created_at) | |
| at_change || current | |
| end | |
| def rebuild_deleted(association, object_id) | |
| reflection = reflection_on(association) | |
| return empty_item unless reflection | |
| version = PaperTrail::Version.order('created_at DESC').find_by(item_type: reflection.klass, item_id: object_id) | |
| return empty_item unless version | |
| version.reify | |
| end | |
| def reflection_on(association) | |
| if version | |
| version.item.class.reflect_on_association(association.to_sym) | |
| else | |
| object_class.reflect_on_association(association.to_sym) | |
| end | |
| end | |
| def ihash(hash = {}) | |
| ActiveSupport::HashWithIndifferentAccess.new hash | |
| end | |
| end |
Author
Author
The human_attribute_name methods is called on any object, to get the matching i18n translation for an attribute, so that I can easily apply labels and place holders in a consistent way from both original objects and OpenStruct versions.
Author
Here is are my tests for VersionStruct:
require 'test_helper'
class VersionStructTest < ActiveSupport::TestCase
def test_initialize
assert_equal ihash(object), version_struct.object
assert_equal ihash(changes), version_struct.changes
end
def test_mimic
object.each do |method, value|
assert_equal value, mimic.send(method), "mimic.#{method} should equal #{value}"
end
end
def test_mimic_without_change
@version_struct = VersionStruct.new(object)
test_mimic
assert_equal({}, mimic.changes)
end
def test_change
assert_equal changes[:foo], mimic.changes[:foo]
end
def test_change_with_serialized_attribute
@version_struct = VersionStruct.new(object, changes: changes, serialized: [:bar])
assert_equal [], mimic.changes[:bar].first
assert_equal [], mimic.bar
end
def test_build_from
@mimic = VersionStruct.build_from(version)
assert_equal before, mimic.title
assert_equal [before, after], mimic.changes[:title]
assert_equal version, mimic.version
end
def test_build_from_with_associations
@version_struct = VersionStruct.new(object, associations: associations)
assert_equal before, mimic.work_requests.first.title
end
def test_human_attribute_name
@version_struct = VersionStruct.new(object, object_class: 'WorkRequest')
assert_equal(
WorkRequest.human_attribute_name(:title),
mimic.human_attribute_name(:title)
)
end
def version_struct
@version_struct ||= VersionStruct.new(object, changes: changes)
end
def mimic
@mimic ||= version_struct.mimic
end
def object
@object ||= {
foo: after,
'this' => 'that',
number: 1,
bar: "--- []\n"
}
end
def changes
@changes ||= {
foo: [before, after],
bar: ["--- []\n", "---\n- One\n"]
}
end
def before
'before'
end
def after
'after'
end
def version
@version ||= build_version
end
def build_version
work_request.update_attribute(:title, before)
work_request.update_attribute(:title, after)
work_request.versions.last
end
def associations
@associations ||= build_associations
end
def build_associations
build_version
{work_requests: {work_request.id => work_request.versions.last.id}}
end
def ihash(hash = {})
ActiveSupport::HashWithIndifferentAccess.new hash
end
endSome of the functionality is tested via the main host object tests, so aren't shown here.
Author
This work started live via paper-trail-gem/paper_trail#433
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
VersionStruct allows me to recover objects that behave like associated objects as they existed at the time a main object was saved. To store the data used to rebuild these objects, this code is added to the main object:
All the models that I want to version, inherit from this abstract class (including the core class):
Then on the main object, I redefine reify like this:
The stakeholders field is serialized, and therefore needs some extra processing.
With this in place, reify generates a modified OpenStruct object, with methods that match the original objects attributes, and return the values matching those attributes at the time the version was saved. A method also exists that matches each association, and this holds (for has_many) an array of OpenStruct objects that match the associated objects at the version time. There is also a changed method, that contains [before, after] pairs for any OpenStuct object where the relevant version changed.