[z3-checkins] r18938 - in z3/Five/branch/efge-object-event: . doc tests

efge at codespeak.net efge at codespeak.net
Tue Oct 25 16:26:56 CEST 2005


Author: efge
Date: Tue Oct 25 16:26:56 2005
New Revision: 18938

Added:
   z3/Five/branch/efge-object-event/event.py   (contents, props changed)
   z3/Five/branch/efge-object-event/event.zcml   (contents, props changed)
Modified:
   z3/Five/branch/efge-object-event/doc/directives.txt
   z3/Five/branch/efge-object-event/doc/features.txt
   z3/Five/branch/efge-object-event/eventconfigure.py
   z3/Five/branch/efge-object-event/fivedirectives.py
   z3/Five/branch/efge-object-event/meta.zcml
   z3/Five/branch/efge-object-event/tests/event.txt
   z3/Five/branch/efge-object-event/tests/test_event.py
Log:
The standard Zope 2 containers now send events. <five:sendEvents> is
removed, and replaced by more <five:containerEvents> and
<five:deprecatedManageAddDelete>

This is a work in progress, some XXX remain, notably:
 - add event listeners for _notifyOfCopyTo and _postCopy
 - add a subscriber for manage_afterClone emulation
 - add subscribers for executable ownership and Owner role fixups
More doc will also be included.

Finally, this includes a "transitional" phase, which will only be used
during development but will be removed when things have been suitably
tested with Zope and CMF, and deprecated classes have been converted or
identified.



Modified: z3/Five/branch/efge-object-event/doc/directives.txt
==============================================================================
--- z3/Five/branch/efge-object-event/doc/directives.txt	(original)
+++ z3/Five/branch/efge-object-event/doc/directives.txt	Tue Oct 25 16:26:56 2005
@@ -164,12 +164,19 @@
 Retrieve size information for a Zope 2 content class via a Zope 3
 style ``ISized`` adapter.
 
-sendEvents
-----------
+containerEvents
+---------------
 
-Lets a Zope 2 content class send out Zope 3 object events that
-correspond to the Zope 2 methods ``manage_afterAdd`` and
-``manage_beforeDelete``.
+Make events be sent for Zope 2 container objects, instead of calling old
+methods like ``manage_afterAdd``. These old methods will still be called
+for classes specified in a ``deprecatedManageAddDelete`` directive.
+
+deprecatedManageAddDelete
+-------------------------
+
+Specify a class that needs its old deprecated methods like
+``manage_afterAdd``, ``manage_beforeDelete`` and ``manage_afterClone``
+to be called. Modern classes should use event subscribers instead.
 
 pagesFromDirectory
 ------------------

Modified: z3/Five/branch/efge-object-event/doc/features.txt
==============================================================================
--- z3/Five/branch/efge-object-event/doc/features.txt	(original)
+++ z3/Five/branch/efge-object-event/doc/features.txt	Tue Oct 25 16:26:56 2005
@@ -103,3 +103,10 @@
 
     from Products.Five.localsite import enableLocalSiteHook
     enableLocalSiteHook(obj)
+
+Object events
+=============
+
+Five supports sending Zope 3 object events when objects are added,
+moved, renamed, copied and deleted. The use of ``manage_afterAdd`` & co
+methods is deprecated.

