Skip to content

Instantly share code, notes, and snippets.

@reggieb
Last active August 29, 2015 14:08
Show Gist options
  • Select an option

  • Save reggieb/518315ed0cc83c4f6471 to your computer and use it in GitHub Desktop.

Select an option

Save reggieb/518315ed0cc83c4f6471 to your computer and use it in GitHub Desktop.
Paper Trail: restoring associated objects with main object.
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
@reggieb

reggieb commented Nov 6, 2014

Copy link
Copy Markdown
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.

@reggieb

reggieb commented Nov 6, 2014

Copy link
Copy Markdown
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

end

Some of the functionality is tested via the main host object tests, so aren't shown here.

@reggieb

reggieb commented Nov 6, 2014

Copy link
Copy Markdown
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