Package: z3c.relationfield
z3c.relationfieldIntroductionThis package implements a new schema field Relation, and the
RelationValue objects that store actual relations. It can index these
relations using thezc.relationinfractructure, and using these
indexes can efficiently answer questions about the relations.The packagez3c.relationfielduiin addition provides a widget to
edit and display Relation fields.Setupz3c.relationfield.Relationis a schema field that can be used to
express relations. Let’s define a schema IItem that uses a relation
field:>>>fromz3c.relationfieldimportRelation>>>fromzope.interfaceimportInterface>>>classIItem(Interface):...rel=Relation(title=u"Relation")We also define a classItemthat implements bothIItemand the specialz3c.relationfield.interfaces.IHasRelationsinterface:>>>fromz3c.relationfield.interfacesimportIHasRelations>>>frompersistentimportPersistent>>>fromzope.interfaceimportimplementer>>>@implementer(IItem,IHasRelations)...classItem(Persistent):......def__init__(self):...self.rel=NoneTheIHasRelationsmarker interface is needed to let the relations
onItembe cataloged (when they are put in a container and removed
from it, for instance). It is in fact a combination ofIHasIncomingRelationsandIHasOutgoingRelations, which is fine
as we want items to support both.Finally we need a test application:>>>fromzope.site.siteimportSiteManagerContainer>>>fromzope.container.btreeimportBTreeContainer>>>classTestApp(SiteManagerContainer,BTreeContainer):...passWe set up the test application:>>>fromZODB.MappingStorageimportDB>>>db=DB()>>>conn=db.open()>>>root=conn.root()['root']=TestApp()>>>conn.add(root)We make sure that this is the current site, so we can look up local
utilities in it and so on. Normally this is done automatically by
Zope’s traversal mechanism:>>>fromzope.site.siteimportLocalSiteManager>>>root.setSiteManager(LocalSiteManager(root))>>>fromzope.component.hooksimportsetSite>>>setSite(root)For this site to work withz3c.relationship, we need to set up two
utilities. Firstly, anIIntIdsthat tracks unique ids for objects
in the ZODB:>>>fromzope.intidimportIntIds>>>fromzope.intid.interfacesimportIIntIds>>>root['intids']=intids=IntIds()>>>sm=root.getSiteManager()>>>sm.registerUtility(intids,provided=IIntIds)And secondly a relation catalog that actually indexes the relations:>>>fromz3c.relationfieldimportRelationCatalog>>>fromzc.relation.interfacesimportICatalog>>>root['catalog']=catalog=RelationCatalog()>>>sm.registerUtility(catalog,provided=ICatalog)Using the relation fieldWe’ll add an itemato our application:>>>root['a']=Item()All items, including the one we just created, should have unique int
ids as this is required to link to them:>>>fromzopeimportcomponent>>>fromzope.intid.interfacesimportIIntIds>>>intids=component.getUtility(IIntIds)>>>a_id=intids.getId(root['a'])>>>a_id>=0TrueThe relation is currentlyNone:>>>root['a'].relisNoneTrueNow we can create an itembthat links to itema(through its
int id):>>>fromz3c.relationfieldimportRelationValue>>>b=Item()>>>b.rel=RelationValue(a_id)We now store thebobject in a container, which will also set up
its relation (as anIObjectAddedEventwill be fired):>>>root['b']=bLet’s examine the relation. First we’ll check which attribute of the
pointing object (‘b’) this relation is pointing from:>>>root['b'].rel.from_attribute'rel'We can ask for the object it is pointing at:>>>to_object=root['b'].rel.to_object>>>to_object.__name__'a'We can also get the object that is doing the pointing; since we
supplied theIHasRelationsinterface, the event system took care
of setting this:>>>from_object=root['b'].rel.from_object>>>from_object.__name__'b'This object is also known as the__parent__; again the event
sytem took care of setting this:>>>parent_object=root['b'].rel.__parent__>>>parent_objectisfrom_objectTrueThe relation also knows about the interfaces of both the pointing object
and the object that is being pointed at:>>>frompprintimportpprint>>>pprint(sorted(root['b'].rel.from_interfaces))[<InterfaceClasszope.location.interfaces.IContained>,<InterfaceClassz3c.relationfield.interfaces.IHasRelations>,<InterfaceClassbuiltins.IItem>,<InterfaceClasspersistent.interfaces.IPersistent>]>>>pprint(sorted(root['b'].rel.to_interfaces))[<InterfaceClasszope.location.interfaces.IContained>,<InterfaceClassz3c.relationfield.interfaces.IHasRelations>,<InterfaceClassbuiltins.IItem>,<InterfaceClasspersistent.interfaces.IPersistent>]We can also get the interfaces in flattened form:>>>pprint(sorted(root['b'].rel.from_interfaces_flattened))[<InterfaceClasszope.location.interfaces.IContained>,<InterfaceClassz3c.relationfield.interfaces.IHasIncomingRelations>,<InterfaceClassz3c.relationfield.interfaces.IHasOutgoingRelations>,<InterfaceClassz3c.relationfield.interfaces.IHasRelations>,<InterfaceClassbuiltins.IItem>,<InterfaceClasszope.location.interfaces.ILocation>,<InterfaceClasspersistent.interfaces.IPersistent>,<InterfaceClasszope.interface.Interface>]>>>pprint(sorted(root['b'].rel.to_interfaces_flattened))[<InterfaceClasszope.location.interfaces.IContained>,<InterfaceClassz3c.relationfield.interfaces.IHasIncomingRelations>,<InterfaceClassz3c.relationfield.interfaces.IHasOutgoingRelations>,<InterfaceClassz3c.relationfield.interfaces.IHasRelations>,<InterfaceClassbuiltins.IItem>,<InterfaceClasszope.location.interfaces.ILocation>,<InterfaceClasspersistent.interfaces.IPersistent>,<InterfaceClasszope.interface.Interface>]PathsWe can also obtain the path of the relation (both from where it is
pointing as well as to where it is pointing). The path should be a
human-readable reference to the object we are pointing at, suitable
for serialization. In order to work with paths, we first need to set
up anIObjectPathutility.Since in this example we only place objects into a single flat root
container, the paths in this demonstration can be extremely simple:
just the name of the object we point to. In more sophisticated
applications a path would typically be a slash separated path, like/foo/bar:>>>fromzope.interfaceimportInterface>>>fromzope.interfaceimportimplementer>>>fromz3c.objpath.interfacesimportIObjectPath>>>@implementer(IObjectPath)...classObjectPath(object):......defpath(self,obj):...returnobj.__name__...defresolve(self,path):...try:...returnroot[path]...exceptKeyError:...raiseValueError("Cannot resolve path%s"%path)>>>fromzope.componentimportgetGlobalSiteManager>>>gsm=getGlobalSiteManager()>>>op=ObjectPath()>>>gsm.registerUtility(op)After this, we can get the path of the object the relation points to:>>>root['b'].rel.to_path'a'We can also get the path of the object that is doing the pointing:>>>root['b'].rel.from_path'b'Comparing and sorting relationsLet’s create a bunch ofRelationValueobjects and compare them:>>>rel_to_a=RelationValue(a_id)>>>b_id=intids.getId(root['b'])>>>rel_to_b=RelationValue(b_id)>>>rel_to_a==rel_to_bFalseRelations of course are equal to themselves:>>>rel_to_a==rel_to_aTrueA relation that is stored is equal to a relation that isn’t stored yet:>>>root['b'].rel==rel_to_aTrueWe can also sort relations:>>>expected=[('','a'),('','b'),('b','a')]>>>observed=[(rel.from_path,rel.to_path)forrelin...sorted([root['b'].rel,rel_to_a,rel_to_b])]>>>expected==observedTrueRelation queriesNow that we have set up and indexed a relationship betweenaandb, we can issue queries using the relation catalog. Let’s first
get the catalog:>>>fromzc.relation.interfacesimportICatalog>>>catalog=component.getUtility(ICatalog)Let’s ask the catalog about the relation frombtoa:>>>l=sorted(catalog.findRelations({'to_id':intids.getId(root['a'])}))>>>l[<...RelationValueobjectat...>]We look at this relation object again. We indeed go the right one:>>>rel=l[0]>>>rel.from_object.__name__'b'>>>rel.to_object.__name__'a'>>>rel.from_path'b'>>>rel.to_path'a'Asking for relations tobwill result in an empty list, as no such
relations have been set up:>>>sorted(catalog.findRelations({'to_id':intids.getId(root['b'])}))[]We can also issue more specific queries, restricting it on the
attribute used for the relation field and the interfaces provided by
the related objects. Here we look for all relations betweenbandathat are stored in object attributereland are pointing
from an object with interfaceIItemto another object with the
interfaceIItem:>>>sorted(catalog.findRelations({...'to_id':intids.getId(root['a']),...'from_attribute':'rel',...'from_interfaces_flattened':IItem,...'to_interfaces_flattened':IItem}))[<...RelationValueobjectat...>]There are no relations stored for another attribute:>>>sorted(catalog.findRelations({...'to_id':intids.getId(root['a']),...'from_attribute':'foo'}))[]There are also no relations stored for a new interface we’ll introduce
here:>>>classIFoo(IItem):...pass>>>sorted(catalog.findRelations({...'to_id':intids.getId(root['a']),...'from_interfaces_flattened':IItem,...'to_interfaces_flattened':IFoo}))[]Changing the relationLet’s create a new objectc:>>>root['c']=Item()>>>c_id=intids.getId(root['c'])Nothing points tocyet:>>>sorted(catalog.findRelations({'to_id':c_id}))[]We currently have a relation frombtoa:>>>sorted(catalog.findRelations({'to_id':intids.getId(root['a'])}))[<...RelationValueobjectat...>]We can change the relation to point at a new objectc:>>>root['b'].rel=RelationValue(c_id)We need to send anIObjectModifiedEventto let the catalog know we
have changed the relations:>>>fromzope.eventimportnotify>>>fromzope.lifecycleeventimportObjectModifiedEvent>>>notify(ObjectModifiedEvent(root['b']))We should find now a single relation frombtoc:>>>sorted(catalog.findRelations({'to_id':c_id}))[<...RelationValueobjectat...>]The relation toashould now be gone:>>>sorted(catalog.findRelations({'to_id':intids.getId(root['a'])}))[]If we store the relation in a non schema field it should persist
the ObjectModifiedEvent.>>>fromz3c.relationfield.eventimport_setRelation>>>_setRelation(root['b'],'my-fancy-relation',rel_to_a)>>>sorted(catalog.findRelations({'to_id':intids.getId(root['a'])}))[<...RelationValueobjectat...>]>>>notify(ObjectModifiedEvent(root['b']))>>>rel=sorted(catalog.findRelations({'to_id':intids.getId(root['a'])}))>>>rel[<...RelationValueobjectat...>]>>>catalog.unindex(rel[0])Removing the relationWe have a relation frombtocright now:>>>sorted(catalog.findRelations({'to_id':c_id}))[<...RelationValueobjectat...>]We can clean up an existing relation frombtocby setting it
toNone:>>>root['b'].rel=NoneWe need to send anIObjectModifiedEventto let the catalog know we
have changed the relations:>>>notify(ObjectModifiedEvent(root['b']))Setting the relation onbtoNoneshould remove that relation
from the relation catalog, so we shouldn’t be able to find it anymore:>>>sorted(catalog.findRelations({'to_id':intids.getId(root['c'])}))[]Let’s reestablish the removed relation:>>>root['b'].rel=RelationValue(c_id)>>>notify(ObjectModifiedEvent(root['b']))>>>sorted(catalog.findRelations({'to_id':c_id}))[<...RelationValueobjectat...>]Copying an object with relationsLet’s copy an object with relations:>>>fromzope.copypastemove.interfacesimportIObjectCopier>>>IObjectCopier(root['b']).copyTo(root)'b-2'>>>'b-2'inrootTrueTwo relations toccan now be found, one from the original, and
the other from the copy:>>>l=sorted(catalog.findRelations({'to_id':c_id}))>>>len(l)2>>>l[0].from_path'b'>>>l[1].from_path'b-2'Relations are sortableRelations are sorted by default on a combination of the relation name,
the path of the object the relation is one and the path of the object
the relation is pointing to.Let’s query all relations availble right now and sort them:>>>l=sorted(catalog.findRelations())>>>len(l)2>>>l[0].from_attribute'rel'>>>l[1].from_attribute'rel'>>>l[0].from_path'b'>>>l[1].from_path'b-2'Removing an object with relationsWe will removeb-2again. Its relation should automatically be remove
from the catalog:>>>delroot['b-2']>>>l=sorted(catalog.findRelations({'to_id':c_id}))>>>len(l)1>>>l[0].from_path'b'Breaking a relationWe have a relation frombtocright now:>>>sorted(catalog.findRelations({'to_id':c_id}))[<...RelationValueobjectat...>]We have no broken relations:>>>sorted(catalog.findRelations({'to_id':None}))[]The relation isn’t broken:>>>b.rel.isBroken()FalseWe are now going to break this relation by removingc:>>>delroot['c']The relation is broken now:>>>b.rel.isBroken()TrueThe original relation still has ato_path:>>>b.rel.to_path'c'It’s broken however as there is noto_object:>>>b.rel.to_objectisNoneTrueTheto_idis also gone:>>>b.rel.to_idisNoneTrueWe cannot find the broken relation in the catalog this way as it’s not
pointing toc_idanymore:>>>sorted(catalog.findRelations({'to_id':c_id}))[]We can however find it by searching for relations that have ato_idofNone:>>>sorted(catalog.findRelations({'to_id':None}))[<...RelationValueobjectat...>]A broken relation isn’t equal toNone(this was a bug):>>>b.rel==NoneFalseRelationChoiceARelationChoicefield is much like an ordinaryRelationfield
but can be used to render a special widget that allows a choice of
selections.We will first demonstrate aRelationChoicefield has the same effect
as aRelationfield itself:>>>fromz3c.relationfieldimportRelationChoice>>>classIChoiceItem(Interface):...rel=RelationChoice(title=u"Relation",values=[])>>>@implementer(IChoiceItem,IHasRelations)...classChoiceItem(Persistent):......def__init__(self):...self.rel=NoneLet’s create an object to point the relation to:>>>root['some_object']=Item()>>>some_object_id=intids.getId(root['some_object'])And let’s establish the relation:.. code-block::python>>> choice_item = ChoiceItem()
>>> choice_item.rel = RelationValue(some_object_id)
>>> root['choice_item'] = choice_itemWe can query for this relation now:>>>l=sorted(catalog.findRelations({'to_id':some_object_id}))>>>l[<...RelationValueobjectat...>]RelationListLet’s now experiment with theRelationListfield which can be used
to maintain a list of relations:>>>fromz3c.relationfieldimportRelationList>>>classIMultiItem(Interface):...rel=RelationList(title=u"Relation")We also define a classMultiItemthat implements bothIMultiItemand the specialz3c.relationfield.interfaces.IHasRelationsinterface:>>>@implementer(IMultiItem,IHasRelations)...classMultiItem(Persistent):......def__init__(self):...self.rel=NoneWe set up a few object we can then create relations between:>>>root['multi1']=MultiItem()>>>root['multi2']=MultiItem()>>>root['multi3']=MultiItem()Let’s create a relation frommulti1to bothmulti2andmulti3:>>>multi1_id=intids.getId(root['multi1'])>>>multi2_id=intids.getId(root['multi2'])>>>multi3_id=intids.getId(root['multi3'])>>>root['multi1'].rel=[RelationValue(multi2_id),...RelationValue(multi3_id)]We need to notify that we modified the ObjectModifiedEvent>>>notify(ObjectModifiedEvent(root['multi1']))Now that this is set up, let’s verify whether we can find the
proper relations in in the catalog:>>>len(list(catalog.findRelations({'to_id':multi2_id})))1>>>len(list(catalog.findRelations({'to_id':multi3_id})))1>>>len(list(catalog.findRelations({'from_id':multi1_id})))2Temporary relationsIf we have an import procedure where we import relations from some
external source such as an XML file, it may be that we read a relation
that points to an object that does not yet exist as it is yet to be
imported. We provide a specialTemporaryRelationValuefor this
case.  ATemporaryRelationValuejust contains the path of what it
is pointing to, but does not resolve it yet. Let’s useTemporaryRelationValuein a new object, creating a relation toa:>>>fromz3c.relationfieldimportTemporaryRelationValue>>>root['d']=Item()>>>root['d'].rel=TemporaryRelationValue('a')A modification event does not actually get this relation cataloged:>>>before=sorted(catalog.findRelations({'to_id':a_id}))>>>notify(ObjectModifiedEvent(root['d']))>>>after=sorted(catalog.findRelations({'to_id':a_id}))>>>len(before)==len(after)TrueWe will now convert all temporary relations ondto real ones:>>>fromz3c.relationfieldimportrealize_relations>>>realize_relations(root['d'])>>>notify(ObjectModifiedEvent(root['d']))We can see the real relation object now:>>>root['d'].rel<...RelationValueobjectat...>The relation will also now show up in the catalog:>>>after2=sorted(catalog.findRelations({'to_id':a_id}))>>>len(after2)>len(before)TrueTemporary relation values also work withRelationListobjects:>>>root['multi_temp']=MultiItem()>>>root['multi_temp'].rel=[TemporaryRelationValue('a')]Let’s convert this to a real relation:>>>realize_relations(root['multi_temp'])>>>notify(ObjectModifiedEvent(root['multi_temp']))Again we can see the real relation object when we look at it:>>>root['multi_temp'].rel[<...RelationValueobjectat...>]And we will now see this new relation appear in the catalog:>>>after3=sorted(catalog.findRelations({'to_id':a_id}))>>>len(after3)>len(after2)TrueBroken temporary relationsLet’s create another temporary relation, this time a broken one that
cannot be resolved:>>>root['e']=Item()>>>root['e'].rel=TemporaryRelationValue('nonexistent')Let’s try realizing this relation:>>>realize_relations(root['e'])We end up with a broken relation:>>>root['e'].rel.isBroken()TrueIt’s pointing to the nonexistent path:>>>root['e'].rel.to_path'nonexistent'Setting up a releation catalogThis package provides a RelationCatalog initialized with a set of indexes commonly useful for queries on RelationValue objects.
The default indexes arefrom_id,to_id,from_attribute,from_interfaces_flattenedandto_interfaces_flattened.Sometimes it is needed to define custom indexes or use less than the default ones.
Thezc.relationfield.index.RelationCatalogclass can be initialized with a list of dicts with keyselementandkwargsto be passed to RelationCatalogaddValueIndexmethod.
Aselementin general the attribute on theIRelationValuelikeIRelationValue[‘from_id’]is expected.
However, if theres a subclass ofIRelationValueis used with additional fields, those fields can be added here as indexes.CHANGES1.1 (2023-08-17)Consider RelationValue without source as broken.
[ksuess]1.0 (2023-02-22)Breaking changes:Drop support for Python 2.7, 3.5, 3.6.New features:Add support for Python 3.7, 3.8, 3.9, 3.10, 3.11.0.9.0 (2019-09-15)New features:Provide IRelationBrokenEvent to be able to distinguish the event when
subscribing to IObjectModifiedEvent
[vangheem]0.8.0 (2019-02-13)New features:AdressesStill uses BTrees wrongly, screws up people changing Interfaces, allows third party software to define which indexes are used.
[jensens]Bug fixes:Fix DeprecationWarnings intests.py.
[jensens]0.7.1 (2018-11-08)Python 3 compatibility: use the implementer decorator and fix ordering
[ale-rt]Python 3 compatibility: MakeRelationValuehashable. [sallner]RenamedREADME.txt``to``README.rstandCHANGES.txttoCHANGES.rst.
[thet]Update buildout / travis config
[tomgross]Fix issue where relations are cleared on modify if they are not stored as
an class attribute. Usecase seehttps://github.com/plone/Products.CMFPlone/issues/2384[tomgross]0.7 (2015-03-13)Remove dependencies on zope.app.*
[davisagli]0.6.3 (2014-04-15)Remove dependency on grok.
[pbauer, jensens]0.6.2 (2012-12-06)Updated test setup and test to run with current versions of dependent
packages, thus running with Python 2.6, too.Added missing (test) dependencies.Rename __neq__ method to __ne__ since __neq__ is not the right builtin
name for != handlers.0.6.1 (2009-10-11)Fixes broken release.0.6 (2009-10-11)Ensure that the value_type of a RelationList is not overwritten to be ‘None’
when the field is constructed.0.5 (2009-06-30)Move lxml and schema2xml dependencies to an [xml] extra so that people can
use this package without having to install lxml, which still causes issues
on some platforms. If z3c.schema2xml and lxml are not importable, the
relevant adapters will not be defined, but everything else will still work.Subscribe to IIntIdAddedEvent instead of IObjectAddedEvent to prevent
errors due to subscriber ordering.0.4.3 (2009-06-04)Add missing dependency for lxml.0.4.2 (2009-04-22)Prevent the event failures from failing when utilities are missing or when
objects do not implement IContained.0.4.1 (2009-02-12)Don’t handleIObjectModifiedevents for objects that do not yet
have a parent. There is no need to do so anyway, as these objects cannot
have outgoing relations indexed.0.4 (2009-02-10)Introduce aRelationChoicefield that behaves likeschema.Choicebut tracks relations. In combination with a source
(such as created byRelationSourceFactoryprovided byz3c.relationfieldui) this can be used to create drop-down
selections for relations.Clarify the way comparing and sorting ofRelationValueobjects is
done in order to better support choice support.0.3.2 (2009-01-21)When a relation is broken, properly re-catalog things.0.3.1 (2009-01-20)Introduce sensible sort order for relations, based on a(from_attribute, from_path, to_path)tuple.Relations will now never compare toNone.0.3 (2009-01-19)Introduce two new interfaces:IHasOutgoingRelationsandIHasIncomingRelations.IHasOutgoingRelationsshould be provided
by objects that actually have relations set on them, so that
they can be properly cataloged.IHasIncomingRelationsshould be
set on objects that can be related to, so that broken relations
can be properly tracked.IHasRelationsnow extends both,
so if you provide those on your object you have an object that can
have both outgoing as well as incoming relations.Improve broken relations support. When you now break a relation (by
removing the relation target),to_idandto_objectbecomeNone.to_pathhowever will remain the path that the relation
last pointed to.TemporaryRelationobjects that when realized
are broken relations can also be created.You can also for broken status by callingisBrokenon a
relation.The signature of the top-level functioncreate_relationchanged. It used to take the object to which the relation was to be
created, but should now get the path (inIObjectPathterms).create_relationwill now create a broken relation object if the
path cannot be resolved.0.2 (2009-01-08)Added support forRelationListfields. This allows one to
maintain a list ofRelationValueobjects that will be cataloged
like the regularRelationfields.Get rid ofIRelationInfoadapter requirement. Just define acreate_relationfunction that does the same work.When looking for relations on an object be more tolerant if those
cannot be found (just skip them) - this can happen when a schema is
changed.0.1 (2008-12-05)Initial public release.Download