Added: z3/Five/branch/efge-object-event/event.py
==============================================================================
--- (empty file)
+++ z3/Five/branch/efge-object-event/event.py	Tue Oct 25 16:26:56 2005
@@ -0,0 +1,751 @@
+##############################################################################
+#
+# Copyright (c) 2004, 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Five event definitions.
+
+May eventually be folded back into Zope 3 proper.
+
+$Id$
+"""
+
+import warnings
+
+from zope.event import notify
+from zope.interface import implements
+from zope.interface import Attribute
+
+from zope.app.event.interfaces import IObjectEvent
+from zope.app.container.interfaces import IObjectAddedEvent
+from zope.app.container.interfaces import IObjectRemovedEvent
+
+from zope.app.event.objectevent import ObjectEvent
+from zope.app.container.contained import ObjectMovedEvent
+from zope.app.container.contained import ObjectAddedEvent
+from zope.app.container.contained import ObjectRemovedEvent
+from zope.app.event.objectevent import ObjectCopiedEvent
+
+from Products.Five.fiveconfigure import isFiveMethod
+
+
+class IObjectWillBeMovedEvent(IObjectEvent):
+    """An object will be moved."""
+    oldParent = Attribute("The old location parent for the object.")
+    oldName = Attribute("The old location name for the object.")
+    newParent = Attribute("The new location parent for the object.")
+    newName = Attribute("The new location name for the object.")
+
+class IObjectWillBeAddedEvent(IObjectWillBeMovedEvent):
+    """An object will be added to a container."""
+
+class IObjectWillBeRemovedEvent(IObjectWillBeMovedEvent):
+    """An object will be removed from a container"""
+
+
+class ObjectWillBeMovedEvent(ObjectEvent):
+    """An object will be moved"""
+    implements(IObjectWillBeMovedEvent)
+
+    def __init__(self, object, oldParent, oldName, newParent, newName):
+        ObjectEvent.__init__(self, object)
+        self.oldParent = oldParent
+        self.oldName = oldName
+        self.newParent = newParent
+        self.newName = newName
+
+class ObjectWillBeAddedEvent(ObjectWillBeMovedEvent):
+    """An object will be added to a container"""
+    implements(IObjectWillBeAddedEvent)
+
+    def __init__(self, object, newParent=None, newName=None):
+        #if newParent is None:
+        #    newParent = object.__parent__
+        #if newName is None:
+        #    newName = object.__name__
+        ObjectWillBeMovedEvent.__init__(self, object, None, None,
+                                        newParent, newName)
+
+class ObjectWillBeRemovedEvent(ObjectWillBeMovedEvent):
+    """An object will be removed from a container"""
+    implements(IObjectWillBeRemovedEvent)
+
+    def __init__(self, object, oldParent=None, oldName=None):
+        #if oldParent is None:
+        #    oldParent = object.__parent__
+        #if oldName is None:
+        #    oldName = object.__name__
+        ObjectWillBeMovedEvent.__init__(self, object, oldParent, oldName,
+                                        None, None)
+
+
+##################################################
+
+import sys
+from cgi import escape
+from zLOG import LOG, ERROR
+from Acquisition import aq_base, aq_parent, aq_inner
+from App.config import getConfiguration
+from App.Dialogs import MessageDialog
+from AccessControl import getSecurityManager
+from ZODB.POSException import ConflictError
+from OFS.ObjectManager import BeforeDeleteException
+from OFS import Moniker
+from OFS.CopySupport import CopyError # Yuck, a string exception
+from OFS.CopySupport import eNoData, eNotFound, eInvalid, _cb_decode
+from OFS.CopySupport import cookie_path, sanity_check
+from webdav.Lockable import ResourceLockedError
+FIVE_ORIGINAL_PREFIX = '__five_original_'
+
+
+containerEventsTransitional = None
+containerEventAwareClasses = []
+deprecatedManageAddDeleteClasses = []
+
+
+def callOldMethods(ob):
+    """Check if we need to call the old methods.
+    """
+    if containerEventsTransitional:
+        for class_ in containerEventAwareClasses:
+            if isinstance(ob, class_):
+                return False
+        return True
+    else:
+        for class_ in deprecatedManageAddDeleteClasses:
+            if isinstance(ob, class_):
+                return True
+        return False
+
+##################################################
+# Adapters and subscribers
+
+from OFS.interfaces import IObjectManager
+from zope.app.location.interfaces import ISublocations
+
+class ObjectManagerSublocations(object):
+    """Get the sublocations for an ObjectManager.
+    """
+    #__used_for__ = IObjectManager
+    #implements(ISublocations)
+
+    def __init__(self, container):
+        self.container = container
+
+    def sublocations(self):
+        for ob in self.container.objectValues():
+            yield ob
+
+
+def callManageAfterAdd(ob, event):
+    """Compatibility subscriber for manage_afterAdd.
+    """
+    if not callOldMethods(ob):
+        return
+    container = event.newParent
+    if container is None:
+        return
+    item = event.object
+    if not isFiveMethod(ob.manage_afterAdd):
+        warnings.warn(
+            "Calling %s.manage_afterAdd is deprecated when using Five, "
+            "use an IObjectAddedEvent subscriber instead."
+            % ob.__class__.__name__,
+            DeprecationWarning)
+    ob.manage_afterAdd(item, container)
+
+
+def callManageBeforeDelete(ob, event):
+    """Compatibility subscriber for manage_beforeDelete.
+    """
+    if not callOldMethods(ob):
+        return
+    container = event.oldParent
+    if container is None:
+        return
+    item = event.object
+    if not isFiveMethod(ob.manage_beforeDelete):
+        warnings.warn(
+            "Calling %s.manage_beforeDelete is deprecated "
+            "when using Five, "
+            "use an IObjectWillBeRemovedEvent subscriber instead."
+            % ob.__class__.__name__,
+            DeprecationWarning)
+    try:
+        ob.manage_beforeDelete(item, container)
+    except BeforeDeleteException:
+        raise
+    except ConflictError:
+        raise
+    except:
+        LOG('Zope', ERROR, '_delObject() threw', error=sys.exc_info())
+        # In debug mode when non-Manager, let exceptions propagate.
+        if getConfiguration().debug_mode:
+            if not getSecurityManager().getUser().has_role('Manager'):
+                raise
+
+def callManageAfterClone(ob, event):
+    """Compatibility subscriber for manage_afterClone.
+    """
+    if not callOldMethods(ob):
+        return
+    container = event.newParent
+    if container is None:
+        return
+    item = event.object
+    if not isFiveMethod(ob.manage_afterAdd):
+        warnings.warn(
+            "Calling %s.manage_afterAdd is deprecated when using Five, "
+            "use an IObjectAddedEvent subscriber instead."
+            % ob.__class__.__name__,
+            DeprecationWarning)
+    ob.manage_afterAdd(item, container)
+
+
+
+
+##################################################
+# Monkey patches
+
+# From ObjectManager
+def manage_afterAdd(self, item, container):
+    # Don't do recursion anymore, a subscriber does that.
+    # A warning is sent by the subscriber
+    pass
+
+# From ObjectManager
+def manage_beforeDelete(self, item, container):
+    # Don't do recursion anymore, a subscriber does that.
+    # A warning is sent by the subscriber
+    pass
+
+# From ObjectManager
+def manage_afterClone(self, item):
+    # Don't do recursion anymore, a subscriber does that.
+    # A warning is sent by the subscriber
+    pass
+
+# From CopyContainer
+def _notifyOfCopyTo(self, container, op=0):
+    # A warning is sent by the caller
+    pass
+
+# From CopyContainer
+def _postCopy(self, container, op=0):
+    # A warning is sent by the caller
+    pass
+
+
+# XXX other recursive dispatching:
+# changeOwnership (called by manage_takeOwnership)
+# manage_fixupOwnershipAfterAdd
+# _deleteOwnershipAfterAdd (called by manage_fixupOwnershipAfterAdd)
+
+
+# From ObjectManager
+def _setObject(self, id, object, roles=None, user=None, set_owner=1,
+               suppress_events=False):
+    """Set an object into this container.
+
+    Also sends IObjectAddedEvent.
+    """
+    v = self._checkId(id)
+    if v is not None:
+        id = v
+    t = getattr(object, 'meta_type', None)
+
+    # If an object by the given id already exists, remove it.
+    for object_info in self._objects:
+        if object_info['id'] == id:
+            self._delObject(id)
+            break
+
+    if not suppress_events:
+        notify(ObjectWillBeAddedEvent(object, self, id))
+
+    self._objects = self._objects + ({'id': id, 'meta_type': t},)
+    self._setOb(id, object)
+    object = self._getOb(id)
+
+    if not suppress_events:
+        notify(ObjectAddedEvent(object, self, id))
+
+    # XXX eventify manage_fixupOwnershipAfterAdd and local role Owner
+    if 0:
+        if set_owner:
+            object.manage_fixupOwnershipAfterAdd()
+
+            # Try to give user the local role "Owner", but only if
+            # no local roles have been set on the object yet.
+            if hasattr(object, '__ac_local_roles__'):
+                if object.__ac_local_roles__ is None:
+                    user = getSecurityManager().getUser()
+                    if user is not None:
+                        userid = user.getId()
+                        if userid is not None:
+                            object.manage_setLocalRoles(userid, ['Owner'])
+
+    # manage_afterAdd was here
+
+    return id
+
+
+# From BTreeFolder2
+def BT_setObject(self, id, object, roles=None, user=None, set_owner=1,
+                 suppress_events=False):
+    v = self._checkId(id)
+    if v is not None:
+        id = v
+
+    # If an object by the given id already exists, remove it.
+    if self.has_key(id):
+        self._delObject(id)
+
+    if not suppress_events:
+        notify(ObjectWillBeAddedEvent(object, self, id))
+
+    self._setOb(id, object)
+    object = self._getOb(id)
+
+    if not suppress_events:
+        notify(ObjectAddedEvent(object, self, id))
+
+    # XXX eventify manage_fixupOwnershipAfterAdd and local role Owner
+    if 0:
+        if set_owner:
+            object.manage_fixupOwnershipAfterAdd()
+
+            # Try to give user the local role "Owner", but only if
+            # no local roles have been set on the object yet.
+            if hasattr(object, '__ac_local_roles__'):
+                if object.__ac_local_roles__ is None:
+                    user = getSecurityManager().getUser()
+                    if user is not None:
+                        userid = user.getId()
+                        if userid is not None:
+                            object.manage_setLocalRoles(userid, ['Owner'])
+
+    # manage_afterAdd was here
+
+    return id
+
+
+# From ObjectManager
+def _delObject(self, id, dp=1, suppress_events=False):
+    """Delete an object from this container.
+
+    Also sends IObjectRemovedEvent.
+    """
+    ob = self._getOb(id)
+
+    # manage_beforeDelete was here
+
+    if not suppress_events:
+        notify(ObjectWillBeRemovedEvent(ob, self, id))
+
+    self._objects = tuple([i for i in self._objects
+                           if i['id'] != id])
+    self._delOb(id)
+
+    # Indicate to the object that it has been deleted. This is
+    # necessary for object DB mount points. Note that we have to
+    # tolerate failure here because the object being deleted could
+    # be a Broken object, and it is not possible to set attributes
+    # on Broken objects.
+    try:
+        ob._v__object_deleted__ = 1
+    except:
+        pass
+
+    if not suppress_events:
+        notify(ObjectRemovedEvent(ob, self, id))
+
+
+# From BTreeFolder2
+def BT_delObject(self, id, dp=1, suppress_events=False):
+    ob = self._getOb(id)
+
+    # manage_beforeDelete was here
+
+    if not suppress_events:
+        notify(ObjectWillBeRemovedEvent(ob, self, id))
+
+    self._delOb(id)
+
+    if not suppress_events:
+        notify(ObjectRemovedEvent(ob, self, id))
+
+# From CopyContainer
+def manage_renameObject(self, id, new_id, REQUEST=None):
+    """Rename a particular sub-object.
+    """
+    try:
+        self._checkId(new_id)
+    except:
+        raise CopyError, MessageDialog(
+            title='Invalid Id',
+            message=sys.exc_info()[1],
+            action ='manage_main')
+
+    ob = self._getOb(id)
+
+    if ob.wl_isLocked():
+        raise ResourceLockedError, ('Object "%s" is locked via WebDAV'
+                                    % ob.getId())
+    if not ob.cb_isMoveable():
+        raise CopyError, eNotSupported % escape(id)
+    self._verifyObjectPaste(ob)
+
+    if callOldMethods(ob):
+        # BBB
+        if not isFiveMethod(ob._notifyOfCopyTo):
+            warnings.warn(
+                "Calling %s._notifyOfCopyTo is deprecated when using Five, "
+                "use an IObjectWillBeMovedEvent subscriber "
+                "instead." % ob.__class__.__name__,
+                DeprecationWarning, stacklevel=2)
+        try:
+            ob._notifyOfCopyTo(self, op=1)
+        except:
+            raise CopyError, MessageDialog(
+                title='Rename Error',
+                message=sys.exc_info()[1],
+                action ='manage_main')
+
+    notify(ObjectWillBeMovedEvent(ob, self, id, self, new_id))
+
+    self._delObject(id, suppress_events=True)
+    ob = aq_base(ob)
+    ob._setId(new_id)
+
+    # Note - because a rename always keeps the same context, we
+    # can just leave the ownership info unchanged.
+    self._setObject(new_id, ob, set_owner=0, suppress_events=True)
+    ob = self._getOb(new_id)
+
+    notify(ObjectMovedEvent(ob, self, id, self, new_id))
+
+    if callOldMethods(ob):
+        # BBB
+        if not isFiveMethod(ob._postCopy):
+            warnings.warn(
+                "Calling %s._postCopy(op=1) is deprecated when using Five, "
+                "use an IObjectMovedEvent subscriber instead."
+                % ob.__class__.__name__,
+                DeprecationWarning, stacklevel=2)
+        ob._postCopy(self, op=1)
+
+    if REQUEST is not None:
+        return self.manage_main(self, REQUEST, update_menu=1)
+    return None
+
+
+# From CopyContainer
+def manage_pasteObjects(self, cb_copy_data=None, REQUEST=None):
+    """Paste previously copied objects into the current object.
+
+    If calling manage_pasteObjects from python code, pass the result of a
+    previous call to manage_cutObjects or manage_copyObjects as the first
+    argument.
+
+    Also sends IObjectCopiedEvent or IObjectMovedEvent.
+    """
+    if cb_copy_data is not None:
+        cp = cb_copy_data
+    elif REQUEST is not None and REQUEST.has_key('__cp'):
+        cp = REQUEST['__cp']
+    else:
+        cp = None
+    if cp is None:
+        raise CopyError, eNoData
+
+    try:
+        op, mdatas = _cb_decode(cp)
+    except:
+        raise CopyError, eInvalid
+
+    oblist = []
+    app = self.getPhysicalRoot()
+    for mdata in mdatas:
+        m = Moniker.loadMoniker(mdata)
+        try:
+            ob = m.bind(app)
+        except ConflictError:
+            raise
+        except:
+            raise CopyError, eNotFound
+        self._verifyObjectPaste(ob, validate_src=op+1)
+        oblist.append(ob)
+
+    result = []
+    if op == 0:
+        # Copy operation
+        for ob in oblist:
+            orig_id = ob.getId()
+            if not ob.cb_isCopyable():
+                raise CopyError, eNotSupported % escape(orig_id)
+
+            try:
+                if callOldMethods(ob):
+                    # BBB
+                    if not isFiveMethod(ob._notifyOfCopyTo):
+                        warnings.warn(
+                            "Calling %s._notifyOfCopyTo is deprecated "
+                            "when using Five, use an IObjectCopiedEvent or "
+                            "IObjectWillBeAddedEvent subscriber instead."
+                            % ob.__class__.__name__,
+                            DeprecationWarning, stacklevel=2)
+                    ob._notifyOfCopyTo(self, op=0)
+            except ConflictError:
+                raise
+            except:
+                raise CopyError, MessageDialog(
+                    title="Copy Error",
+                    message=sys.exc_info()[1],
+                    action='manage_main')
+
+            id = self._get_id(orig_id)
+            result.append({'id': orig_id, 'new_id': id})
+
+            ob = ob._getCopy(self)
+            ob._setId(id)
+            notify(ObjectCopiedEvent(ob))
+
+            self._setObject(id, ob)
+            ob = self._getOb(id)
+            ob.wl_clearLocks()
+
+            if callOldMethods(ob):
+                # BBB
+                if not isFiveMethod(ob._postCopy):
+                    warnings.warn(
+                        "Calling %s._postCopy(op=0) is deprecated "
+                        "when using Five, "
+                        "use an IObjectCopiedEvent subscriber instead."
+                        % ob.__class__.__name__,
+                        DeprecationWarning, stacklevel=2)
+                ob._postCopy(self, op=0)
+                if not isFiveMethod(ob.manage_afterClone):
+                    warnings.warn(
+                        "Calling %s.manage_afterClone is deprecated "
+                        "when using Five, "
+                        "use an IObjectCopiedEvent subscriber instead."
+                        % self.__class__.__name__,
+                        DeprecationWarning, stacklevel=2)
+                ob.manage_afterClone(ob)
+
+        if REQUEST is not None:
+            return self.manage_main(self, REQUEST, update_menu=1,
+                                    cb_dataValid=1)
+
+    elif op == 1:
+        # Move operation
+        for ob in oblist:
+            orig_id = ob.getId()
+            if not ob.cb_isMoveable():
+                raise CopyError, eNotSupported % escape(orig_id)
+
+            try:
+                if callOldMethods(ob):
+                    # BBB
+                    if not isFiveMethod(ob._notifyOfCopyTo):
+                        warnings.warn(
+                            "Calling %s._notifyOfCopyTo is deprecated "
+                            "when using Five, "
+                            "use an IObjectWillBeMovedEvent subscriber "
+                            "instead." % ob.__class__.__name__,
+                            DeprecationWarning, stacklevel=2)
+                    ob._notifyOfCopyTo(self, op=1)
+            except ConflictError:
+                raise
+            except:
+                raise CopyError, MessageDialog(
+                    title="Move Error",
+                    message=sys.exc_info()[1],
+                    action='manage_main')
+            if not sanity_check(self, ob):
+                raise CopyError, "This object cannot be pasted into itself"
+
+            orig_container = aq_parent(aq_inner(ob))
+            if aq_base(orig_container) is aq_base(self):
+                id = orig_id
+            else:
+                id = self._get_id(orig_id)
+            result.append({'id': orig_id, 'new_id': id})
+
+            notify(ObjectWillBeMovedEvent(ob, orig_container, orig_id,
+                                          self, id))
+
+            # try to make ownership explicit so that it gets carried
+            # along to the new location if needed.
+            ob.manage_changeOwnershipType(explicit=1) # XXX event?
+
+            orig_container._delObject(orig_id, suppress_events=True)
+            ob = aq_base(ob)
+            ob._setId(id)
+
+            self._setObject(id, ob, set_owner=0, suppress_events=True)
+            ob = self._getOb(id)
+
+            notify(ObjectMovedEvent(ob, orig_container, orig_id,
+                                    self, id))
+
+            if callOldMethods(ob):
+                # BBB
+                if not isFiveMethod(ob._postCopy):
+                    warnings.warn(
+                        "Calling %s._postCopy(op=1) is deprecated "
+                        "when using Five, "
+                        "use an IObjectMovedEvent subscriber instead."
+                        % ob.__class__.__name__,
+                        DeprecationWarning, stacklevel=2)
+                ob._postCopy(self, op=1)
+
+            # try to make ownership implicit if possible
+            ob.manage_changeOwnershipType(explicit=0) # XXX event?
+
+        if REQUEST is not None:
+            REQUEST['RESPONSE'].setCookie('__cp', 'deleted',
+                                path='%s' % cookie_path(REQUEST),
+                                expires='Wed, 31-Dec-97 23:59:59 GMT')
+            REQUEST['__cp'] = None
+            return self.manage_main(self, REQUEST, update_menu=1,
+                                    cb_dataValid=0)
+
+    return result
+
+# From CopyContainer
+def manage_clone(self, ob, id, REQUEST=None):
+    """Clone an object, creating a new object with the given id.
+    """
+    if not ob.cb_isCopyable():
+        raise CopyError, eNotSupported % escape(ob.getId())
+    try:
+        self._checkId(id)
+    except:
+        raise CopyError, MessageDialog(
+            title='Invalid Id',
+            message=sys.exc_info()[1],
+            action ='manage_main')
+
+    self._verifyObjectPaste(ob)
+
+    if callOldMethods(ob):
+        # BBB
+        if not isFiveMethod(ob._notifyOfCopyTo):
+            warnings.warn(
+                "Calling %s._notifyOfCopyTo is deprecated when using Five, "
+                "use an IObjectCopiedEvent or IObjectWillBeAddedEvent "
+                "subscriber instead." % ob.__class__.__name__,
+                DeprecationWarning, stacklevel=2)
+        try:
+            ob._notifyOfCopyTo(self, op=0)
+        except:
+            raise CopyError, MessageDialog(
+                title='Clone Error',
+                message=sys.exc_info()[1],
+                action ='manage_main')
+
+    ob = ob._getCopy(self)
+    ob._setId(id)
+    notify(ObjectCopiedEvent(ob))
+
+    self._setObject(id, ob)
+    ob = self._getOb(id)
+
+    if callOldMethods(ob):
+        # BBB
+        if not isFiveMethod(ob._postCopy):
+            warnings.warn(
+                "Calling %s._postCopy(op=0) is deprecated when using Five, "
+                "use an IObjectCopiedEvent subscriber instead."
+                % ob.__class__.__name__,
+                DeprecationWarning, stacklevel=2)
+        ob._postCopy(self, op=0)
+        if not isFiveMethod(ob.manage_afterClone):
+            warnings.warn(
+                "Calling %s.manage_afterClone is deprecated when using Five, "
+                "use an IObjectCopiedEvent subscriber instead."
+                % self.__class__.__name__,
+                DeprecationWarning, stacklevel=2)
+        ob.manage_afterClone(ob)
+
+    return ob
+
+##################################################
+# Structured monkey-patching
+
+import Products.Five
+from Products.Five import zcml
+from Products.Five.fiveconfigure import killMonkey
+from zope.testing.cleanup import addCleanUp
+
+_monkied = []
+
+from OFS.ObjectManager import ObjectManager
+from OFS.CopySupport import CopyContainer
+from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2Base
+
+def doMonkies(transitional):
+    """Monkey patch various methods to provide container events.
+    """
+    global containerEventsTransitional
+    containerEventsTransitional = transitional
+
+    patchMethod(ObjectManager, '_setObject',
+                _setObject)
+    patchMethod(ObjectManager, '_delObject',
+                _delObject)
+    patchMethod(ObjectManager, 'manage_afterAdd',
+                manage_afterAdd)
+    patchMethod(ObjectManager, 'manage_beforeDelete',
+                manage_beforeDelete)
+    patchMethod(ObjectManager, 'manage_afterClone',
+                manage_afterClone)
+
+    patchMethod(BTreeFolder2Base, '_setObject',
+                BT_setObject)
+    patchMethod(BTreeFolder2Base, '_delObject',
+                BT_delObject)
+
+    patchMethod(CopyContainer, '_notifyOfCopyTo',
+                _notifyOfCopyTo)
+    patchMethod(CopyContainer, '_postCopy',
+                _postCopy)
+    patchMethod(CopyContainer, 'manage_renameObject',
+                manage_renameObject)
+    # XXX also fix OrderSupport._old_manage_renameObject
+    patchMethod(CopyContainer, 'manage_pasteObjects',
+                manage_pasteObjects)
+    patchMethod(CopyContainer, 'manage_clone',
+                manage_clone)
+
+    zcml.load_config('event.zcml', Products.Five)
+
+    addCleanUp(undoMonkies)
+
+def patchMethod(class_, name, new_method):
+    method = getattr(class_, name, None)
+    if isFiveMethod(method):
+        return
+    setattr(class_, '__five_original_' + name, method)
+    setattr(class_, name, new_method)
+    new_method.__five_method__ = True
+    _monkied.append((class_, name))
+
+def undoMonkies():
+    """Undo monkey patches.
+    """
+    for class_, name in _monkied:
+        killMonkey(class_, name, FIVE_ORIGINAL_PREFIX + name)
+    containerEventAwareClasses[:] = []
+    deprecatedManageAddDeleteClasses[:] = []

Added: z3/Five/branch/efge-object-event/event.zcml
==============================================================================
--- (empty file)
+++ z3/Five/branch/efge-object-event/event.zcml	Tue Oct 25 16:26:56 2005
@@ -0,0 +1,64 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+  <adapter
+      for="OFS.interfaces.IObjectManager"
+      provides="zope.app.location.interfaces.ISublocations"
+      factory=".event.ObjectManagerSublocations"
+      />
+
+  <!-- Ordering of the following subscribers is important -->
+
+  <!-- dispatch IObjectWillBeMovedEvent to sublocation before
+       compatibility calls, to have "bottom-up" semantics -->
+
+  <subscriber
+      for="OFS.interfaces.IObjectManager
+           .event.IObjectWillBeMovedEvent"
+      factory="zope.app.container.contained.dispatchToSublocations">
+      Handler dispatches moved events to sublocations of any ObjectManager.
+  </subscriber>
+
+  <!-- manage_beforeDelete compatibility -->
+
+  <subscriber
+      for="OFS.interfaces.ISimpleItem
+           .event.IObjectWillBeMovedEvent"
+      factory=".event.callManageBeforeDelete"
+      />
+
+  <subscriber
+      for="OFS.interfaces.IObjectManager
+           .event.IObjectWillBeMovedEvent"
+      factory=".event.callManageBeforeDelete"
+      />
+
+  <!-- manage_afterAdd compatibility -->
+
+  <subscriber
+      for="OFS.interfaces.ISimpleItem
+           zope.app.container.interfaces.IObjectMovedEvent"
+      factory=".event.callManageAfterAdd"
+      />
+
+  <subscriber
+      for="OFS.interfaces.IObjectManager
+           zope.app.container.interfaces.IObjectMovedEvent"
+      factory=".event.callManageAfterAdd"
+      />
+
+  <!-- dispatch IObjectMovedEvent to sublocations after
+       compatibility calls, to have "top-down" semantics -->
+
+  <subscriber
+      for="OFS.interfaces.IObjectManager
+           zope.app.container.interfaces.IObjectMovedEvent"
+      factory="zope.app.container.contained.dispatchToSublocations">
+      Handler dispatches moved events to sublocations of any ObjectManager.
+  </subscriber>
+
+  <!-- XXX dispatcher for manage_afterClone -->
+
+  <!-- XXX subscriber (not multi) for _notifOfCopyTo and _postCopy -->
+
+
+</configure>

Modified: z3/Five/branch/efge-object-event/eventconfigure.py
==============================================================================
--- z3/Five/branch/efge-object-event/eventconfigure.py	(original)
+++ z3/Five/branch/efge-object-event/eventconfigure.py	Tue Oct 25 16:26:56 2005
@@ -17,107 +17,39 @@
 
 $Id$
 """
