Setting Up a Subscriber in Five
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 ifType Ahas, or later acquires, amanage_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):
|
|
| [*] 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

