[Checkins] SVN: z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/ * Generalise the IObjectAddedEvent handler to a IObjectMovedEvent handler which

Michael Kerrin michael.kerrin at openapp.ie
Sun Jun 24 14:15:44 EDT 2007


Log message for revision 77015:
  * Generalise the IObjectAddedEvent handler to a IObjectMovedEvent handler which
    enforces the WebDAV lock model on objects that get moved about.
  * Add a IObjectModifiedEvent handle to enforce the WebDAV lock model on
    objects that get modified.
  

Changed:
  U   z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/configure.zcml
  U   z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/manager.py
  U   z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/tests.py

-=-
Modified: z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/configure.zcml
===================================================================
--- z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/configure.zcml	2007-06-24 18:09:37 UTC (rev 77014)
+++ z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/configure.zcml	2007-06-24 18:15:43 UTC (rev 77015)
@@ -46,9 +46,13 @@
      />
 
   <subscriber
-     for="zope.interface.Interface
-          zope.app.container.interfaces.IObjectAddedEvent"
-     handler=".manager.indirectlyLockObjectOnAdd"
+     for="zope.app.container.interfaces.IObjectMovedEvent"
+     handler=".manager.indirectlyLockObjectOnMovedEvent"
      />
 
+  <subscriber
+     for="zope.lifecycleevent.ObjectModifiedEvent"
+     handler=".manager.checkLockedOnModify"
+     />
+
 </configure>

Modified: z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/manager.py
===================================================================
--- z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/manager.py	2007-06-24 18:09:37 UTC (rev 77014)
+++ z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/manager.py	2007-06-24 18:15:43 UTC (rev 77015)
@@ -21,6 +21,7 @@
 import zope.interface
 import zope.security.management
 import zope.publisher.interfaces.http
+import zope.lifecycleevent.interfaces
 from zope.app.container.interfaces import IReadContainer
 import z3c.dav.interfaces
 import z3c.dav.locking