-from Products.Five.fiveconfigure import isFiveMethod
-from zope.event import notify
-from zope.interface import implements
-from zope.app.container.interfaces import IObjectAddedEvent,\
-     IObjectRemovedEvent
-from zope.app.container.contained import ObjectMovedEvent
-from zope.app.event.objectevent import ObjectCopiedEvent
-
-# holds classes that were monkeyed with; for clean up
-_monkied = []
-
-# ObjectAddedEvent and ObjectRemovedEvent are different in Zope 2
-class ObjectAddedEvent(ObjectMovedEvent):
-    implements(IObjectAddedEvent)
-
-    def __init__(self, object, newParent=None, newName=None):
-        if newParent is None:
-            newParent = object.aq_inner.aq_parent
-        if newName is None:
-            newName = object.id
-        ObjectMovedEvent.__init__(self, object, None, None, newParent, newName)
-    
-class ObjectRemovedEvent(ObjectMovedEvent):
-    implements(IObjectRemovedEvent)
-
-    def __init__(self, object, oldParent=None, oldName=None):
-        if oldParent is None:
-            oldParent = object.aq_inner.aq_parent
-        if oldName is None:
-            oldName = object.id
-        ObjectMovedEvent.__init__(self, object, oldParent, oldName, None, None)
-    
-def manage_afterAdd(self, item, container):
-    original_location_path = getattr(self, '__five_location_path__', None)
-    self.__five_location_path__ = self.getPhysicalPath()
-    # if there still is an object in the original location, we're copied
-    # we cannot rely on manage_afterClone, as this gets triggered only
-    # *after* a manage_afterAdd. This logic might fail in the case where
-    # something *is* somehow left in the original location that can
-    # be traversed to.
-    is_copied = original_location_path and (self.unrestrictedTraverse(
-        original_location_path, None) is not None)
-    if is_copied:
-        notify(ObjectCopiedEvent(self))
-    if original_location_path is None or is_copied:
-        notify(ObjectAddedEvent(self))
-    else:
-        original_location = self.unrestrictedTraverse(
-            original_location_path[:-1])
-        notify(ObjectMovedEvent(self,
-                                original_location, original_location_path[-1],
-                                container, self.id))
-    # call original
-    method = getattr(self, '__five_original_manage_afterAdd', None)
-    if method is not None:
-        self.__five_original_manage_afterAdd(item, container)
-
-manage_afterAdd.__five_method__ = True
-
-def manage_beforeDelete(self, item, container):
-    notify(ObjectRemovedEvent(self))
-    # call original
-    method = getattr(self, '__five_original_manage_beforeDelete', None)
-    if method is not None:
-        self.__five_original_manage_beforeDelete(item, container)
-
-manage_beforeDelete.__five_method__ = True
-
-def classSendEvents(class_):
-    """Make instances of the class send Object*Event."""
-    # tuck away original methods if necessary
-    for name in ['manage_afterAdd', 'manage_beforeDelete']:
-        method = getattr(class_, name, None)
-        if not isFiveMethod(method):
-            # if we haven't alread overridden this, tuck away originals
-            setattr(class_, '__five_original_' + name, method)
-
-    class_.manage_afterAdd = manage_afterAdd
-    class_.manage_beforeDelete = manage_beforeDelete
-    # remember class for clean up
-    _monkied.append(class_)
-    
-def sendEvents(_context, class_):
+
+from event import doMonkies
+from event import containerEventAwareClasses
+from event import deprecatedManageAddDeleteClasses
+
+def setContainerEvents(transitional):
+    doMonkies(transitional)
+
+def setContainerEventAware(class_):
+    """Instances of the class will receive object events."""
+    containerEventAwareClasses.append(class_)
+
+def setDeprecatedManageAddDelete(class_):
+    """Instances of the class will still see their old methods called."""
+    deprecatedManageAddDeleteClasses.append(class_)
+
+def containerEvents(_context, transitional):
     _context.action(
-        discriminator = ('five:sendEvents', class_),
-        callable = classSendEvents,
-        args=(class_,)
+        discriminator=('five:containerEvents',),
+        callable=setContainerEvents,
+        args=(transitional,),
         )
 
