07 Januar 2006

Zope3 Events


Every now and then, while working on fsgallery, I realized that I need some kind of a notification system - feedback from the gallery engine if something goes wrong. Zope3 is highly unitized and after some time one tends to split a Zope3 application into coherent units (components - adapters/utilities/content type/browser related stuff) where units are actually python packages. Now how do you push a notification message from a (otherwise totaly unrelated) utility to a browser view without giving up the loosely couppled structure? The answer is - events.


Events in Zope3 are really something you don't want to miss once you find out it's there. They are easy to use, they are easy to customize and they offer elegant solutions to problems you thought you'd be never able to fix without dirty hacks and black magic.



Now, how does my notification system look like? Let's start by having a look at the interfaces.py:

If an object wants to be used as context for notification, it has to implement the INotifiable interface.


from zope.interface import Interface, Attribute
from zope.schema import Text, Choice

from zope.i18nmessageid import MessageFactory
_ = MessageFactory("notifications")

class INotifiable(Interface):
""" an interface for objects which wish to be added
to the notification stack
"""

def notificationId():
""" should return a unique id used to identify
the object in notification stack
"""


class INotificationEvent(Interface):
""" An event which adds a message to a
notification stack
"""

obj = Attribute("The object we've been notified about.")
message = Attribute("The message of the event.")


Our custom event interface with two attributes - the context object we're talking about and the notification message itself. Speaking of messages, here's the interface for a pretty simple one:


SEVERITIES = (
u'error',
u'warning',
u'info',
)

class INotificationMessage(Interface):
""" A message used for notification """

text = Text(title=_(u"Message text"))

severity = Choice(
title = _(u"Severity"),
description = _(u"The message severity."),
default = u'info',
required = True,
values = SEVERITIES,
)

def isError():
""" returns True if severity is 'error' """

def isWarning():
""" returns True if severity is 'warning' """

def isInfo():
""" returns True if severity is 'info' """

We need (obviously) a text field and something which would allow us to
differentiate between message types. Severity seemed to be a good choice.
I added a very basic one - a Choice field and three hardcoded severity
levels (those three isX methods below are just syntactic sugar).


Now if we trigger a notification event, we need a way to store it somewhere
temporarely until it's consumed by an appropriate view, here's an interface
for the utility which should be able to handle this:

class INotificationStack(Interface):
""" a stack that holds notifications """

def add(context, message):
""" adds a message to the stack """

def pop(context):
""" returns last message from stack """

def tell(context):
""" returns list of all messages on stack """

def tellBySeverity(context, severity):
""" returns an iterator for all messages on stack
specified by a certain severity
"""


This is really a very basic structure, it doesn't take message
severity into account when returning them from stack, there's no
way to limit the number of notifications per context, etc., but
it's good enough to serve as a proof-of-concept :)


That's almost it - we still lack a mechanism to retrieve our
notifications from the stack above. Since presenting messages
in a browser view is the main purpose of this system, this
last piece is going to be an adapter:

class INotificationHandler(Interface):

def all():
""" return all notifications on stack for this context """

def warnings():
""" return all warnings for this context """

def infos():
""" return all info messages """

def errors():
""" return all error messages """


Again, the code is as simple as possible, I don't think
it deserves further explanation :)



After having defined the crucial components, we can
review the code implementing them and the ZCML part
which glues it all together:


message.py

from zope.interface import implements
from interfaces import INotificationMessage, SEVERITIES
from zope.app import zapi

class NotificationMessage(object):
""" a simple notification message """

implements(INotificationMessage)

def __init__(self, text, severity='info'):
self.text = text
severity = severity.lower()
assert severity in SEVERITIES
self.severity = severity

def isError(self):
return self.severity == 'error'

def isWarning(self):
return self.severity == 'warning'

def isInfo(self):
return self.severity == 'info'





event.py

from zope.interface import implements
from interfaces import INotificationEvent, INotificationStack
from zope.app import zapi

def notificationsHandler(event):
stack = zapi.getUtility(INotificationStack)
stack.add(event.obj, event.message)

class NotificationEvent(object):

implements(INotificationEvent)

def __init__(self, obj, message):
self.obj = obj
self.message = message




stack.py

from zope.interface import implements
from interfaces import INotificationStack, INotifiable

class NotificationStack(object):

