Using a relationfield to express relationships between objects

Author:Unknown
Version:unknown

Introduction

It is sometimes desirable to somehow have objects store references to other objects. There are different ways to do this, some better than others, some easier than others. I’ll show you one way of doing it, based on the z3c.relationfield and z3c.relationfieldui packages. I won’t claim it’s the best way out there, but I certainly found it easy!

Prerequisites

You should know how to work with schema and AddForms based on them. You should also know about catalogs, indexes and integer IDs.

How Relations work

The z3c.relationfield (currently) provides 3 fields you can use as schema fields in your interface definitions: Relation, RelationChoice and RelationList. The value(s) they store are RelationValue objects.

You could store relations to objects by using standard python references, by doing

a = A()
b = B()
...
b.rel = a

This will work perfectly, and even if ‘a’ and ‘b’ are already stored separately in the ZODB and afterwards related like that, the ZODB is smart enough not to store a copy of ‘a’, but a real reference to ‘a’. (which is, by the way, a common misconception about the ZODB [1]). But this has a problem: if you later intend to remove ‘a’ from the ZODB (by removing it from its container), it won’t actually be deleted. You won’t find it under its previous container, but ‘b.rel’ will still yield the ‘a’ object, as ‘b’ holds a reference to it.

This is the same problem normal indexes in catalogs have, and they solved it by using integer IDs for objects: each object that is entered into the ZODB gets its own unique ID. If you have this ID, you can later look up the object it represents, whenever you need it.

The same problem can usually use the same solution, so a RelationValue merely stores the integer ID of the ‘to’ object. As the RelationValue is meant to be stored as an attribute of the ‘from’ object, storing the id of the ‘from’ object doesn’t make much sense, so a real reference is stored here. With the ‘to_object’ and ‘from_object’ attributes you can get to the actual objects of the RelationValue, ‘to_id’ and ‘from_id’ gives access to the integer IDs.

This, of course requires you to have an Unique Integer ID utility activated in your app. You can do this by configuring your application like this:

from zope.app.intid.interfaces import IIntIds
from zope.app.intid import IntIds

class Relations(grok.Application, grok.Container):
  grok.local_utility(IntIds, provides=IIntIds)

Searching relations

A very handy and important feature of zc.relation is the relation catalog (zc.relation.catalog.Catalog): it enables you to search for specific relations, like ‘Give me all employees that have to report to this manager’. However, zc.relation requires you still to define indexes, it just provided the catalog functionality. Luckily z3c.relationfield provides a RelationCatalog that derives from zc.relation.catalog.Catalog, but automatically creates the needed indexes. So creating the catalog is quite easy:

from zc.relation.interfaces import ICatalog
from z3c.relationfield import RelationCatalog

class Test(grok.Application, grok.Container):
  grok.local_utility(IntIds, provides=IIntIds)
  grok.local_utility(RelationCatalog, provides=ICatalog)

z3c.relationfield also defines the appropriate event handlers so that all objects that have relations are automatically indexed. This is done by using the ‘marker interfaces’ IHasOutgoingRelations, IHasIncomingRelations, IHasRelations. The first one indicates that the object refers to another object, the second one that the object can be referred to by another object. The 3rd one is a combination of both

Relation and RelationChoice

Enough theory, let’s define an application to use our relations in. Imagine you want an app in which everyone can have a buddy. So every ‘buddy’ object can have a ‘link’ to another buddy object. The relationfieldui[4] package has 2 possible schema fields for this: Relation and RelationChoice. The difference is really about what widget will be used to render it in a form. Relation will render a textbox and a button that will popup a window in which you are to select an object and then via AJAX fill in the path to the object. The other one however, works just like an ordinary Choice field, so we’ll use that one. Let’s define the buddy interface like this:

from zope.interface import Interface
from zope import schema
from z3c.relationfield import RelationChoice

class IBuddy(Interface, IHasRelations):
    name = schema.TextLine(title=u'Name')
    buddy = RelationChoice(title=u'Buddy', source = BuddySource(), required = False)

Note the use of the IHasRelations marker interface.

As the RelationChoice field works like a normal Choice field, you need to specify a set of values, a vocabulary or a source where it will draw it’s values from. I use a source, and the relationfieldui defines a handy baseclass you can use: RelationSourceFactory.

from z3c.relationfieldui import RelationSourceFactory

class BuddySource(RelationSourceFactory):
    def getTargets(self):
        return [b for b in grok.getSite().values() if IBuddy.providedBy(b)]
    def getTitle(self, value):
        return value.to_object.name

All you need to do is define a getTargets method that returns an iterable to all the objects that can be a target for your relation, and a getTitle method that can turn a RelationValue into a human readable text to show in the drop- down list.

Another thing you’ll be needing is an IObjectPath implementation, like this:

from z3c.objpath.interfaces import IObjectPath
from z3c.objpath import path, resolve

class ObjectPath(grok.GlobalUtility):
  grok.provides(IObjectPath)
  def path(self, obj):
      return path(grok.getSite(), obj)
  def resolve(self, path):
      return resolve(grok.getSite(), path)

This class can create a path to an object or an object from a path. A path is really how to get to the object in the ZODB. The RelationSourceFactory uses this to create tokens from objects.

Now let’s implement IBuddy and create an AddForm and a View:

class Buddy(grok.Model):
  grok.implements(IBuddy)

class AddBuddy(grok.AddForm):
  grok.context(grok.Container)
  grok.name('add')
  form_fields = grok.Fields(IBuddy)

  @grok.action('Add')
  def Add(self, **data):
      buddy = Buddy()
      self.applyData(buddy, **data)
      self.context[buddy.name] = buddy
      self.redirect(self.url(buddy))