-# clean up code
-from Products.Five.fiveconfigure import killMonkey
-from zope.testing.cleanup import addCleanUp
-
-def unsendEvents(class_):
-    """Restore class's initial state with respect to sending events"""
-    for name in ['manage_afterAdd', 'manage_beforeDelete']:
-        killMonkey(class_, name, '__five_original_'+name)
-
-def cleanUp():
-    for class_ in _monkied:
-        unsendEvents(class_)
+def containerEventAware(_context, class_):
+    _context.action(
+        discriminator=('five:containerEventAware', class_),
+        callable=setContainerEventAware,
+        args=(class_,),
+        )
 
-addCleanUp(cleanUp)
-del addCleanUp
+def deprecatedManageAddDelete(_context, class_):
+    _context.action(
+        discriminator=('five:deprecatedManageAddDelete', class_),
+        callable=setDeprecatedManageAddDelete,
+        args=(class_,),
+        )

Modified: z3/Five/branch/efge-object-event/fivedirectives.py
==============================================================================
--- z3/Five/branch/efge-object-event/fivedirectives.py	(original)
+++ z3/Five/branch/efge-object-event/fivedirectives.py	Tue Oct 25 16:26:56 2005
@@ -18,6 +18,7 @@
 from zope.interface import Interface
 from zope.app.publisher.browser.metadirectives import IBasicResourceInformation
 from zope.configuration.fields import GlobalObject, Tokens, PythonIdentifier
