Setting Up a Subscriber in Five

by Frank Bennett last modified Dec 30, 2008 03:03 PM
Gives a step-by-step description of how to set up a simple event-driven "listener" or "subscriber" using Five. After you have seen a concrete example, all that talk of adapters and components and whatnot may seem a little less daunting.

I recently ran into a problem that turned out to be easily fixed using the facilities available through Five (many thanks to optilude for pointing me in that direction!). This memo provides the sort of mechanical setup explanation that would have helped me resolve my local issue a little more quickly.

Hypothetical Use Case

Suppose we have a bunch of objects of Type A inside Plone, and we want to provide our users with a way to add personal notes to them. We decide to do what seems obvious at first, and provide a mechanism by which a user can create a little custom note object of Type X corresponding to a given Type A object, with an Archetypes reference connecting the two. By indexing the text of the notes, we can let our users search for items on the basis of their own notes, which will be totally cool and awesome, especially on a flash MacBook with the glossy screen. The design will look something like this:

This looks nice in diagram form, but we're going to run into a problem when it comes to implementing it in Plone. The note at the top of the diagram, "Title from Type A object" captures the issue. A search against a user's notes (say, "crappy how-to", for example) is likely to turn up multiple hits, so we will want to provide the user with a list of titles in response to every search. The search will be run against Type X objects, but the titles it returns should be those of the corresponding Type A objects. For speed, we want a Type X object to provide the title text from its own catalog metadata. We can copy it there, but the problem is that the Type A objects might be edited at any time. We need a way to keep this index metadata "in sync", even when a Type A object is modified.

In Plone under Zope 2, there are no built-in hooks to support this, as far as I know. The nearest thing is manage_afterAdd(), but this has two drawbacks for our use case:

  • This hook is only called when the object is created, not when it is modified; and
  • A method of this name needs to be written into the code of Type A, which creates a maintenance headache -- especially if Type A has, or later acquires, a manage_afterAdd() method of its own.

This is now sounding pretty complicated, so let's get stubborn here. Because we're fundamentally lazy, we want to implement our Type X note object without touching Type A's code at all. Otherwise, we should abandon this whole idea as too much of a maintenance headache. On the other hand, if we can find a painless way to keep Type A and Type X in sync, the rest of our design will pretty much fall neatly into place.

In Zope 2, we would be out of luck. But with Five, this problem can be solved rather simply.

Implementation

What we want to do is actually very simple. Every time a Type A object is modified, we want to invoke a little utility method that refreshes the Title index of the corresponding Type X "note" objects. Since Type X belongs to us and is only going to be used in this context, we can easily give it a Title() method that fetches and returns the real Title from the corresponding Type A object. With that in place, the only thing the utility method needs to do is reindex each Type X partner of the Type A object that has just been modified. This can be done using Zope 3 events, so let's take a look at how that works.

In Five, a utility method that is triggered by an event is called a "handler". Handlers take two arguments, the first for the object on which they are invoked, and the second for an "event object" that contains information about the event that is triggering the handler. The handler for our use case might look something like this:

    def myTitleHandler(ob, event):
        ob.reindexObject(idxs=['Title'])

The next question is, um, where do we put this code? Since we own the Type X product, we can put it in there. The handler runs as a bare Python function, with no self or context. So we can create a file in the top folder of our Type X product called, say, utils.py, and just put the code above directly into that file. That's it. No registration magic or boilerplate code or anything is required in the file.

Now we need to teach the server to invoke our little method when a Type A object is modified. We can do this by relying on event machinery that is built into Zope 3. "Events" are like semaphore signals sent out when certain operations are performed. You can program your own products to issue them, but in this case, we don't have to do that. Zope 2.9 offers the following event triggers (the description string from the Python source is given for each):

IBeforeTraverseEvent
An event which gets sent on publication traverse
IContainerModifiedEvent
The container has been modified
IEndRequestEvent
An event which gets sent when the publication is ended
IIntIdAddedEvent
The event which gets sent when an object is registered in a unique id utility
IIntIdRemovedEvent
The event which gets published before the unique id is removed from the utility so that the catalogs can unindex the object
IObjectAddedEvent
An object has been added to a container
IObjectAnnotationsModifiedEvent [*]
An object's annotations have been modified
IObjectContentModifiedEvent [*]
An object's content has been modified
IObjectCopiedEvent
An object has been copied
IObjectModifiedEvent
An object has been modified
IObjectMovedEvent
An object has been moved
IObjectRemovedEvent
An object has been removed from a container
IObjectWillBeAddedEvent
An object will be added to a container
IObjectWillBeMovedEvent
An object will be moved
IObjectWillBeRemovedEvent
An object will be removed from a container
[*] Deprecated and will disappear in Zope 3.3, so avoid this one.