implements(INotificationStack)

def __init__(self):
self._stack = dict()

def add(self, context, message):
if not INotifiable.providedBy(context):
return
nid = context.notificationId()
if self._stack.get(nid, None) is None:
self._stack[nid] = []
self._stack[nid].append(message)

def pop(self, context):
if INotifiable.providedBy(context) and self._stack.get(context.notificationId(), None) is not None:
return self._stack[context.notificationId()].pop()
return None

def tell(self, context):
nid = context.notificationId()
if self._stack.get(nid, None) is None:
return []
ret = self._stack[nid]
del self._stack[nid]
return ret

def tellBySeverity(self, context, severity):
nid = context.notificationId()
if self._stack.get(nid, None) is None:
return []
ret, rest = [], []
for a in self._stack[nid]:
if a.severity == severity:
ret.append(a)
else:
rest.append(a)
self._stack[nid] = rest
return ret





adapter.py

from zope.interface import implements
from zope.app import zapi
from zope.component import adapts
from interfaces import INotifiable, INotificationStack, INotificationHandler
from zope.app.publisher.browser import BrowserView

class NotificationHandler(BrowserView):

implements(INotificationHandler)

adapts(INotifiable)

def __init__(self, context, request):
BrowserView.__init__(self, context, request)
self.nu = zapi.getUtility(INotificationStack)

def all(self):
return self.nu.tell(self.context)

def warnings(self):
return self.nu.tellBySeverity(self.context, 'warning')

def infos(self):
return self.nu.tellBySeverity(self.context, 'info')

def errors(self):
return self.nu.tellBySeverity(self.context, 'error')




The glue: configure.zcml

<configure
xmlns='http://namespaces.zope.org/zope'
xmlns:browser='http://namespaces.zope.org/browser'
i18n_domain="notifications"
xmlns:i18n="http://namespaces.zope.org/i18n"
>

<utility
factory=".stack.NotificationStack"
provides=".interfaces.INotificationStack"
permission="zope.Public"
/>

<content class=".adapter.NotificationHandler">
<require permission="zope.Public"
interface=".interfaces.INotificationHandler" />
</content>

<adapter
provides=".interfaces.INotificationHandler"
factory=".adapter.NotificationHandler"
for=".interfaces.INotifiable"
/>

<view
for=".interfaces.INotifiable"
name="notifications"
factory=".adapter.NotificationHandler"
type="zope.publisher.interfaces.http.IHTTPRequest"
permission="zope.Public"
allowed_interface=".interfaces.INotificationHandler"
/>

<subscriber
handler=".event.notificationsHandler"
for=".interfaces.INotificationEvent"
trusted="y"
/>

</configure>




That's it. Put the code into a python module (e.g. notifications, dont forget to `touch __init__.py` and `mkdir locales`), add an appropriate notifications-include.zcml to you etc/package-includes and you're ready to roll.
Here's an example of how you could use the code:

1) Implement INotifiable in your content class

from zope.interface import implements
from notifications.interfaces import INotifiable
from zope.app import zapi

class MyContent:
implements(INotifiable)

def notificationId(self):
return zapi.getPath(self)



2) "Send" a notification (trigger an INotificationEvent):

# ...
from zope.event import notify
from notifications.event import NotificationEvent
from notifications.message import NotificationMessage
# ...
def doSomethingImportant(obj):
try:
obj.importantFunction()
except SomeEvilException, e:
msg = "Failed to run importantFunction()! Here's what happend: %s " % (str(e),)
notify(NotificationEvent(obj, NotificationMessage(msg, 'error')))
return
notify(NotificationEvent(obj, NotificationMessage('It worked!', 'info')))
# ...


3) And finally display the messages in a view (consume the stack):

...
<ul class="notifications">
<li tal:repeat="message context/@@notifications/all">
<span tal:attributes="class string:notification-${message/severity}"
tal:content="message/text" />
</li>
</ul>
...


Enjoy :)

04 Januar 2006

I'll try to comment on my experience with zope3 by adding short reports about my forthcomings on a simple projects I 'm working on from time to time - fsgallery. What is it? It's a zope3 photo gallery which serves photos directly from filesystem.
The idea behind the project was to create a photo-album system which would allow to leave the originals photos untouched and enrich the served images with metadata (title, description, comments, tags) and EXIF information.