+from zope.configuration.fields import Bool
 from zope.schema import TextLine
 
 class IImplementsDirective(Interface):
@@ -56,7 +57,7 @@
         required=True
         )
 
-class ISendEventsDirective(Interface):
+class ISizableDirective(Interface):
     """Make instances of class send events.
     """
 
@@ -65,6 +66,32 @@
         required=True
         )
 
+class IContainerEventsDirective(Interface):
+    """Global switch to enable container events
+    """
+    transitional = Bool(
+        title=u"Transitional",
+        required=False,
+        default=False,
+        )
+
+class IContainerEventAwareDirective(Interface):
+    """Send events for these contained content classes (transitional).
+    """
+    class_ = GlobalObject(
+        title=u"Class",
+        required=True,
+        )
+
+class IDeprecatedManageAddDeleteDirective(Interface):
+    """Call manage_afterAdd & co for these contained content classes.
+    """
+    class_ = GlobalObject(
+        title=u"Class",
+        required=True,
+        )
+
+
 class IBridgeDirective(Interface):
     """Bridge from a Zope 2 interface to an equivalent Zope3 interface.
     """

Modified: z3/Five/branch/efge-object-event/meta.zcml
==============================================================================
--- z3/Five/branch/efge-object-event/meta.zcml	(original)
+++ z3/Five/branch/efge-object-event/meta.zcml	Tue Oct 25 16:26:56 2005
@@ -128,14 +128,26 @@
        />
 
     <meta:directive
-       name="sendEvents"
-       schema=".fivedirectives.ISendEventsDirective"
-       handler=".eventconfigure.sendEvents"
+       name="containerEvents"
+       schema=".fivedirectives.IContainerEventsDirective"
+       handler=".eventconfigure.containerEvents"
+       />
+
+    <meta:directive
+       name="containerEventAware"
+       schema=".fivedirectives.IContainerEventAwareDirective"
+       handler=".eventconfigure.containerEventAware"
+       />
+
+    <meta:directive
+       name="deprecatedManageAddDelete"
+       schema=".fivedirectives.IDeprecatedManageAddDeleteDirective"
+       handler=".eventconfigure.deprecatedManageAddDelete"
        />
 
     <meta:directive
        name="sizable"
-       schema=".fivedirectives.ISendEventsDirective"
+       schema=".fivedirectives.ISizableDirective"
        handler=".sizeconfigure.sizable"
        />
 

Modified: z3/Five/branch/efge-object-event/tests/event.txt
==============================================================================
--- z3/Five/branch/efge-object-event/tests/event.txt	(original)
+++ z3/Five/branch/efge-object-event/tests/event.txt	Tue Oct 25 16:26:56 2005
@@ -1,295 +1,579 @@
-Test events
-===========
+================
+Container events
+================
 
-Before we can start, we need to set up an event subscriber that allows
-us to inspect events that will be thrown during the test:
+Zope 3 container events are used to inform subscribers that an object is
+about to be added/removed from a container, and also after it has been
+done. This is used for bookkeeping and cleaning up in subobjects.
 
-  >>> from zope.app.tests.placelesssetup import setUp, tearDown
-  >>> setUp()
-
-Add a folder that doesn't verify objects on paste.  We use it as a
-test sandbox:
-
-  >>> from Products.Five.tests.testing import manage_addNoVerifyPasteFolder
-  >>> manage_addNoVerifyPasteFolder(self.folder, 'npvf')
-  >>> folder = self.folder.npvf
-
-Finally add a manager user login, give it the right permissions and
-log in using it:
-
-  >>> uf = self.folder.acl_users
-  >>> uf._doAddUser('manager', 'r00t', ['Manager'], [])
-  >>> self.setPermissions(standard_permissions + ['Copy or Move'], 'Manager')
-  >>> self.login('manager')
-
-  >>> from zope.app.event.tests.placelesssetup import getEvents, clearEvents
-
-
-Sending events
---------------
-
-Zope 2 classes need to be modified so that they send Zope 3 style
-events.  Our stub class here is such a case.  We can add it to a
-folder, for example, ...
-
-  >>> from Products.Five.tests.testing.simplecontent import manage_addSimpleContent
-  >>> manage_addSimpleContent(folder, 'foo', 'Foo')
+These events replace the Zope 2 manage_afterAdd, manage_beforeDelete and
+manage_afterClone methods.
 
-and no event will have been triggered:
+Phases
+======
 
-  >>> len(getEvents())
-  0
+There are 4 steps to the migration process of having Zope 2 use Zope 3
+container events.
 
-Clean up:
+Phase 1
+-------
 
-  >>> folder.manage_delObjects(['foo'])
+Original Zope 2 status, where no events are sent, and manage_afterAdd &
+co are called on the children.
 
-Now make the class send events:
+Phase 2
+-------
 
-  >>> from Products.Five.eventconfigure import classSendEvents
-  >>> from Products.Five.tests.testing.simplecontent import SimpleContent
-  >>> classSendEvents(SimpleContent)
+<five:containerEvents transitional="true"/>
 
+Some content classes have been migrated to using Zope 3 events.
 
-Added event
-------------
+All standard Zope containers send Zope 3 events. Their manage_afterAdd &
+co methods do not do recursion anymore, and send a deprecation warning
+if called. The dispatching to subobjects is done through the standard
+Zope 3 dispatchToSublocations subscriber.
 
-Let's add an object to a folder:
+Containers will still call manage_afterAdd & co (with a deprecation
+warning) for content classes not specified in a::
 
-  >>> manage_addSimpleContent(folder, 'foo', 'Foo')
-
-One object event should have been sent with the event's object being
-our foo object:
-
-  >>> events = getEvents()
-  >>> len(events)
-  1
-  >>> foo = folder.foo
-  >>> events[0].object == foo
-  True
-
-That object event should have been an object added event:
-
-  >>> from zope.app.container.interfaces import IObjectAddedEvent
-  >>> events = getEvents(IObjectAddedEvent)
-  >>> len(events)
-  1
-  >>> events[0].object == foo
-  True
-  >>> events[0].newParent == foo.aq_parent
-  True
-
-Check that the object's original manage_afterAdd method was also called:
-
-  >>> foo.afterAdd_called
-  True
+  <five:containerEventAware class="some.content.class"/>
 
-Now clean up:
+directive.
 
-  >>> clearEvents()
-
-
-Moved event (I) -- Renaming
---------------------------
-
-Somehow we need to at least commit a subtransaction to make renaming
-succeed:
-
-  >>> import transaction
-  >>> transaction.commit(1)
-
-Let's rename the object we created before:
-
-  >>> folder.manage_renameObject('foo', 'bar')
-
-We should get two events...
-
-  >>> events = getEvents()
-  >>> len(events)
-  2
-
-the removed event...
-
-  >>> event = events[0]
-  >>> from zope.app.container.interfaces import IObjectRemovedEvent
-  >>> IObjectRemovedEvent.providedBy(event)
-  True
-  >>> event.object == foo
-  True
-  >>> event.oldName, event.newName
-  ('foo', None)
-  >>> event.oldParent == folder
-  True
-  >>> event.newParent is None
-  True
-
-and the moved event:
-
-  >>> event = events[1]
-  >>> event.object == foo
-  True
-  >>> event.oldName, event.newName
-  ('foo', 'bar')
-  >>> event.oldParent == folder
-  True
-  >>> event.newParent == folder
-  True
+Phase 3
+-------
 
-Now clean up:
+<five:containerEvents/>
 
-  >>> folder.manage_delObjects(['bar'])
-  >>> clearEvents()
+Most content classes have been migrated to using Zope 3 events.
 
-We don't delete the stub object just yet because it's being used in
-the next part of the test.
+All standard Zope containers will only call manage_afterAdd & co on
+classes specified in a::
 
+  <five:deprecatedManageAddDelete class="some.content.class"/>
 