@@ -35,24 +36,15 @@
 class DAVLockmanager(object):
     """
 
-      >>> import UserDict
       >>> from zope.interface.verify import verifyObject
       >>> from zope.locking import utility, utils
       >>> from zope.locking.adapters import TokenBroker
       >>> from zope.traversing.browser.absoluteurl import absoluteURL
-      >>> from zope.app.container.contained import ObjectAddedEvent
 
-      >>> class ReqAnnotation(UserDict.IterableUserDict):
-      ...    zope.interface.implements(zope.annotation.interfaces.IAnnotations)
-      ...    def __init__(self, request):
-      ...        self.data = request._environ.setdefault('annotation', {})
-      >>> zope.component.getGlobalSiteManager().registerAdapter(
-      ...    ReqAnnotation, (zope.publisher.interfaces.http.IHTTPRequest,))
-
       >>> file = Demo()
 
-    Before we register a ITokenUtility utility make sure that the DAVLockmanager
-    is not lockable.
+    Before we register a ITokenUtility utility make sure that the
+    DAVLockmanager is not lockable.
 
       >>> adapter = DAVLockmanager(file)
       >>> adapter.islockable()
@@ -74,7 +66,7 @@
       >>> oldNow = utils.now
       >>> utils.now = hackNow
 
-    Test the DAVLockmanager implements the descired interface.
+    Test the DAVLockmanager implements the descried interface.
 
       >>> adapter = DAVLockmanager(file)
       >>> verifyObject(z3c.dav.interfaces.IDAVLockmanager, adapter)
@@ -240,7 +232,7 @@
       >>> exclusivelock.end()
 
     If a shared lock is taken out on the resource, then this lock token is
-    probable not annotated with the extra information required by webdav.
+    probable not annotated with the extra information required by WebDAV.
 
       >>> sharedlock = util.register(
       ...    zope.locking.tokens.SharedLock(file, ('michael',)))
@@ -258,8 +250,8 @@
       frozenset(['michael'])
       >>> sharedlock.end()
 
-    Recursive lock suport
-    ---------------------
+    Recursive lock support
+    ----------------------
 
       >>> demofolder = DemoFolder()
       >>> demofolder['demo'] = file
@@ -282,21 +274,6 @@
       >>> activelock.lockscope
       [u'exclusive']
 
-    If a collection is locked with an infinite depth lock then all member
-    resources are indirectly locked. Any resource that is added to this
-    collection then becomes indirectly locked against the lockroot for
-    the collection.
-
-      >>> file2 = Demo()
-      >>> demofolder['file2'] = file2
-      >>> indirectlyLockObjectOnAdd(file2,
-      ...    ObjectAddedEvent(file2, demofolder, 'file2'))
-      >>> file2lock = util.get(file2)
-      >>> interfaces.IIndirectToken.providedBy(file2lock)
-      True
-      >>> file2lock.roottoken == util.get(demofolder)
-      True
-
     Already locked
     --------------
 
@@ -332,9 +309,6 @@
     Cleanup
     -------
 
-      >>> zope.component.getGlobalSiteManager().unregisterAdapter(
-      ...    ReqAnnotation, (zope.publisher.interfaces.http.IHTTPRequest,))
-      True
       >>> zope.component.getGlobalSiteManager().unregisterUtility(
       ...    util, zope.locking.interfaces.ITokenUtility)
       True
@@ -436,6 +410,7 @@
         return locktoken
 
     def getActivelock(self, locktoken, request = None):
+        # Note that this is only used for testing purposes.
         if self.islocked():
             token = zope.locking.interfaces.ITokenBroker(self.context).get()
             return properties.DAVActiveLock(
@@ -488,27 +463,369 @@
 
     return principal_id
 
+###############################################################################
+#
+# These event handlers enforce the WebDAV lock model. Namely on modification
+# we need to check the `IF` header to see if the client knows about any
+# locks on the object, and if they don't then a AlreadyLocked exception
+# should be raised.
+#
+# We only do these checks for non-browser methods (i.e. not GET, HEAD or POST)
+# as web browsers never send the `IF` header, and will always fail this test.
+# For the browser methods we should use a specialized security policy like
+# zc.tokenpolicy instead.
+#
+# Note that the events that tigger these handlers are always emitted after
+# the fact, so any security assertions that might raise an exception will
+# have done so before these tests, so if any condition outside the WebDAV
+# lock model occurs then an AlreadyLocked exception must be raised which will
+# abort the transaction and set the appropriate status and body to send to
+# the client
+#
+###############################################################################
 
- at zope.component.adapter(
-    zope.interface.Interface, zope.app.container.interfaces.IObjectAddedEvent)
-def indirectlyLockObjectOnAdd(object, event):
+BROWSER_METHODS = ("GET", "HEAD", "POST")
+
+ at zope.component.adapter(zope.app.container.interfaces.IObjectMovedEvent)
+def indirectlyLockObjectOnMovedEvent(event):
+    """
+    This event handler listens for IObjectAddedEvent, IObjectRemovedEvent as
+    while as IObjectMovedEvents and is responsible for testing whether the
+    container modification in question is allowed.
+
+      >>> import UserDict
+      >>> import datetime
+      >>> from zope.locking import utility
+      >>> from zope.publisher.browser import TestRequest
+      >>> from zope.security.proxy import removeSecurityProxy
+      >>> from zope.app.container.contained import ObjectAddedEvent
+      >>> from zope.app.container.contained import ObjectRemovedEvent
+      >>> from zope.app.container.contained import ObjectMovedEvent
+
+      >>> class ReqAnnotation(UserDict.IterableUserDict):
+      ...    zope.interface.implements(zope.annotation.interfaces.IAnnotations)
+      ...    def __init__(self, request):
+      ...        self.data = request._environ.setdefault('annotation', {})
+      >>> zope.component.getGlobalSiteManager().registerAdapter(
+      ...    ReqAnnotation, (zope.publisher.interfaces.http.IHTTPRequest,))
+
+      >>> class Statetokens(object):
+      ...    zope.interface.implements(z3c.dav.ifvalidator.IStateTokens)
+      ...    def __init__(self, context, request, view):
+      ...        self.context = context
+      ...    schemes = ('', 'opaquetoken')
+      ...    @property
+      ...    def tokens(self):
+      ...        context = removeSecurityProxy(self.context) # ???
+      ...        if getattr(context, '_tokens', None) is not None:
+      ...            return context._tokens
+      ...        return []
+      >>> zope.component.getGlobalSiteManager().registerAdapter(
+      ...    Statetokens, (None, TestRequest, None))
+
+    Now some content.
+
+      >>> util = utility.TokenUtility()
+      >>> zope.component.getGlobalSiteManager().registerUtility(
+      ...    util, zope.locking.interfaces.ITokenUtility)
+
+      >>> demofolder = DemoFolder()
+
+    Nothing is locked at this stage so the test passes.
+
+      >>> file1 = Demo()
+      >>> demofolder['file1'] = file1
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectAddedEvent(file1, demofolder, 'file1'))
+      >>> util.get(file1) is None
+      True
+
+    If a collection is locked with an infinite depth lock then all member
+    resources are indirectly locked. Any resource that is added to this
+    collection then becomes indirectly locked against the lockroot for
+    the collection.
+
+      >>> adapter = DAVLockmanager(demofolder)
+      >>> locktoken = adapter.lock(u'exclusive', u'write',
+      ...    u'MichaelK', datetime.timedelta(seconds = 3600), 'infinity')
+
+      >>> file2 = Demo()
+      >>> demofolder['file2'] = file2
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectAddedEvent(file2, demofolder, 'file2'))
+      >>> file2lock = util.get(file2)
+      >>> interfaces.IIndirectToken.providedBy(file2lock)
+      True
+      >>> file2lock.roottoken == util.get(demofolder)
+      True
+
+    If we rerun the event handler the new object is already locked, we
+    get an AlreadyLocked exception as I don't know how to merge lock tokens,
+    which might be possible under circumstances.
+
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectAddedEvent(file2, demofolder, 'file2')) #doctest:+ELLIPSIS
+      Traceback (most recent call last):
+      ...
+      AlreadyLocked: <z3c.davapp.zopelocking.tests.Demo object at ...>: None
+
+    An `IF` header must be present in the request object in-order for us
+    to be allowed to remove this.
+
+      >>> file2._tokens = ['statetoken']
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectRemovedEvent(file2, demofolder, 'file2')) #doctest:+ELLIPSIS
+      Traceback (most recent call last):
+      ...
+      AlreadyLocked: <z3c.davapp.zopelocking.tests.Demo object at ...>: None
+
+    Now if we set the request annotation right the event handler doesn't
+    raise a AlreadyLocked request.
+
+      >>> request = zope.security.management.getInteraction().participations[0]
+      >>> ReqAnnotation(request)[z3c.dav.ifvalidator.STATE_ANNOTS] = {
+      ...    '/file2': {'statetoken': True}}
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectRemovedEvent(file2, demofolder, 'file2'))
+
+    `IF` access was granted to the source folder, and the destination folder
+    is not locked is this is allowed.
+
+      >>> demofolder2 = DemoFolder()
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectMovedEvent(file2, demofolder, 'file2',
+      ...                     demofolder2, 'file3'))
+
+    Now the state token for the demofolder2 object matches a state token
+    in the `IF` header but it isn't specific for this resource or any of its
+    parents.
+
+      >>> demofolder['subfolder'] = demofolder2
+      >>> adapter2 = DAVLockmanager(demofolder2)
+      >>> locktoken2 = adapter2.lock(u'exclusive', u'write', u'Michael 2',
+      ...    datetime.timedelta(seconds = 3600), 'infinity')
+      >>> demofolder2._tokens = ['statetoken']
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectMovedEvent(file2, demofolder, 'file2',
+      ...                     demofolder2, 'file3')) #doctest:+ELLIPSIS
+      Traceback (most recent call last):
+      ...
+      AlreadyLocked: <z3c.davapp.zopelocking.tests.Demo object at ...>: None
+
+    But if we update the request annotation, we still fail because the
+    object we are moving is still locked.
+
+      >>> ReqAnnotation(request)[
+      ...    z3c.dav.ifvalidator.STATE_ANNOTS]['/subfolder'] = {
+      ...        'statetoken': True}
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectMovedEvent(file2, demofolder, 'file2',
+      ...                     demofolder2, 'file3')) #doctest:+ELLIPSIS
+      Traceback (most recent call last):
+      ...
+      AlreadyLocked: <z3c.davapp.zopelocking.tests.Demo object at ...>: None
+
+    Move to same folder, the destination folder is locked, but the object is
+    not.
+
+      >>> file3 = Demo()
+      >>> demofolder['file3'] = file3
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectMovedEvent(file3, demofolder, 'file3',
+      ...                     demofolder, 'file4'))
+
+    Indirect tokens.
+
+      >>> roottoken = util.get(demofolder)
+      >>> zope.locking.interfaces.IExclusiveLock.providedBy(roottoken)
+      True
+      >>> subfolder = DemoFolder()
+      >>> demofolder['subfolder'] = subfolder
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectAddedEvent(subfolder, demofolder, 'subfolder'))
+      >>> subtoken = util.get(subfolder)
+      >>> interfaces.IIndirectToken.providedBy(subtoken)
+      True
+      >>> subtoken.roottoken == roottoken
+      True
+
+      >>> subsubfolder = DemoFolder()
+      >>> subfolder['subfolder'] = subsubfolder
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectAddedEvent(subsubfolder, subfolder, 'subsubfolder'))
+      >>> subsubtoken = util.get(subsubfolder)
+      >>> interfaces.IIndirectToken.providedBy(subsubtoken)
+      True
+      >>> subsubtoken.roottoken == roottoken
+      True
+
+    But this eventhandler never raises exceptions for any of the browser
+    methods, GET, HEAD, POST.
+
+      >>> del ReqAnnotation(request)[z3c.dav.ifvalidator.STATE_ANNOTS]
+      >>> request.method = 'POST'
+      >>> indirectlyLockObjectOnMovedEvent(
+      ...    ObjectRemovedEvent(file2, demofolder, 'file2'))
+
+    Cleanup
+    -------
+
+      >>> zope.component.getGlobalSiteManager().unregisterUtility(
+      ...    util, zope.locking.interfaces.ITokenUtility)
+      True
+      >>> zope.component.getGlobalSiteManager().unregisterAdapter(
+      ...    ReqAnnotation, (zope.publisher.interfaces.http.IHTTPRequest,))
+      True
+      >>> zope.component.getGlobalSiteManager().unregisterAdapter(
+      ...    Statetokens, (None, TestRequest, None))
+      True
+
+    """
+    utility = zope.component.queryUtility(
+        zope.locking.interfaces.ITokenUtility, context = event.object)
+    if not utility:
+        # If there is no utility then is nothing that we can check against.
+        return
+
+    # This is an hack to get at the current request object
     interaction = zope.security.management.queryInteraction()
+    if interaction:
+        request = interaction.participations[0]
+        if zope.publisher.interfaces.http.IHTTPRequest.providedBy(request) \
+               and request.method not in BROWSER_METHODS:
+            objectToken = utility.get(event.object)
+            if objectToken:
+                # The object is been moved out of its parent - hance we need
+                # to validate that we are allowed to perform this
+                # modification.
+                if event.oldParent is not None and \
+                       not z3c.dav.ifvalidator.matchesIfHeader(
+                           event.object, request):
+                    raise z3c.dav.interfaces.AlreadyLocked(
+                        event.object, "Locked object cannot be moved ")
+                # Otherwise since the oldParent hasn't changed we don't
+                # need to check if we are allowed to perform this action,
+                # this is probable a copy.
+            if event.newParent is not None:
+                # Probable an object added event, the object lock must be
+                # consistent we the lock on its parent.
+                parentToken = utility.get(event.newParent)
+                if parentToken is not None:
+                    if not z3c.dav.ifvalidator.matchesIfHeader(
+                        event.newParent, request):
+                        raise z3c.dav.interfaces.AlreadyLocked(
+                            event.object, "Destination folder is locked") 
+                    if objectToken is not None:
+                        # XXX - this needs to be smarter. We the lock on
+                        # the parent as depth '0' or the objectToken is
+                        # indirectly locked against the parentToken then
+                        # we shouldn't raise this exception.
+                        raise z3c.dav.interfaces.AlreadyLocked(
+                            event.object, "Locked object cannot be moved.")
+                    if interfaces.IIndirectToken.providedBy(parentToken):
+                        parentToken = parentToken.roottoken
+                    utility.register(
+                        indirecttokens.IndirectToken(event.object, parentToken))
 
+
+ at zope.component.adapter(zope.lifecycleevent.interfaces.IObjectModifiedEvent)
+def checkLockedOnModify(event):
+    """
+    When a content object is modified we need to check that the client
+    submitted an `IF` header that corresponds with the lock.
+
+      >>> import UserDict
+      >>> import datetime
+      >>> from zope.locking import utility
+      >>> import zope.publisher.interfaces.http
+      >>> from zope.publisher.browser import TestRequest
+      >>> from zope.security.proxy import removeSecurityProxy
+      >>> from zope.lifecycleevent import ObjectModifiedEvent
+
+    Some adapters needed to represent the data stored in the `IF` header,
+    and the current state tokens for the content.
+
+      >>> class ReqAnnotation(UserDict.IterableUserDict):
+      ...    zope.interface.implements(zope.annotation.interfaces.IAnnotations)
+      ...    def __init__(self, request):
+      ...        self.data = request._environ.setdefault('annotation', {})
+      >>> zope.component.getGlobalSiteManager().registerAdapter(
+      ...    ReqAnnotation, (zope.publisher.interfaces.http.IHTTPRequest,))
+
+      >>> class Statetokens(object):
+      ...    zope.interface.implements(z3c.dav.ifvalidator.IStateTokens)
+      ...    def __init__(self, context, request, view):
+      ...        self.context = context
+      ...    schemes = ('', 'opaquetoken')
+      ...    @property
+      ...    def tokens(self):
+      ...        context = removeSecurityProxy(self.context) # ???
+      ...        if getattr(context, '_tokens', None) is not None:
+      ...            return context._tokens
+      ...        return []
+      >>> zope.component.getGlobalSiteManager().registerAdapter(
+      ...    Statetokens, (None, TestRequest, None))
+
+      >>> util = utility.TokenUtility()
+      >>> zope.component.getGlobalSiteManager().registerUtility(
+      ...    util, zope.locking.interfaces.ITokenUtility)
+
+      >>> demofolder = DemoFolder()
+      >>> demofile = Demo()
+      >>> demofolder['demofile'] = demofile
+
+    The test passes when the object is not locked.
+
+      >>> checkLockedOnModify(ObjectModifiedEvent(demofile))
+
+    Lock the file and setup the request annotation.
+
+      >>> adapter = DAVLockmanager(demofile)
+      >>> locktoken = adapter.lock(u'exclusive', u'write',
+      ...    u'Michael', datetime.timedelta(seconds = 3600), '0')
+
+      >>> request = zope.security.management.getInteraction().participations[0]
+      >>> ReqAnnotation(request)[z3c.dav.ifvalidator.STATE_ANNOTS] = {
+      ...    '/demofile': {'statetoken': True}}
+
+      >>> demofile._tokens = ['wrongstatetoken'] # wrong token.
+      >>> checkLockedOnModify(ObjectModifiedEvent(demofile)) #doctest:+ELLIPSIS
+      Traceback (most recent call last):
+      ...
+      AlreadyLocked: <z3c.davapp.zopelocking.tests.Demo object at ...>: None
+
+    With the correct lock token submitted the test passes.
+
+      >>> demofile._tokens = ['statetoken'] # wrong token.
+      >>> checkLockedOnModify(ObjectModifiedEvent(demofile))
+
+    Child of locked token.
+
+      >>> ReqAnnotation(request)[z3c.dav.ifvalidator.STATE_ANNOTS] = {
+      ...    '/': {'statetoken': True}}
+      >>> demofile._tokens = ['statetoken']
+      >>> checkLockedOnModify(ObjectModifiedEvent(demofile))
+
+    Cleanup
+    -------
+
+      >>> zope.component.getGlobalSiteManager().unregisterAdapter(
+      ...    ReqAnnotation, (zope.publisher.interfaces.http.IHTTPRequest,))
+      True
+      >>> zope.component.getGlobalSiteManager().unregisterAdapter(
+      ...    Statetokens, (None, TestRequest, None))
+      True
+      >>> zope.component.getGlobalSiteManager().unregisterUtility(
+      ...    util, zope.locking.interfaces.ITokenUtility)
+      True
+
+    """
+    # This is an hack to get at the current request object
+    interaction = zope.security.management.queryInteraction()
     if interaction:
         request = interaction.participations[0]
         if zope.publisher.interfaces.http.IHTTPRequest.providedBy(request) \
-               and z3c.dav.ifvalidator.matchesIfHeader(object, request):
-            parent = object.__parent__
-            utility = zope.component.queryUtility(
-                zope.locking.interfaces.ITokenUtility, context = parent)
-            # XXX - potentially two problems here. One the object is already
-            # locked then the if we have created a conflicting lock. Two the
-            # lock on the collection might not be an infinite lock and as such
-            # we should not lock the object.
-            if utility and utility.get(object) is None:
-                token = utility.get(parent)
-                if token is not None:
-                    if interfaces.IIndirectToken.providedBy(token):
-                        token = token.roottoken
-                    utility.register(
-                        indirecttokens.IndirectToken(object, token))
+               and request.method not in BROWSER_METHODS:
+            if not z3c.dav.ifvalidator.matchesIfHeader(event.object, request):
+                raise z3c.dav.interfaces.AlreadyLocked(
+                    event.object, "Modifing locked object is not permitted.")

Modified: z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/tests.py
===================================================================
--- z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/tests.py	2007-06-24 18:09:37 UTC (rev 77014)
+++ z3c.davapp.zopelocking/trunk/src/z3c/davapp/zopelocking/tests.py	2007-06-24 18:15:43 UTC (rev 77015)
@@ -24,6 +24,7 @@
 from zope.security.management import newInteraction, endInteraction, \
      queryInteraction
 import zope.event
+from zope.traversing.interfaces import IPhysicallyLocatable
 from zope.app.testing import placelesssetup
 from zope.component.interfaces import IComponentLookup
 from zope.app.component.site import SiteManagerAdapter
@@ -65,6 +66,19 @@
         self.data[key] = value
 
 
+class PhysicallyLocatable(object):
+    zope.interface.implements(IPhysicallyLocatable)
+
+    def __init__(self, context):
+        self.context = context
+
+    def getRoot(self):
+        return root
+
+    def getPath(self):
+        return '/' + self.context.__name__
+
+
 class DemoKeyReference(object):
     _class_counter = 0
     zope.interface.implements(zope.app.keyreference.interfaces.IKeyReference)
@@ -116,8 +130,8 @@
     z3c.etree.testing.etreeSetup(test)
 
     # create principal
-    participation = TestRequest()
-    participation.setPrincipal(Principal('michael'))
+    participation = TestRequest(environ = {"REQUEST_METHOD": "PUT"})
+    participation.setPrincipal(Principal("michael"))
     if queryInteraction() is not None:
         queryInteraction().add(participation)
     else:
@@ -131,6 +145,8 @@
     gsm.registerAdapter(DemoKeyReference,
                         (IDemo,),
                         zope.app.keyreference.interfaces.IKeyReference)
+    gsm.registerAdapter(PhysicallyLocatable, (Demo,))
+    gsm.registerAdapter(PhysicallyLocatable, (DemoFolder,))
     gsm.registerAdapter(DemoKeyReference, (IDemoFolder,),
                         zope.app.keyreference.interfaces.IKeyReference)
     gsm.registerAdapter(SiteManagerAdapter,
@@ -181,6 +197,8 @@
     gsm.unregisterAdapter(DemoKeyReference,
                           (IDemo,),
                           zope.app.keyreference.interfaces.IKeyReference)
+    gsm.unregisterAdapter(PhysicallyLocatable, (Demo,))
+    gsm.unregisterAdapter(PhysicallyLocatable, (DemoFolder,))
     gsm.unregisterAdapter(DemoKeyReference, (IDemoFolder,),
                           zope.app.keyreference.interfaces.IKeyReference)
     gsm.unregisterAdapter(SiteManagerAdapter,



More information about the Checkins mailing list