===============
Plone Undelete
===============

Plone undelete is an add-on Product for `CMFEditions`_ which adds support for
restoring content that has been deleted from a container.  Containers that
wish to provide this functionality must provide a marker interface,
``IUndeleteContainer``, and must also be adaptable to ``IAnnotatable``.  Let's
make a simple container that provides these interfaces::

    >>> from Products.ploneundelete.tests import FakeContainer
    >>> from Products.ploneundelete.interfaces import IUndeleteContainer
    >>> from zope.app.annotation.interfaces import IAttributeAnnotatable
    >>> from zope.interface import directlyProvides
    >>> folder = FakeContainer()
    >>> directlyProvides(folder, IUndeleteContainer, IAttributeAnnotatable)

Providing this interface allows us to adapt our object to the
``IUndeleteSupport`` interface which provides some utility methods to help
with the undeleletion process.  First we need to add a sub object with some
properties, which we will be deleting and undeleting::

    >>> from Products.ploneundelete.tests import VersionedObject
    >>> folder['new_obj'] = VersionedObject('new_obj')
    >>> child = folder['new_obj']
    >>> child.title = 'Title'
    >>> child.extra = 'extra stuff'

Then we apply our undelete support interface and see how it works.  We start
with the method ``saveObjectInfo``, which is used before objects are deleted to
save metadata about the object, as well as make a permanent snapshot of the
object for undeletion::

    >>> from Products.ploneundelete.interfaces import IUndeleteSupport
    >>> undelete_folder = IUndeleteSupport(folder)
    >>> undelete_folder.saveObjectInfo(child)
    >>> deleted = undelete_folder.listDeleted()
    >>> len(deleted) == 1
    True
    >>> deleted[0]['id']
    'new_obj'
    >>> deleted[0]['title']
    'Title'
    >>> deleted[0]['revision']
    0


Saving the info again will not update the revision::

    >>> undelete_folder.saveObjectInfo(child)
    >>> deleted = undelete_folder.listDeleted()
    >>> len(deleted) == 1
    True
    >>> deleted[0]['revision']
    0

Changing the object will cause a new revision to be saved::

    >>> child.title = 'Updated Title'
    >>> undelete_folder.saveObjectInfo(child)
    >>> deleted = undelete_folder.listDeleted()
    >>> len(deleted) == 1
    True
    >>> deleted[0]['revision']
    1
    >>> deleted[0]['title']
    'Updated Title'
    >>> uid = deleted[0]['uid']

When we delete the object, we should now be able to restore it::

    >>> del folder['new_obj']
    >>> folder.get('new_obj', 'No Object')
    'No Object'
    >>> undelete_folder.undelete(uid)
    >>> child = folder['new_obj']
    >>> child.title
    'Updated Title'
    >>> deleted = undelete_folder.listDeleted()
    >>> len(deleted) == 0
    True

This is all a bit manual so far, which also makes it a bit useless. The
magic comes in because of an event subscriber that's registered to look for
``IObjectWillBeRemovedEvent`` events on ``ICanBeUndeleted`` objects, and
automatically perform saveObjectInfo on them.  So any ``ICanBeUndeleted``
object which has been deleted from an ``IUndeleteContainer`` will
automatically be undeletable::

    >>> from Products.ploneundelete.interfaces import ICanBeUndeleted
    >>> folder['new_obj2'] = VersionedObject('new_obj2')
    >>> child2 = folder['new_obj2']
    >>> directlyProvides(child2, ICanBeUndeleted)
    >>> child2.title = 'My Title for 2'
    >>> child2.extra = 'My Extra for 2'
    >>> del folder['new_obj2']
    >>> folder.get('new_obj2', 'No Object')
    'No Object'
    >>> deleted = undelete_folder.listDeleted()
    >>> len(deleted) == 1
    True
    >>> undelete_folder.undelete(deleted[0]['uid'])
    >>> child2 = folder['new_obj2']
    >>> child2.title
    'My Title for 2'
    >>> child2.extra
    'My Extra for 2'
    >>> deleted = undelete_folder.listDeleted()
    >>> len(deleted) == 0
    True

That's about it.

Notes
=====

Currently ploneundelete requires a specific branch of `CMFEditions`_ from svn:
http://svn.plone.org/svn/collective/CMFEditions/branches/undelete-support

At present turning on ``<five:containerEvents />``, which is needed for
``IObjectWillBeRemovedEvent`` to be fired, in Zope 2.8 will break a number of
critical AT and CMF functions.  So objects that want to be undeletable need to
call saveObjectInfo explicitly in their ``manage_beforeDelete`` methods. :-(

Credits
=======
* **Alec Mitchell** <apm13@columbia.edu> sponsored by `The Open Planning Project`_

* This product would not be possible without `CMFEditions`_

.. _`CMFEditions`: http://www.plone.org/products/cmfeditions
.. _`The Open Planning Project`: http://www.openplans.org