-Moved event (II) -- Cut and paste
----------------------------------
+directive. A deprecation warning is sent for these.
 
-Let's move from one folder to another:
+<five:containerEventAware/> is deprecated, as it is now the default
+for other classes.
 
-  >>> manage_addNoVerifyPasteFolder(folder, 'folder1', 'Folder1')
-  >>> folder1 = folder.folder1
-  >>> manage_addNoVerifyPasteFolder(folder, 'folder2', 'Folder2')
-  >>> folder2 = folder.folder2
-  >>> manage_addSimpleContent(folder1, 'foo', 'Foo')
-  >>> foo = folder1.foo
+Phase 4
+-------
 
-We need to trigger a subtransaction before cut/paste can work:
+All content classes use Zope 3 events. <five:containerEvents/> is
+deprecated as it is now the default. <five:deprecatedManageAddDelete/>
+is forbidden.
 
-  >>> transaction.commit(1)
-  >>> cb = folder1.manage_cutObjects(['foo'])
-  >>> info = folder2.manage_pasteObjects(cb)
 
-Apart from the added event we triggerred when we added the stub object
-to the folder, we expect two events...
+Testing
+=======
 
-  >>> events = getEvents()
-  >>> len(events)
-  3
-  >>> len(getEvents(IObjectAddedEvent))
-  1
+Let's see what happens for these phases. There is a bit of setup for the
+tests.
 
-a removed event...
-
-  >>> event = events[1]
-  >>> from zope.app.container.interfaces import IObjectRemovedEvent
-  >>> IObjectRemovedEvent.providedBy(event)
-  True
-  >>> event.oldParent == folder1
-  True
-  >>> event.newParent is None
-  True
-
-and a moved event:
-
-  >>> event = events[2]
-  >>> event.object == foo
-  True
-  >>> event.oldName, event.newName
-  ('foo', 'foo')
-  >>> event.oldParent == folder1
-  True
-  >>> event.newParent == folder2
-  True
-
-Now clean up:
-
-  >>> folder.manage_delObjects(['folder1'])
-  >>> folder.manage_delObjects(['folder2'])
-  >>> clearEvents()
-
-
-Copied event
-------------
-
-  >>> manage_addSimpleContent(folder, 'foo', 'Foo')
-  >>> manage_addNoVerifyPasteFolder(folder, 'folder1')
-  >>> folder1 = folder.folder1
-
-We need to trigger subtransaction before copy/paste can work
-
-  >>> transaction.commit(1)
-  >>> cb = folder.manage_copyObjects(['foo'])
-  >>> info = folder1.manage_pasteObjects(cb)
-  >>> foo = folder1.foo
-
-Apart from the added event we triggerred when we added the stub object
-to the folder, we expect two events...
-
-  >>> events = getEvents()
-  >>> len(events)
-  3
-
-a copied event...
-
-  >>> event = events[1]
-  >>> from zope.app.event.interfaces import IObjectCopiedEvent
-  >>> IObjectCopiedEvent.providedBy(event)
-  True
-  >>> events[1].object == foo
-  True
+  >>> from zope.app.tests.placelesssetup import setUp, tearDown
+  >>> setUp()
 
-and an added event:
+Because we'll test copy/paste, we need to work inside a database.
 
-  >>> event = events[2]
-  >>> IObjectAddedEvent.providedBy(event)
-  True
-  >>> event.object == foo
-  True
-  >>> event.newName
-  'foo'
-  >>> event.newParent == folder1
+  >>> import ZODB.tests.util
+  >>> db = ZODB.tests.util.DB()
+  >>> connection = db.open()
+  >>> root = connection.root()
+
+We'll use two simple classes (defined in python code for picklability)
+and simple folders for our tests.
+
+  >>> from Products.Five.tests.test_event import MyApp, MyContent
+  >>> from Products.Five.tests.test_event import MyFolder, MyBTreeFolder
+
+  >>> app = MyApp('')
+  >>> root['app'] = app
+  >>> folder = MyFolder('folder')
+  >>> app._setObject('folder', folder)
+  old manage_afterAdd folder folder
+  'folder'
+  >>> folder = app.folder
+  >>> btfolder = MyBTreeFolder('btfolder')
+  >>> app._setObject('btfolder', btfolder)
+  old manage_afterAdd btfolder btfolder
+  'btfolder'
+
+To observe what object events are dispatched, we'll have some
+subscribers print them. We'll actually do that for a specific interface,
+not for (None, IObjectEvent), and register our subscribers before the
+framework's ones, so ours will be called first. This has the effect that
+printed events will be in their "natural" order.
+
+  >>> from zope.app.event.interfaces import IObjectEvent
+  >>> from zope.app.container.interfaces import IObjectMovedEvent
+  >>> from Products.Five.event import IObjectWillBeMovedEvent
+  >>> from OFS.interfaces import IObjectManager
+  >>> from OFS.interfaces import ISimpleItem
+  >>> from zope.app.tests import ztapi
+  >>> def printObjectEvent(object, event):
+  ...     print event.__class__.__name__, object.getId()
+  >>> def printObjectEventExceptMoved(object, event):
+  ...     if (IObjectMovedEvent.providedBy(event) or
+  ...         IObjectWillBeMovedEvent.providedBy(event)):
+  ...         return
+  ...     print event.__class__.__name__, object.getId()
+  >>> ztapi.handle([IObjectManager, IObjectMovedEvent], printObjectEvent)
+  >>> ztapi.handle([ISimpleItem, IObjectMovedEvent], printObjectEvent)
+  >>> ztapi.handle([IObjectManager, IObjectWillBeMovedEvent], printObjectEvent)
+  >>> ztapi.handle([ISimpleItem, IObjectWillBeMovedEvent], printObjectEvent)
+  >>> ztapi.handle([None, IObjectEvent], printObjectEventExceptMoved)
+
+Phase 1
+-------
+
+Let's test standard folder operations. The methods manage_afterAdd and
+manage_beforeDelete, and several others, are called.
+
+  >>> ob = MyContent('milou')
+  >>> folder._setObject('milou', ob)
+  old manage_afterAdd milou milou folder
+  'milou'
+  >>> folder.manage_delObjects('milou')
+  old manage_beforeDelete milou milou folder
+
+We can also move objects.
+
+  >>> ob = MyContent('tintin')
+  >>> folder._setObject('tintin', ob)
+  old manage_afterAdd tintin tintin folder
+  'tintin'
+  >>> cp = folder.manage_cutObjects('tintin')
+  >>> folder.manage_pasteObjects(cp)
+  old _notifyOfCopyTo tintin folder 1
+  old manage_beforeDelete tintin tintin folder
+  old manage_afterAdd tintin tintin folder
+  old _postCopy tintin folder 1
+  [{'new_id': 'tintin', 'id': 'tintin'}]
+  >>> 'tintin' in folder.objectIds()
+  True
+
+And we can copy them.
+
+  >>> cp = folder.manage_copyObjects('tintin')
+  >>> folder.manage_pasteObjects(cp)
+  old _notifyOfCopyTo tintin folder 0
+  old manage_afterAdd copy_of_tintin copy_of_tintin folder
+  old _postCopy copy_of_tintin folder 0
+  old manage_afterClone copy_of_tintin copy_of_tintin
+  [{'new_id': 'copy_of_tintin', 'id': 'tintin'}]
+  >>> 'tintin' in folder.objectIds()
+  True
+  >>> 'copy_of_tintin' in folder.objectIds()
+  True
+
+We can rename objects:
+
+  >>> folder.manage_renameObject('copy_of_tintin', 'haddock')
+  old _notifyOfCopyTo copy_of_tintin folder 1
+  old manage_beforeDelete copy_of_tintin copy_of_tintin folder
+  old manage_afterAdd haddock haddock folder
+  old _postCopy haddock folder 1
+  >>> 'copy_of_tintin' in folder.objectIds()
+  False
+  >>> 'haddock' in folder.objectIds()
   True
 
-Now clean up:
-
-  >>> folder.manage_delObjects(['folder1'])
-  >>> folder.manage_delObjects(['foo'])
-  >>> clearEvents()
-
-
-Removed event
--------------
+We can also call manage_clone by hand:
 