In this particular case, it looks like IObjectModifiedEvent will suit our needs. So (to be thoroughly pedantic about this) what we want to do is invoke the little myTitleHandler utility method whenever the IObjectModifiedEvent event is triggered on a Type A object.

We are now ready to set up the Zope 3 code to establish the event handler. Our handler will be invoked by a Five "subscriber" (some people call this a "listener"). A subscriber waits for event triggers, and when one is issued, it checks both the event type and the Z3 interfaces of the object against which it was triggered. If both items match what the subscriber is looking for, the handler is invoked. Otherwise, the subscriber just sits there, waiting for the next event.

To set things up, then, we're going to need a Z3 interface on the Type A object. If Type A already has a Z3 interface, we will use that (see Walking through Five to Zope 3: Interfaces for a complete run-down on Z2 and Z3 interfaces, how they are built, where they are located, and most importantly how to test for them). As of this writing, many Plone content types don't have Z3 interfaces, so we'll assume that a bit of DIY is needed to get the Type A content type primed for our Z3 handler.

An interface is just a stub class associated with an object. For our limited purposes here, you can think of it as a kind of label that can be stuck on the object to classify it in some way. There will be two steps to setting up a Z3 interface on our Type A objects: setting up the interface class; and attaching the interface to the objects themselves.

Setting up the class is easy enough. In a file called, say, interfaces.py, in the top-level folder of our Type X product, we can set up the interface with code like this:

from zope.interface.interface import Interface as Z3Interface

class IWantsToExportTitlesToTypeX(Z3Interface):
    """ This object wants to export its titles to Type X """

To attach this to all instances of the Type A class, we can use the Five method classImplements(), which is intended exactly for this purpose. This method needs to be invoked on Zope initialization, so it belongs in the initialize method within a product's __init__.py file. Since we own the Type X product, let's put it there. The relevant code might read something like this:

# Import the Plone version of classImplements
# (converts Z2 interfaces to Z3 if necessary)
from Products.CMFPlone.utils import classImplements

# Import the interface we're going to slap on Type A
from Products.ProductX.interfaces import IWantsToExportTitlesToTypeX

# Import the Type A class
from Products.ProductA.content.typeA import TypeA

def initialize(context):

    classImplements(TypeA,IWantsToExportTitlesToTypeX)

Note that we did not have to modify the code of the Type A product in any way to add the interface; it can be done "from a distance" by code inside our Type X product. Note also that we could equally well slap this interface on several target content types, if we wanted to, which would extend our Type X note system to those additional object types; the event machinery in Five reacts to interface labels, not to the object type, so no other changes would be necessary.

The only task remaining is to tell the events machinery of Five to invoke our handler on objects with this interface when they are modified. This is done using a configure.zcml file. To keep things clear and compact, let's put this file in the top level of the Type X product directory as well.

First off, here's what an empty configure.zcml file looks like:

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:browser="http://namespaces.zope.org/browser"
    xmlns:five="http://namespaces.zope.org/five">

</configure>

The code for our subscriber will go in between the configure tags. If you already have a configure.zcml file, it may have other items in it already; no matter, we can just add our subscriber to the list of items. The code of the subscriber will look something like this:

    <subscriber
        for="Products.TypeX.interfaces.IWantsToExportTitlesToTypeX
             zope.app.event.interfaces.IObjectModifiedEvent"
        handler="Products.TypeX.utils.myTitleHandler" />

(The one item in the code above that needs a little explaining is the line with IObjectModifiedEvent. Some of the event interfaces may not be located in zope.app.event.interfaces; you may need to find this interface in the Zope software home using grep (in Unix) or some other search utility if you use another operating system.)

Restart Zope, and things should just work. If you need to debug the setup, or if you're just curious about what the world looks like from inside the handler, you can import zLOG into the handler and issue log entries when it is invoked.

This how-to is only meant to help people who are new to Five (as I was until yesterday ...) to get a feel for how things fit together. If it's served that purpose, hurrah! Otherwise, there's a lot of other documentation out there ...

And, er ... that's it.

Further information

The following are good places to check for information on the simple wonders of Zope 3 and Five (I'd recommend starting at the top and working down):

In the Zope 2.9 software home:
  • Products/Five/doc/manual.txt
  • Products/Five/tests/event.txt
On the Web: