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:
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 :)