-  >>> manage_addSimpleContent(folder, 'foo', 'Foo')
-  >>> foo = folder.foo
-  >>> foo.beforeDelete_called
+  >>> res = folder.manage_clone(folder.tintin, 'tournesol')
+  old _notifyOfCopyTo tintin folder 0
+  old manage_afterAdd tournesol tournesol folder
+  old _postCopy tournesol folder 0
+  old manage_afterClone tournesol tournesol
+  >>> res.getId()
+  'tournesol'
+  >>> 'tournesol' in folder.objectIds()
+  True
+
+Let's also test with a BTreeFolder:
+
+  >>> ob = MyContent('castafiore')
+  >>> btfolder._setObject('castafiore', ob)
+  old manage_afterAdd castafiore castafiore btfolder
+  'castafiore'
+  >>> btfolder.manage_delObjects('castafiore')
+  old manage_beforeDelete castafiore castafiore btfolder
+
+When a tree of objects is affected, the methods are called for all
+levels.
+
+  >>> subfolder = MyFolder('subfolder')
+  >>> folder._setObject('subfolder', subfolder)
+  old manage_afterAdd subfolder subfolder folder
+  'subfolder'
+  >>> subfolder = folder.subfolder
+  >>> ob = MyContent('riri')
+  >>> subfolder._setObject('riri', ob)
+  old manage_afterAdd riri riri subfolder
+  'riri'
+
+Renaming a tree of objects:
+
+  >>> folder.manage_renameObject('subfolder', 'bob')
+  old _notifyOfCopyTo subfolder folder 1
+  old manage_beforeDelete riri subfolder folder
+  old manage_beforeDelete subfolder subfolder folder
+  old manage_afterAdd bob bob folder
+  old manage_afterAdd riri bob folder
+  old _postCopy bob folder 1
+  >>> 'bob' in folder.objectIds()
+  True
+
+Cloning a tree of objects:
+
+  >>> res = folder.manage_clone(folder.bob, 'loulou')
+  old _notifyOfCopyTo bob folder 0
+  old manage_afterAdd loulou loulou folder
+  old manage_afterAdd riri loulou folder
+  old _postCopy loulou folder 0
+  old manage_afterClone loulou loulou
+  old manage_afterClone riri loulou
+  >>> res.getId()
+  'loulou'
+  >>> 'bob' in folder.objectIds()
+  True
+  >>> 'loulou' in folder.objectIds()
+  True
+
+Phase 2
+-------
+
+In this phase, transitional events are in effect.
+
+We need some of the Five setup, to get proper five:interfaces declared
+for Zope 2 classes.
+
+  >>> from Products.Five import zcml
+  >>> import Products.Five
+  >>> zcml.load_config('meta.zcml', Products.Five)
+  >>> zcml.load_config('interfaces.zcml', Products.Five)
+
+Phase 2 is when we're using <five:containerEvents transitional="true"/>
+
+  >>> from Products.Five.event import doMonkies, undoMonkies
+  >>> doMonkies(transitional=True)
+
+When we add an instance of an old class for which we haven't specified
+anything, some events are sent but the old manage_afterAdd method is
+also called.
+
+  >>> ob = MyContent('lassie')
+  >>> folder._setObject('lassie', ob)
+  ObjectWillBeAddedEvent lassie
+  ObjectAddedEvent lassie
+  old manage_afterAdd lassie lassie folder
+  'lassie'
+
+And when we delete the object, manage_beforeDelete is also called and
+events are sent.
+
+  >>> folder.manage_delObjects('lassie')
+  ObjectWillBeRemovedEvent lassie
+  old manage_beforeDelete lassie lassie folder
+  ObjectRemovedEvent lassie
+
+The old behavior happens for a move or a copy, with events too.
+For a move:
+
+  >>> ob = MyContent('blueberry')
+  >>> folder._setObject('blueberry', ob)
+  ObjectWillBeAddedEvent blueberry
+  ObjectAddedEvent blueberry
+  old manage_afterAdd blueberry blueberry folder
+  'blueberry'
+  >>> cp = folder.manage_cutObjects('blueberry')
+  >>> folder.manage_pasteObjects(cp)
+  old _notifyOfCopyTo blueberry folder 1
+  ObjectWillBeMovedEvent blueberry
+  old manage_beforeDelete blueberry blueberry folder
+  ObjectMovedEvent blueberry
+  old manage_afterAdd blueberry blueberry folder
+  old _postCopy blueberry folder 1
+  [{'new_id': 'blueberry', 'id': 'blueberry'}]
+  >>> 'blueberry' in folder.objectIds()
+  True
+
+Old behavior with events for a copy:
+
+  >>> cp = folder.manage_copyObjects('blueberry')
+  >>> folder.manage_pasteObjects(cp)
+  old _notifyOfCopyTo blueberry folder 0
+  ObjectCopiedEvent copy_of_blueberry
+  ObjectWillBeAddedEvent copy_of_blueberry
+  ObjectAddedEvent copy_of_blueberry
+  old manage_afterAdd copy_of_blueberry copy_of_blueberry folder
+  old _postCopy copy_of_blueberry folder 0
+  old manage_afterClone copy_of_blueberry copy_of_blueberry
+  [{'new_id': 'copy_of_blueberry', 'id': 'blueberry'}]
+  >>> 'blueberry' in folder.objectIds()
+  True
+  >>> 'copy_of_blueberry' in folder.objectIds()
+  True
+
+Old behavior with events for a renaming:
+
+  >>> folder.manage_renameObject('copy_of_blueberry', 'myrtille')
+  old _notifyOfCopyTo copy_of_blueberry folder 1
+  ObjectWillBeMovedEvent copy_of_blueberry
+  old manage_beforeDelete copy_of_blueberry copy_of_blueberry folder
+  ObjectMovedEvent myrtille
+  old manage_afterAdd myrtille myrtille folder
+  old _postCopy myrtille folder 1
+  >>> 'copy_of_blueberry' in folder.objectIds()
   False
-  >>> folder.manage_delObjects(['foo'])
-
-  >>> events = getEvents()
-  >>> len(events)
-  2
-
-  >>> events[1].object.id
-  'foo'
-
-Check that the object's original manage_beforeDelete method was also called:
-
-  >>> foo.beforeDelete_called
+  >>> 'myrtille' in folder.objectIds()
   True
 
-  >>> clearEvents()
-
+Old behavior with events for a clone:
 
