diff --git a/relation/README.txt b/relation/README.txt index d60c7bd..0a9d48f 100644 --- a/relation/README.txt +++ b/relation/README.txt @@ -117,8 +117,17 @@ working with events). >>> from cybertools.relation.registry import RelationsRegistry >>> from cybertools.relation.interfaces import IRelationsRegistry >>> from zope.app.testing import ztapi - >>> ztapi.provideUtility(IRelationsRegistry, RelationsRegistry()) + >>> ztapi.provideUtility(IRelationsRegistry, RelationsRegistry()) + >>> from zope.app import zapi + >>> relations = zapi.getUtility(IRelationsRegistry) + +In real life the indexes needed will be set up manually after object creation +or via subscription to IObjectCreatedEvent - here we have to do this +explicitly: + + >>> relations.setupIndexes() + In order to register relations the objects that are referenced have to be registered with an IntIds (unique ids) utility, so we have to set up such an utility (using a stub/dummy implementation for testing purposes): @@ -139,9 +148,6 @@ the attributes needed for indexing: So we are ready again to register a set of relations with our new relations registry and query it. - >>> from zope.app import zapi - >>> relations = zapi.getUtility(IRelationsRegistry) - >>> relations.register(LivesIn(clark, washington)) >>> relations.register(LivesIn(audrey, newyork)) >>> relations.register(LivesIn(kirk, newyork)) @@ -189,28 +195,51 @@ It should work also for triadic relations: Handling object removal ~~~~~~~~~~~~~~~~~~~~~~~ -Often it is desirable to unregister a when one of the objects -involved in the relation is removed from its container. This can be +Often it is desirable to unregister a relation when one of the objects +involved in it is removed from its container. This can be done by subscribing to IObjectRemovedEvent. The relation.registry module -provides a simple handler for this event. +provides a simple handler for this event. (In real life all this is +done via configure.zcml - see relation/configure.zcml for an example that +also provides the default behaviour.) >>> from zope.app.container.interfaces import IObjectRemovedEvent - >>> from zope.interface import Interface - >>> from cybertools.relation.registry import unregisterRelations - >>> ztapi.subscribe([Interface, IObjectRemovedEvent], None, unregisterRelations) - -We simulate the removal of kirk by calling notify: - >>> from zope.app.container.contained import ObjectRemovedEvent >>> from zope.event import notify + >>> from zope.interface import Interface + >>> from cybertools.relation.registry import invalidateRelations + >>> ztapi.subscribe([Interface, IObjectRemovedEvent], None, + ... invalidateRelations) + +The invalidateRelations handler will query for all relations the object to be +removed is involved in and then fire for these relations a +IRelationInvalidatedEvent. This then does the real work. + +So we also have to subscribe to this event. We use a standard handler that +removes the relation from whereever it is known that is provided in +the registry module. (There might be other handlers that raise an +exception thus preventing the removal of objects that take part in certain +relations.) + + >>> from cybertools.relation.interfaces import IRelation + >>> from cybertools.relation.interfaces import IRelationInvalidatedEvent + >>> from cybertools.relation.registry import removeRelation + + >>> ztapi.subscribe([IRelation, IRelationInvalidatedEvent], None, + ... removeRelation) + +Let's first check if everything is still as before: + >>> len(relations.query(first=clark)) 2 +We simulate the removal of kirk by calling notify, so clark hasn't got +a son any longer :-( + >>> notify(ObjectRemovedEvent(kirk)) Thus there should only remain one relation containing clark as first: >>> len(relations.query(first=clark)) 1 - \ No newline at end of file + diff --git a/relation/configure.zcml b/relation/configure.zcml index 700b7ed..fb2d37c 100644 --- a/relation/configure.zcml +++ b/relation/configure.zcml @@ -36,10 +36,22 @@ trusted="true" /> + + + + diff --git a/relation/ftests.py b/relation/ftests.py index 0159195..be55db1 100755 --- a/relation/ftests.py +++ b/relation/ftests.py @@ -2,12 +2,28 @@ import unittest, doctest from zope.app.testing.functional import FunctionalTestCase +from zope.app.testing import setup from zope.testbrowser import Browser +from zope.app import component, intid, zapi + class BrowserTest(FunctionalTestCase): "Functional tests for the relation package." - def test(self): + def setUp(self): + super(BrowserTest, self).setUp() + root = self.getRootFolder() + sitemanager = zapi.getSiteManager(root) + #defaultSite = component.site.LocalSiteManager(root)['default'] + default = sitemanager['default'] + intids = intid.IntIds() + default['intids'] = intids + reg = component.site.UtilityRegistration(u'', + intid.interfaces.IIntIds, default['intids']) + key = default.registrationManager.addRegistration(reg) + default.registrationManager[key].status = component.interfaces.registration.ActiveStatus + + def test(self): browser = Browser() browser.handleErrors = False browser.addHeader('Authorization', 'Basic mgr:mgrpw') @@ -26,7 +42,7 @@ class BrowserTest(FunctionalTestCase): def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS - #browser = FunctionalDocFileSuite('skin/cyberview.txt', optionflags=flags) + #browser = FunctionalDocFileSuite('funky.txt', optionflags=flags) browser = unittest.makeSuite(BrowserTest) return unittest.TestSuite((browser,)) diff --git a/relation/interfaces.py b/relation/interfaces.py index 22d5973..0b9090b 100644 --- a/relation/interfaces.py +++ b/relation/interfaces.py @@ -23,8 +23,41 @@ $Id$ """ from zope.interface import Interface, Attribute +from zope.app.event.interfaces import IObjectEvent +# relation interfaces + +class IRelation(Interface): + """ Base interface for relations. + """ + + +class IDyadicRelation(IRelation): + """ Relation connecting two objects. + """ + + first = Attribute('First object that belongs to the relation.') + second = Attribute('Second object that belongs to the relation.') + + +class ITriadicRelation(IDyadicRelation): + """ Relation connecting three objects. + """ + + third = Attribute('Third object that belongs to the relation.') + + +# event interfaces + +class IRelationInvalidatedEvent(IObjectEvent): + """ This event fires when a relation is invalidated, typically because + an object that is involved in the relation is removed. + """ + + +# relations registry interfaces + class IRelationsRegistryUpdate(Interface): """ Interface for registering and unregistering relations with a relations registry. @@ -56,22 +89,3 @@ class IRelationsRegistry(IRelationsRegistryUpdate, IRelationsRegistryQuery): """ -class IRelation(Interface): - """ Base interface for relations. - """ - - -class IDyadicRelation(IRelation): - """ Relation connecting two objects. - """ - - first = Attribute('First object that belongs to the relation.') - second = Attribute('Second object that belongs to the relation.') - - -class ITriadicRelation(IDyadicRelation): - """ Relation connecting three objects. - """ - - third = Attribute('Third object that belongs to the relation.') - diff --git a/relation/registry.py b/relation/registry.py index 1a19f6a..df70dbe 100644 --- a/relation/registry.py +++ b/relation/registry.py @@ -28,8 +28,12 @@ from zope.app import zapi from zope.app.catalog.catalog import Catalog from zope.app.catalog.field import FieldIndex from zope.app.intid.interfaces import IIntIds +from zope.app.location.interfaces import ILocation +from zope.event import notify +from zope.app.event.objectevent import ObjectEvent +from zope.security.proxy import removeSecurityProxy -from interfaces import IRelationsRegistry +from interfaces import IRelationsRegistry, IRelationInvalidatedEvent class DummyRelationsRegistry(object): @@ -70,18 +74,13 @@ class RelationsRegistry(Catalog): implements(IRelationsRegistry) - indexesSetUp = False - def setupIndexes(self): - self['relationship'] = FieldIndex('relationship', IIndexableRelation) - self['first'] = FieldIndex('first', IIndexableRelation) - self['second'] = FieldIndex('second', IIndexableRelation) - self['third'] = FieldIndex('third', IIndexableRelation) - self.indexesSetUp = True + for idx in ('relationship', 'first', 'second', 'third'): + if idx not in self: + self[idx] = FieldIndex(idx, IIndexableRelation) def register(self, relation): - if not self.indexesSetUp: - self.setupIndexes() + #self.setupIndexes() self.index_doc(_getUid(relation), relation) def unregister(self, relation): @@ -133,24 +132,48 @@ def _getUid(ob): return zapi.getUtility(IIntIds).getId(ob) def _getRelationship(relation): - return _getClassString(relation.__class__) + return _getClassString(removeSecurityProxy(relation).__class__) def _getClassString(cls): - return cls.__module__ + '.' + cls.__name__ + return '%s.%s' % (cls.__module__, cls.__name__) -# event handler +# events and handlers -def unregisterRelations(context, event): - """ Handles IObjectRemoved event: unregisters all relations for the - object that has been removed. +class RelationInvalidatedEvent(ObjectEvent): + implements(IRelationInvalidatedEvent) + + +def invalidateRelations(context, event): + """ Handles IObjectRemoved event: sends out an IRelationInvalidatedEvent + for all relations the object to be removed is involved in. """ relations = [] registry = zapi.getUtility(IRelationsRegistry) for attr in ('first', 'second', 'third'): relations = registry.query(**{attr: context}) for relation in relations: - registry.unregister(relation) - # to do: unregister relation also from the IntId utility - # (if appropriate). + notify(RelationInvalidatedEvent(relation)) + +def removeRelation(context, event): + """ Handles IRelationInvalidatedEvent by unregistering the relation + and removing it from its container (if appropriate) and the IntIds + utility. + """ + registry = zapi.getUtility(IRelationsRegistry) + registry.unregister(context) + if ILocation.providedBy(context): + parent = zapi.getParent(context) + if parent is not None: + del parent[context] + intids = zapi.getUtility(IIntIds) + intids.unregister(context) + +def setupIndexes(context, event): + """ Handles IObjectCreated event for the RelationsRegistry utility + and creates the indexes needed. + """ + if isinstance(context, RelationsRegistry): + context.setupIndexes() + diff --git a/relation/tests.py b/relation/tests.py index e0a46c0..1aa7c8c 100755 --- a/relation/tests.py +++ b/relation/tests.py @@ -29,6 +29,10 @@ class IntIdsStub: self.objs.append(ob) return self.objs.index(ob) + def unregister(self, ob): + id = self.getId(ob) + self.objs[id] = None + class TestRelation(unittest.TestCase): "Basic tests for the relation package."