class BuddyView(grok.View):
  grok.context(IBuddy)
  grok.name('index')

The buddyview template may look like this:

<html>
<head></head>
<body>
<h1 tal:content="context/name"/>
<span tal:condition="context/buddy/to_object | nothing">
 <dl>
       <dt><em>My Buddy:</em></dt>
       <dd  tal:content="context/buddy/to_object/name"/>
 </dl>
</span>
</body>
</html>

The main Index view can be changed to

class Index(grok.View):
  grok.context(Relations) # see app_templates/index.pt

  def buddies(self):
      return [b for b in grok.getSite().values() if IBuddy.providedBy(b)]

Your main index view template can look like this

<html>
<head></head>
<body>
<h1>Buddies</h1>
<ul>
<li tal:repeat="buddy view/buddies"><a tal:attributes="href python:view.url(buddy)" tal:content="buddy/name"/></li>
</ul>
<a tal:attributes="href python:view.url('add')">Add buddy</a>
</body>
</html>

Now start the server, add your app (I named it ‘test’), and go to http://localhost:8080/test. Click ‘Add Buddy’ and you should see a form. Enter the name and click add. Of course you can’t select a buddy yet, as there are none. If you now go back to the add form (and refresh the page), you’ll notice that the buddy you added before can now be selected as the buddy for the buddy you are to create.

Am I my buddy’s buddy?

How can we now find out who I am a buddy of? By searching the relation catalog! Add this method to the BuddyView:

from zope import component

  def referers(self):
      catalog = component.getUtility(ICatalog)
      intids = component.getUtility(IIntIds)
      return list(catalog.findRelations({'to_id': intids.getId(self.context)}))

Change the buddyview template to this:

<html>
<head></head>
<body>
<h1 tal:content="context/name"/>
<span tal:condition="context/buddy/to_object | nothing">
 <dl>
       <dt><em>My Buddy:</em></dt>
       <dd  tal:content="context/buddy/to_object/name"/>
 </dl>
</span>
<span tal:define="referers view/referers" tal:condition="referers | nothing">
       <dt><em>I am a buddy of</em></dt>
       <dd tal:repeat="ref referers" tal:content="ref/from_object/name"/>
</span>
</body>
</html>

Hiding the relation inside the implementation

You most probably noticed the gratuitous use of ‘to_object’ to actually get to the referred object. This can be quite annoying: imagine you want to get the name of your buddy’s buddy:

me.buddy.to_object.buddy.to_object.name

Not so nice...

I’ll present you a way of getting rid of this by hiding the entire relation inside the application so that my grandfathers again become me.father.father and me.mother.father, so to speak. I also feel that using the RelationChoice directly in the interface is like forcing the users of the interface to know how to use z3c.relationfield, while all they want is to get to the actual objects you refer to. You might also want to refactor an existing app that implemented references in a different way (for instance by simply storing the id) to zc.relationfield without wanting to change any existing interfaces.

Remove the IHasRelations from the IBuddy interface:

class IBuddy(Interface):
  name = schema.TextLine(title=u'The Name')
  buddy = schema.Choice(title=u'My Buddy', required=False, source = BuddySource())

And change the BuddySource to an ‘ordinary’ source:

from zc.sourcefactory.basic import BasicSourceFactory

class BuddySource(BasicSourceFactory):
  def getValues(self):
      return grok.getSite()['buddies'].values()
  def getTitle(self, value):
      return value.to_object.name

Now we need to adapt the IBuddy implementation, which is the Buddy class. We still will be using a RelationValue somewhere in the background, and use that each time the buddy attribute is accessed. You might be tempted to try something like this:

class Buddy(grok.Model):
  grok.implements(IBuddy, IHasRelations)
  buddy_rel = Relation()

However, this won’t work, because the relationfield packages automatically creates the relation indexes by looking up all fields of the interfaces the newly created object implements and checking if they provide IRelation (or IRelationList). In this case, there is no interface that has any, so the relation catalog will not be correctly updated.

The trick here is to first subclass the IBuddy interface, and then implement that new interface:

class IRelationBuddy(IBuddy, IHasRelations):
  buddy_rel = Relation()

  def referers():
      "iterate over all referers"

class Buddy(grok.Model):
  grok.implements(IRelationBuddy)

  def setBuddy(self, obj):
      intids = component.getUtility(IIntIds)
      self.buddy_rel = RelationValue(intids.queryId(obj))
  def getBuddy(self):
      return self.buddy_rel.to_object
  buddy=property(getBuddy, setBuddy)

  def referers(self):
      catalog = component.getUtility(ICatalog)
      intids = component.getUtility(IIntIds)
      return [r.from_object for r in catalog.findRelations({'to_id': intids.getId(self)})]

As you may have noticed, I also moved the ‘referers’ method from the view to the IRelationBuddy class.

Now you can change the buddyview.pt into:

<html>
 <head></head>
 <body>
 <h1 tal:content="context/name"/>
 <span tal:condition="context/buddy | nothing">
  <dl>
      <dt><em>My Buddy:</em></dt>
      <dd  tal:content="context/buddy/name"/>
  </dl>
 </span>
 <span tal:define="referers context/referers" tal:condition="referers | nothing">
      <dt><em>I am a buddy of</em></dt>
      <dd tal:repeat="ref referers" tal:content="ref/name"/>
 </span>
 </body>
 </html>

Notice the lack of ‘to_object’ and ‘from_object’!

I’ll leave the use of the RelationList as an exercise for the reader...