[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