-Clean up
---------
+  >>> res = folder.manage_clone(folder.blueberry, 'strawberry')
+  old _notifyOfCopyTo blueberry folder 0
+  ObjectCopiedEvent strawberry
+  ObjectWillBeAddedEvent strawberry
+  ObjectAddedEvent strawberry
+  old manage_afterAdd strawberry strawberry folder
+  old _postCopy strawberry folder 0
+  old manage_afterClone strawberry strawberry
+  >>> res.getId()
+  'strawberry'
+  >>> 'strawberry' in folder.objectIds()
+  True
+
+Events are also sent when we work with a BTreeFolder:
+
+  >>> ob = MyContent('luckyluke')
+  >>> btfolder._setObject('luckyluke', ob)
+  ObjectWillBeAddedEvent luckyluke
+  ObjectAddedEvent luckyluke
+  old manage_afterAdd luckyluke luckyluke btfolder
+  'luckyluke'
+
+  >>> btfolder.manage_delObjects('luckyluke')
+  ObjectWillBeRemovedEvent luckyluke
+  old manage_beforeDelete luckyluke luckyluke btfolder
+  ObjectRemovedEvent luckyluke
+
+Here is what happens for a tree of objects. Let's create a simple one.
+
+  >>> subfolder = MyFolder('subfolder')
+  >>> folder._setObject('subfolder', subfolder)
+  ObjectWillBeAddedEvent subfolder
+  ObjectAddedEvent subfolder
+  old manage_afterAdd subfolder subfolder folder
+  'subfolder'
+  >>> subfolder = folder.subfolder
+  >>> ob = MyContent('donald')
+  >>> subfolder._setObject('donald', ob)
+  ObjectWillBeAddedEvent donald
+  ObjectAddedEvent donald
+  old manage_afterAdd donald donald subfolder
+  'donald'
+
+Renaming a tree of objects.
+
+  >>> folder.manage_renameObject('subfolder', 'pluto')
+  old _notifyOfCopyTo subfolder folder 1
+  ObjectWillBeMovedEvent subfolder
+  ObjectWillBeMovedEvent donald
+  old manage_beforeDelete donald subfolder folder
+  old manage_beforeDelete subfolder subfolder folder
+  ObjectMovedEvent pluto
+  old manage_afterAdd pluto pluto folder
+  ObjectMovedEvent donald
+  old manage_afterAdd donald pluto folder
+  old _postCopy pluto folder 1
+  >>> 'pluto' in folder.objectIds()
+  True
+
+Cloning a tree of objects:
+
+  >>> res = folder.manage_clone(folder.pluto, 'mickey')
+  old _notifyOfCopyTo pluto folder 0
+  ObjectCopiedEvent mickey
+  ObjectWillBeAddedEvent mickey
+  ObjectWillBeAddedEvent donald
+  ObjectAddedEvent mickey
+  old manage_afterAdd mickey mickey folder
+  ObjectAddedEvent donald
+  old manage_afterAdd donald mickey folder
+  old _postCopy mickey folder 0
+  old manage_afterClone mickey mickey
+  >>> res.getId()
+  'mickey'
+  >>> 'pluto' in folder.objectIds()
+  True
+  >>> 'mickey' in folder.objectIds()
+  True
+
+XXX there should be more calls to manage_afterClone above
+
+If however we specify using ZCML that our classes can react to events,
+the framework won't call manage_afterAdd and manage_beforeDelete
+anymore.
+
+  >>> from Products.Five.eventconfigure import setContainerEventAware
+  >>> setContainerEventAware(MyContent)
+  >>> setContainerEventAware(MyFolder)
+
+  >>> ob = MyContent('dogbert')
+  >>> folder._setObject('dogbert', ob)
+  ObjectWillBeAddedEvent dogbert
+  ObjectAddedEvent dogbert
+  'dogbert'
+  >>> folder.manage_delObjects('dogbert')
+  ObjectWillBeRemovedEvent dogbert
+  ObjectRemovedEvent dogbert
+
+Now move:
+
+  >>> ob = MyContent('dilbert')
+  >>> folder._setObject('dilbert', ob)
+  ObjectWillBeAddedEvent dilbert
+  ObjectAddedEvent dilbert
+  'dilbert'
+  >>> cp = folder.manage_cutObjects('dilbert')
+  >>> folder.manage_pasteObjects(cp)
+  ObjectWillBeMovedEvent dilbert
+  ObjectMovedEvent dilbert
+  [{'new_id': 'dilbert', 'id': 'dilbert'}]
+  >>> 'dilbert' in folder.objectIds()
+  True
+
+And copy:
+
+  >>> cp = folder.manage_copyObjects('dilbert')
+  >>> folder.manage_pasteObjects(cp)
+  ObjectCopiedEvent copy_of_dilbert
+  ObjectWillBeAddedEvent copy_of_dilbert
+  ObjectAddedEvent copy_of_dilbert
+  [{'new_id': 'copy_of_dilbert', 'id': 'dilbert'}]
+  >>> 'dilbert' in folder.objectIds()
+  True
+  >>> 'copy_of_dilbert' in folder.objectIds()
+  True
+
+Then rename:
+
+  >>> folder.manage_renameObject('copy_of_dilbert', 'wally')
+  ObjectWillBeMovedEvent copy_of_dilbert
+  ObjectMovedEvent wally
+  >>> 'wally' in folder.objectIds()
+  True
+
+Or copy using manage_clone:
+
+  >>> res = folder.manage_clone(folder.dilbert, 'phb')
+  ObjectCopiedEvent phb
+  ObjectWillBeAddedEvent phb
+  ObjectAddedEvent phb
+  >>> res.getId()
+  'phb'
+  >>> 'dilbert' in folder.objectIds()
+  True
+  >>> 'phb' in folder.objectIds()
+  True
+
+Also on a BTreeFolder:
+
+  >>> ob = MyContent('alice')
+  >>> btfolder._setObject('alice', ob)
+  ObjectWillBeAddedEvent alice
+  ObjectAddedEvent alice
+  'alice'
+  >>> btfolder.manage_renameObject('alice', 'rabbit')
+  ObjectWillBeMovedEvent alice
+  ObjectMovedEvent rabbit
+  >>> btfolder.manage_delObjects('rabbit')
+  ObjectWillBeRemovedEvent rabbit
+  ObjectRemovedEvent rabbit
+
+Now for a tree of objects. Let's create a simple one.
+
+  >>> subfolder = MyFolder('subfolder')
+  >>> folder._setObject('subfolder', subfolder)
+  ObjectWillBeAddedEvent subfolder
+  ObjectAddedEvent subfolder
+  'subfolder'
+  >>> subfolder = folder.subfolder
+  >>> ob = MyContent('mel')
+  >>> subfolder._setObject('mel', ob)
+  ObjectWillBeAddedEvent mel
+  ObjectAddedEvent mel
+  'mel'
+
+Renaming a tree of objects.
+
+  >>> folder.manage_renameObject('subfolder', 'firefly')
+  ObjectWillBeMovedEvent subfolder
+  ObjectWillBeMovedEvent mel
+  ObjectMovedEvent firefly
+  ObjectMovedEvent mel
+  >>> 'firefly' in folder.objectIds()
+  True
+
+Cloning a tree of objects:
+
+  >>> res = folder.manage_clone(folder.firefly, 'serenity')
+  ObjectCopiedEvent serenity
+  ObjectWillBeAddedEvent serenity
+  ObjectWillBeAddedEvent mel
+  ObjectAddedEvent serenity
+  ObjectAddedEvent mel
+  >>> res.getId()
+  'serenity'
+  >>> 'firefly' in folder.objectIds()
+  True
+  >>> 'serenity' in folder.objectIds()
+  True
+
+Now cleanup all the monkey patches:
+
+  >>> undoMonkies()
+
+Phase 3
+-------
+
+This is the target phase.
+
+  >>> doMonkies(transitional=False)
+
+By default, events are sent and manage_afterAdd is not called.
+
+  >>> ob = MyContent('droopy')
+  >>> folder._setObject('droopy', ob)
+  ObjectWillBeAddedEvent droopy
+  ObjectAddedEvent droopy
+  'droopy'
+  >>> folder.manage_delObjects('droopy')
+  ObjectWillBeRemovedEvent droopy
+  ObjectRemovedEvent droopy
+
+Old methods are called only for classes where we have specified that
+they are old.
+
+  >>> from Products.Five.eventconfigure import setDeprecatedManageAddDelete
+  >>> setDeprecatedManageAddDelete(MyContent)
+
+  >>> ob = MyContent('rantanplan')
+  >>> folder._setObject('rantanplan', ob)
+  ObjectWillBeAddedEvent rantanplan
+  ObjectAddedEvent rantanplan
+  old manage_afterAdd rantanplan rantanplan folder
+  'rantanplan'
+  >>> folder.manage_delObjects('rantanplan')
+  ObjectWillBeRemovedEvent rantanplan
+  old manage_beforeDelete rantanplan rantanplan folder
+  ObjectRemovedEvent rantanplan
 
-Finally, we need to put our stub class back the way it was before we
-monkeyed with it:
-
-  >>> from Products.Five.eventconfigure import cleanUp
-  >>> cleanUp()
-
-Now adding an object won't trigger an event anymore:
-
-  >>> from Products.Five.tests.testing.simplecontent import manage_addSimpleContent
-  >>> manage_addSimpleContent(folder, 'foo', 'Foo')
-  >>> len(getEvents())
-  0
-
-Finally, we need to tear down everything else (services, etc.)
+Now cleanup:
 
+  >>> import transaction
+  >>> transaction.abort()
+  >>> undoMonkies()
   >>> tearDown()

Modified: z3/Five/branch/efge-object-event/tests/test_event.py
==============================================================================
--- z3/Five/branch/efge-object-event/tests/test_event.py	(original)
+++ z3/Five/branch/efge-object-event/tests/test_event.py	Tue Oct 25 16:26:56 2005
@@ -19,6 +19,53 @@
 if __name__ == '__main__':
     execfile(os.path.join(sys.path[0], 'framework.py'))
 
+
+# These classes aren't defined in the doctest because otherwise
+# they wouldn't be picklable, and we need that to test copy/paste.
+
+from OFS.Folder import Folder
+from OFS.SimpleItem import SimpleItem
+from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2
+
+class NotifyBase(object):
+    def _verifyObjectPaste(self, object, validate_src=1):
+        pass
+    def cb_isMoveable(self):
+        return True
+    def cb_isCopyable(self):
+        return True
+    def _notifyOfCopyTo(self, container, op):
+        print 'old _notifyOfCopyTo', self.getId(), container.getId(), op
+    def _postCopy(self, container, op):
+        print 'old _postCopy', self.getId(), container.getId(), op
+    def manage_afterAdd(self, item, container):
+        print 'old manage_afterAdd %s %s %s' % (self.getId(), item.getId(),
+                                                container.getId())
+        super(NotifyBase, self).manage_afterAdd(item, container)
+    def manage_beforeDelete(self, item, container):
+        super(NotifyBase, self).manage_beforeDelete(item, container)
+        print 'old manage_beforeDelete %s %s %s' % (self.getId(), item.getId(),
+                                                    container.getId())
+    def manage_afterClone(self, item):
+        print 'old manage_afterClone %s %s' % (self.getId(), item.getId())
+        super(NotifyBase, self).manage_afterClone(item)
+
+class MyApp(Folder):
+    def getPhysicalRoot(self):
+        return self
+
+class MyFolder(NotifyBase, Folder):
+    pass
+
+class MyBTreeFolder(NotifyBase, BTreeFolder2):
+    def _verifyObjectPaste(self, object, validate_src=1):
+        pass
+
+class MyContent(NotifyBase, SimpleItem):
+    def __init__(self, id):
+        self._setId(id)
+
+
 def test_suite():
     from Testing.ZopeTestCase import ZopeDocFileSuite
     return ZopeDocFileSuite('event.txt', package="Products.Five.tests")


More information about the z3-checkins mailing list