跳转至: 导航, 搜索

Chapter 19: Events and Subscribers (第 19 章:事件和订阅)

原文出处:The Zope 3 Developers Book - An Introduction for Python Programmers

原文作者:StephanRichter, zope.org




适用版本:Zope 3



Contributor (贡献者)


  • You should be comfortable with the topics covered in the “Content Components – The Basics” part.
    你应该掌握 "内容组件 - 基础" 部分的全部内容。
  • Feel comfortable with the Component Architecture.
  • Be familiar with annotations. Read the appropriate chapters in this book, if necessary.


Events are a powerful programming tool and are primary citizens in Zope 3. This chapter will concentrate on the subscription of existing events by implementing a mail subscription system for messages – whenever a message is modified, subscribers receive an E-mail about the change. This will also demonstrate again how annotations can b e added to an ob ject. In the last part of the chapter we will talk theoretically ab out triggering events.


There are two main components that need to b e develop ed. The first is the mail subscription adapter for the message, which manages the subscription E-mails. The second comp onent is the Event Subscriber , which listens for incoming events and starts the mailing process, if appropriate.

19.1 Step I: Mail Subscription Interface(19.1 步骤 I:邮件订阅接口)

We need to have an interface for managing the subscriptions for a particular message, i.e. add, delete and getting E-mail addresses. So add the following interface to the interfaces module:

class IMailSubscriptions(Interface):
"""This interface allows you to retrieve a list of E-mails for
mailings. In our context these are messages."""

def getSubscriptions():
"""Return a list of E-mails."""

def addSubscriptions(emails):
"""Add a bunch of subscriptions; one would be okay too."""

def removeSubscriptions(emails):
"""Remove a set of subscriptions."""

This code is simple enough, so that no further explanation is needed at this point.

19.2 Step II: Implementing the Mail Subscription Adapter(19.2 步骤 II:实现邮件订阅适配器 )

The implementation should be straightforward. The subscriptions are implemented as a simple tuple data structure, which are accessible via the annotation adapter. Note that the implementation makes no assumption about the type of annotation that is going to be used, i.e. we might have used the `AttributeAnnotations` out of pure convenience, but the data could just as well be stored in LDAP without having any effect on the `MailSubscriptions` implementation.
上述接口的实现直接而又简单。订阅可用简单的tuple数据结构实现,并且通过标记(annotation)adapter访问订阅。注意该实现并不对将要用到的标记(annotation)类型作任何假定,比如我们可能纯粹出于方便而用 `AttributeAnnotation` ,但数据同样也可以被保存在LDAP中而不会对邮件订阅的实现有任何影响。

Since there is no need to create a new module, add the following code to the message.py file:
因此,并不需要创建一个新模块,只需把下列代码添加到message.py 文件中:

from zope.app.annotation.interfaces import IAnnotations
from book.messageboard.interfaces import IMailSubscriptions


class MailSubscriptions:
"""Message Mail Subscriptions."""

<u>used_for</u> = IMessage

def <u>init</u>(self, context):
self.context == self.<u>parent</u> == context
self._annotations = IAnnotations(context)
if not self._annotations.get(SubscriberKey):
self._annotations[SubscriberKey] = ()

def getSubscriptions(self):
"See book.messageboard.interfaces.IMailSubscriptions"
return self._annotations[SubscriberKey]

def addSubscriptions(self, emails):
"See book.messageboard.interfaces.IMailSubscriptions"
subscribers = list(self._annotations[SubscriberKey])
for email in emails:
if email not in subscribers:
self._annotations[SubscriberKey] = tuple(subscribers)

def removeSubscriptions(self, emails):
"See book.messageboard.interfaces.IMailSubscriptions"
subscribers = list(self._annotations[SubscriberKey])
for email in emails:
if email in subscribers:
self._annotations[SubscriberKey] = tuple(subscribers)
  • Line 4: This is the fully qualified subscriber annotation key that will uniquely identify this annotation data. Here a URL is used, but dotted names are also common.
    第 4 行:这是完全确定的订阅者annotation 关键字,它将唯一确定这份annotation 数据。这里使用了一个URL,以点分隔的名字也很常见。
  • Line 11: While this declaration is not needed, it clearly signifies that this implementation is an adapter for IMessage objects.
    第 11 行:尽管这句声明不是必需的,但它能清晰的表明该实现是个IMessage对象适配器。
  • Line 14: Since this adapter will use annotations, it will be a trusted adapter, meaning that it will be a proxied object. All proxied objects must provide a location (at least through a `parent` attribute) so that permission declarations can be found. Otherwise only global permission settings would be available.
    第 14 行:既然该适配器要使用nnotation,那么它就将是一个可信任适配器,也就意味着它将是一个被代理的对象。所有被代理的对象必须提供一个位置(至少也要通过一个`parent`属性)以便能找到权限声明。否则只能使用全局权限设置。
  • Line 15: Here we are getting the Annotations adapter that will provide us with a mapping object in which we will store the annotations. Note that this statement says nothing about the type of annotation we are about to get.
    第 15 行:在这里我们得到了一个 Annotations 适配器,以便提供给我们一个在哪保存 annotations 的一个映射对象。注意该语句并未提及我们将使用什么类型的 annotation 。
  • Line 16-17: Make sure an entry for our subscriber key exists. If not, create an empty one.
    第 16–17 行:确保我们的订户关键字存在。如果没有的话,创建一个空的。
  • Line 19-37: There is nothing interesting going on here. The only fact worth mentioning is the use of tuples instead of lists, which make the code a bit more complex, but tuples are not mutable, so that they are automatically saved in the ZODB, if we have `AttributeAnnotations`.
    第 19-37 行:这里没什么可讲的。唯一值得提及的就是用元组代替了列表,使代码复杂了点,但元组是不可变的,因此如果我们有 `AttributeAnnotation` 的话,它们是会自动存到ZODB中的。

This is pretty much everything that is to the subscription part of this step. We can now register the new component via ZCML using the adapter directive:

1  <adapter
2      factory=".message.MailSubscriptions"
3      provides=".interfaces.IMailSubscriptions"
4      for=".interfaces.IMessage"
5      permission="book.messageboard.Add"
6      trusted="true" />
  • Line 2-4: Like for the ISized adapter, we specify the necessary adapter registration information.
    第 2–4 行:象 ISized 适配器一样,我们也指定必要的适配器注册信息。
  • Line 6: If an adapter is declared trusted then its context (the object being passed into the adapter constructor) will not be security proxied. This is necessary so that the annotations adapter can use the `annotations` attribute to store the annotations. If the adapter is not trusted and the context is security proxied, then a `ForbiddenAttribute` error will be raised whenever we try to access the annotations.
    第 6 行:如果一个适配器声明是受信任的,那么它的 context (被放入适配器构造器中的对象)将不是安全代理。这是必需的,因此 annotation 适配器可以使用 `annotations` 属性来保存 annotation 。如果适配器是不受信任的且 context 是安全代理,那么我们如果要访问 annotation ,那么就会得到一个 `ForbiddenAttribute` 错误。
  • Line 5: Once an adapter is trusted, the adapter itself is security proxied. Therefore we need to define a permission that is required to use the adapter.
    第 5 行:一旦一个适配器受信任,那么该适配器它自己也被安全地代理。因此我们需要定义一个权限以便使用该适配器。

19.3 Step III: Test the Adapter(19.3 步骤 III: 测试适配器 )

The tests are as straightforward as the implementation. In the doc string of the `MailSubscription`s class add the following documented testing code.
该测试如同实现一样简单。在 `MailSubscriptions` 类的文档字符串中添加下列文档测试代码。

1  Verify the interface implementation
3  >>> from zope.interface.verify import verifyClass
4  >>> verifyClass(IMailSubscriptions, MailSubscriptions)
5  True
7  Create a subscription instance of a message
9  >>> msg = Message()
10  >>> sub = MailSubscriptions(msg)
12  Verify that we have initially no subscriptions and then add some.
14  >>> sub.getSubscriptions()
15  ()
16  >>> sub.addSubscriptions(('[email protected]',))
17  >>> sub.getSubscriptions()
18  ('[email protected]',)
19  >>> sub.addSubscriptions(('[email protected]',))
20  >>> sub.getSubscriptions()
21  ('[email protected]', '[email protected]')
22  >>> sub.addSubscriptions(('[email protected]',))
23  >>> sub.getSubscriptions()
24  ('[email protected]', '[email protected]', '[email protected]')
26  Now let's also check that we can remove entries.
28  >>> sub.removeSubscriptions(('[email protected]',))
29  >>> sub.getSubscriptions()
30  ('[email protected]', '[email protected]')
32  When we construct a new mail subscription adapter instance, the values
33  should still be there.
35  >>> sub1 = MailSubscriptions(msg)
36  >>> sub1.getSubscriptions()
37  ('[email protected]', '[email protected]')
  • Line 3-5: Do a very detailed analysis to ensure that the `MailSubscriptions` class implements the `IMailSubscriptions interface`.
    第 3–5 行: 对 `MailSubscriptions` 类进行非常细致的分析,以确保 `IMailSubscription`s 接口的实现。
  • Line 7-10: In doc tests it helps very much if you emphasize how you setup your test case. Here we make that very explicit by creating a separate section and add some explanation to it.
    第 7–10 行:在文档测试中,强调如何设定你的测试事例是非常有帮助的。这里我们十分明确地创建了单独的一节并对其增加一些说明。
  • Line 12-24: Check that we can retrieve the list of subscribers and add new ones as well.
    第 12–24 行:检查我们能检索到的订户列表并添加一个新的订户。
  • Line 26-30: Make sure deleting subscriptions works as well.
    第 26-30 行:确认订户删除也运行正常。
  • Line 32-37: When we create a new adapter using the same message, the subscriptions should still be available. This ensures that the data is not lost when the adapter is destroyed. An even stronger test would be that the persistence also works.
    第 32–37 行:当我们使用同样消息创建一个新的适配器时,订阅也一直可用。这样就可以确保在适配器被销毁时数据不会丢失。一个更有效的测试就是一直运行下去。

Note that there is no check for the case the annotation is not there. This is due to the fact that the `MailSubscriptions` constructor should make sure the annotation is available, even though this means to simply create an empty storage, so we have definitely covered this case in the implementation.
注意这里并没有检查 annotation 事例是否存在。这源于这样一个事实,`MailSubscriptions`构造器确保 annotation 是可用, 即使这只意味着简单地创建一个空的存储。因此我们可以确定整个事例在这个实现中。

Since the adapter uses annotations, it requires some setup of the component architecture to run the tests. We already bring the services up for the tests, but now we also have to register an adapter to provide the annotations. Therefore we have to write a custom setUp() method and use it. The testing code in tests/test_message.py changes to:
因为适配器使用 annotations,因此在测试时它需要设置组件结构来运行该测试。我们已经为测试提供了服务,但现在我们需要注册一个适配器用来提供 annotations。因此,我们要编写一个自定义的setUp()方法并使用它。将message.py文件里的 tests/test 中的测试代码改成:

from zope.interface import classImplements

from zope.app.annotation.attribute import AttributeAnnotations
from zope.app.interfaces.annotation import IAnnotations
from zope.app.interfaces.annotation import IAttributeAnnotatable
from zope.app.tests import placelesssetup
from zope.app.tests import ztapi

def setUp(test):
classImplements(Message, IAttributeAnnotatable)
ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,

def test_suite():
return unittest.TestSuite((
setUp=setUp, tearDown=placelesssetup.tearDown),
  • Line 7: The ztapi module contains some very useful convenience functions to set up the component architecture for a test, such as view and adapter registration.
    第 7 行:ztapi 模块包含了一些对测试来说非常方便且有用的功能用来设置测试的组件结构,如视图和适配器注册。
  • Line 9: Note that the setUp() expects a test argument which is an instance of `DocTest`. You can use this object to provide global test variables.
    第 9 行:注意 setUp() 需要一个`DocTest`实体的测试参数。你可以用该对象来提供全局的测试变量。
  • Line 11: We usually use ZCML to declare that Message implements `IAttributeAnnotatable`. Since ZCML is not executed for unit tests, we have to do it manually here.
    第 11 行:我们通常用ZCML来声明消息实现 `IAttributeAnnotatable`。因为ZCML不会被单元测试执行,所以我们需要在这里手动运行它。
  • Line 12-13: Setup the adapter that allows us to look up an annotations adapter for any object claiming it is `IAttributeAnnotatable`.
    第 12-13 行:将适配器设置成允许我们可以查找在任何对象中声明它是 `IAttributeAnnotatable` 的 annotations 适配器。

You should now run the tests and ensure they pass.

19.4 Step IV: Providing a View for the Mail Subscription(19.4 步骤 IV:为邮件订阅提供一个视图)

The last piece we have to provide is a view to manage the subscriptions via the Web. The page template ( subscriptions.pt) could look like this:
最后一步我们要提供一个视图以便可以通过Web来管理订阅。页面模板 ( subscriptions.pt) 看上去就象这样:

1  <html metal:use-macro="views/standard_macros/view">
2    <body>
3      <div metal:fill-slot="body" i18n:domain="messageboard">
5        <form action="changeSubscriptions.html" method="post">
7          <div class="row">
8              <div class="label"
9                  i18n:translate="">Current Subscriptions</div>
10              <div class="field">
11             <div tal:repeat="email view/subscriptions">
12                  <input type="checkbox" name="remails:list"
13                         value="" tal:attributes="value email">
14                  <div tal:replace="email">[email protected]</div>
15                </div>
16                <input type="submit" name="REMOVE" value="Remove"
17                     i18n:attributes="value remove-button">
18              </div>
19          </div>
21          <div class="row">
22              <div class="label" i18n:translate="">
23                Enter new Users (separate by 'Return')
24              </div>
25              <div class="field">
26             <textarea name="emails" cols="40" rows="10"></textarea>
27              </div>
28          </div>
30               <div class="row">
31                 <div class="controls">
32                   <input type="submit" value="Refresh"
33                  i18n:attributes="value refresh-button" />
34                   <input type="submit" name="ADD" value="Add"
35                       i18n:attributes="value add-button" />
36                 </div>
37               </div>
39        </form>
41      </div>
42    </body>
43  </html>
  • Line 7-19: The first part lists the existing subscriptions and let’s you select them for removal.
    第 7–19 行:第一部分列出已有订阅,同时让我们可以选择它们以便删除。
  • Line 20-38: The second part provides a textarea for adding new subscriptions. Each E-mail address should be separated by a newline (one E-mail per line).
    第 20–38 行:第二部分提供一个文本框用来添加新订阅。每个E-mail地址将被一个新行分隔开(每行一个E-mail)。

The supporting View Python class then simply needs to provide a subscriptions() method (see line 11 above) and a form action. Place the following code into browser/message.py:
然后视图 Python 类只需简单提供一个 subscriptions() 方法的支持(参见上面第 11 行)和一个表单操作。将下列代码放入browser/message.py文件中:

from book.messageboard.interfaces import IMailSubscriptions

class MailSubscriptions:

def subscriptions(self):
return IMailSubscriptions(self.context).getSubscriptions()

def change(self):
if 'ADD' in self.request:
emails = self.request['emails'].split('\n')
elif 'REMOVE' in self.request:
emails = self.request['remails']
if isinstance(emails, (str, unicode)):
emails = [emails]

  • Line 9 & 12: We simply use the name of the submit button to decide which action the user ntended.
    第 9 & 12 行:我们简单地使用提交按钮来确定用户想做的操作。

The rest of the code should be pretty forward. The view can be registered as follows:
The rest of the code should be pretty forward. 该视图可以象下面那样被注册:

1  <pages
2      for="book.messageboard.interfaces.IMessage"
3      class=".message.MailSubscriptions"
4      permission="book.messageboard.Edit"
5      >
6    <page
7        name="subscriptions.html"
8        template="subscriptions.pt"
9        menu="zmi_views" title="Subscriptions"
10        />
11    <page
12       name="changeSubscriptions.html"
13       attribute="change"
14       />
15  </pages>
  • Line 1: The browser:pages directive allows us to register several pages for an interface using the same view class and permission at once. This is particularly useful for views that provide a lot of functionality.
    第 1 行:浏览器:页面指令允许我们可以马上为使用相同视图类和权限的接口注册相应的页面。这对于视图来说是特别有用的,它可以提供很多的功能。
  • Line 6-10: This page uses a template for creating the HTML.
    第 6-10:该页利用一个模板来创建HTML
  • Line 11-14: This view on the other hand, uses an attribute of the view class. Usually methods on the view class do not return HTML but redirect the browser to another page.
    第 11-14: 另外该视图使用了视类的属性。通常视图类中的方法不会返回HTML,但可以将浏览器重定向到另一个页。
  • Line 9: Make sure the Subscriptions view becomes a tab for the Message object.
    第 9 行:确保订阅视图已经成为一个消息对象标签。

It is amazing how compact the browser:pages and browser:page directives make the registration. In the early development stages we did not have this directive and everything had to be registered via browser:view, which required a lot of repetitive boilerplate in the ZCML and Python code.
如此紧凑地使用 browser:pages 和 browser:page 指令来进行注册是令人惊奇的。在早期的开发阶段我们没有这个指令,什么都必须通过 browser:view 来注册,它在 ZCML 和 Python 代码中大量的可重用模板是必需的。

19.5 Step V: Message Mailer - Writing an Event Subscriber(19.5 步骤 V: 消息收发器 - 编写事件订阅)

Until now we have not even said one word about events. But this is about to change, since the next task is to implement the subscriber object. The generic event system is very simple: It consists of a list of subscribers and a notify() function. Subscribers can be subscribed to the event system by appending them to the list. To unsubscribe an object it must be removed from the list. Subscribers do not have to be any special type of objects; they merely have to be callable. The notify() function takes an object (the event) as a parameter; it then iterates though the list and calls each subscriber passing through the event.
直到现在我们也还没有说到事件。不过现在不同了,因为下一个任务就是要实现订阅对象。这个通用的事件系统是非常简单的:它包括着一个订户列表和一个 notify() 函数。订户可以被事件系统将其添加到列表中以完成订阅。而取消订阅则需将该对象从列表中移除即可。订户不必是特定类型的对象,它们只是能被调用就行。 notify() 函数将对象(事件)作为参数;然后通过该事件来反复使用列表调用每个订户。

This means that we have to implement a `call`() method as part of our message mailer API in order to make it a subscriber. The entire `MessageMailer` class should look like this (put it in the message module):
这就意味着我们必须实现 `call`() 方法,并将其做为我们消息收发器 API 的一部分,以便将其做为一个订户。完整的 `MessageMailer` 类如下所示(将下面这段放到 message 模块中):

from zope.app import zapi
from zope.app.container.interfaces import IObjectAddedEvent
from zope.app.container.interfaces import IObjectRemovedEvent
from zope.app.event.interfaces import IObjectModifiedEvent
from zope.app.mail.interfaces import IMailDelivery

class MessageMailer:
"""Class to handle all outgoing mail."""

def <u>call</u>(self, event):
"""Called by the event system."""
if IMessage.providedBy(event.object):
if IObjectAddedEvent.providedBy(event):
elif IObjectModifiedEvent.providedBy(event):
elif IObjectRemovedEvent.providedBy(event):

def handleAdded(self, object):
subject = 'Added: '+zapi.getName(object)
emails = self.getAllSubscribers(object)
body = object.body
self.mail(emails, subject, body)

def handleModified(self, object):
subject = 'Modified: '+zapi.getName(object)
emails = self.getAllSubscribers(object)
body = object.body
self.mail(emails, subject, body)

def handleRemoved(self, object):
subject = 'Removed: '+zapi.getName(object)
emails = self.getAllSubscribers(object)
body = subject
self.mail(emails, subject, body)

def getAllSubscribers(self, object):
"""Retrieves all email subscribers."""
emails = ()
msg = object
while IMessage.providedBy(msg):
emails += tuple(IMailSubscriptions(msg).getSubscriptions())
msg = zapi.getParent(msg)
return emails

def mail(self, toaddrs, subject, body):
"""Mail out the Message Board change message."""
if not toaddrs:
msg = 'Subject: %s\n\n\n%s' %(subject, body)
mail_utility = zapi.getUtility(IMailDelivery, 'msgboard-delivery')
mail_utility.send('[email protected]' , toaddrs, msg)

mailer = MessageMailer()
  • Line 2-4: We want our subscriber to handle add, edit and delete events. We import the interfaces of these events, so that we can differentiate among them.
    第 2-4 行: 我们想我们的订户可以手工添加、编辑和删除事件。我们导入这些事件的接口,以便区分这些操作。
  • Line 10-18: This is the heart of the subscriber and this chapter. When an event occurs the `call`() method is called. First we need to check whether the event was caused by a change of an IMessage object; if so, let’s check which event was triggered. Based on the event that occurred, a corresponding handler method is called.
    第 10-18 行: 这是订户和本章的核心部分。当事件引起 `call`() 方法被调用时,首先我们需要检查是否是 IMessage 对象的改变引发了事件,如果是的话,我们检查触发了什么事件。并根据该事件调用相应的处理方法。
  • Line 20-36: These are the three handler methods that handle the various events. Note that the modified event handler should really generate a nice diff, instead of sending the entire message again.
    第 20-36 行: 这里有三个处理方法用以处理不同的事件。注意被修改后的事件处理真正可以做到 Diff,而不是将整个消息再发一次。
  • Line 38-45: This method retrieves all the subscriptions of the current message and all its ancestors. This way someone who subscribed to message `HelloEveryone` will also get e-mailed about all responses to `HelloEveryone`.
    第 38-45 行: 该方法得到当前消息及其父消息的所有订阅。通过这种方式订阅了消息 `HelloEveryone` 的用户就可以收到所有对 `HelloEveryone` 消息回复的 e-mail 了。
  • Line 47-53: This method is a quick introduction to the Mail Delivery utility. Note how simple the send() method of the Mail Delivery utility is; it is the same API as for smtplib. The policy and configuration on how the mail is sent is fully configured via ZCML. See the configuration part later in this chapter.
    第 47-53 行: 该方法是对 Mail Delivery utility 的快速说明。注意 Mail Delivery utility 的 send() 方法是多么的简单;它有着同 smtplib 相同的 API。关于如何通过ZCML发送完整配置的策略和配置,可参见本章后面的配置部分。
  • Line 60: We can only subscribe callable objects to the event system, so we need to instantiate the `MessageMailer` component.
    第 60(55?)行: 我们可以仅仅订阅可调用对象到事件系统,因此我们需要实例化 `MessageMailer` 组件。

Lastly, we need to register the message mailer component to the event service and setup the mail utility correctly. Go to your configuration file and register the following two namespaces in the configure element:

1  xmlns:mail="http://namespaces.zope.org/mail"

Next we setup the mail utility:然后我们设置邮件程序

1  <mail:smtpMailer name="msgboard-smtp" hostname="localhost" port="25" />
3  <mail:queuedDelivery
4      name="msgboard-delivery"
5      permission="zope.SendMail"
6      queuePath="./mail-queue"
7      mailer="msgboard-smtp" />
  • Line 1: Here we decided to send the mail via an SMTP server from localhost on the standard port 25. We could also have chosen to send the mail via the command line tool sendmail.
    第 1 行: 这里我们决定通过本机 25 端口的 SMTP 服务来发送邮件。我们也可以通过命令行工具 sendmail 来发送邮件。
  • Line 3-7: The Queued Mail Delivery utility does not send mails out directly but schedules them to be sent out independent of the current transaction. This has huge advantages, since the request does not have to wait until the mails are sent. However, this version of the Mail Utility requires a directory to store E-mail messages until they are sent. Here we specify the mail-queue directory inside the message board package. The value of the attribute name is used by the `MessageMailer` to retrieve the Queued Mail Delivery utility. Another Mail utility is the Direct Mail Delivery utility, which blocks the request until the mails are sent.
    第 3-7 行: Queued Mail Delivery utility 不会直接将邮件发出去,而是将它们以独立于当前事务的方式发送出去。这样做带来极大的好处,就是请求不必再等待直到邮件被发好。不过这样的 Mail Utility 需要一个目录用来保存 E-mail信息直到它们被发送出去。在这里我们在消息栏包中指定了 mail-queue 目录。属性名的值被 `MessageMailer` 用来获得 Queued Mail Delivery utility。而另一个 Mail utility 是 Direct Mail Delivery utility,它在邮件被发送之前会阻止请求。

Now we register our message mailer object for the events we want to observe:

1  <subscriber
2      factory=".message.mailer"
3      for="zope.app.event.interfaces.IObjectModifiedEvent" />
5  <subscriber
6      factory=".message.mailer"
7      for="zope.app.container.interfaces.IObjectAddedEvent" />
9  <subscriber
10      factory=".message.mailer"
11      for="zope.app.container.interfaces.IObjectRemovedEvent" />

The subscriber directive adds a new subscriber (specified via the factory attribute) to the subscriber list. The for attribute specifies the interface the event must implement for this subscriber to be called. You might be wondering at this point why such strange attribute names were chosen. In the Zope application server, subscriptions are realized via adapters. So internally, we registered an adapter from `IObjectModifiedEvent` to None, for example.
订户直接添加新订户到订户列表中(通过 factory 属性来指定)。for 属性指定接口以调用必须为该订户实现的事件。你也许会在这点上觉得惊奇,为什么选择这么奇怪的属性名。在 Zope 应用程序服务中,订阅是通过适配器来实现的。因此从内部来讲,我们是从 `IObjectModifiedEvent` 到 None 来注册一个适配器的。

Now you might think: “Oh let’s try the new code!”, but you should be careful. We should write some unit tests before testing the code for real.

19.6 Step VI: Testing the Message Mailer(19.6 步骤 VI: 测试消息收发器)

So far we have not written any complicated tests in the previous chapters of the “Content Components - The Basics” part. This changes now. First of all, we have to bring up quite a bit more of the framework to do the tests. The test_message.py module’s setUp() function needs to register the location adapters and the message mail subscription adapter. So it should look like that:
到目前为止我们在先前“内容组件 - 基础”部分的章节中并没有写什么复杂的测试。现在不同了。首先我们提出相当多的框架来做测试。test_message.py 模块的 setUp() 函数需要注册位置适配器和消息邮件订阅适配器。如下所示:

from zope.app.location.traversing import LocationPhysicallyLocatable
from zope.app.location.interfaces import ILocation
from zope.app.traversing.interfaces import IPhysicallyLocatable

from book.messageboard.interfaces import IMailSubscriptions
from book.messageboard.interfaces import IMessage
from book.messageboard.message import MailSubscriptions

def setUp():
ztapi.provideAdapter(ILocation, IPhysicallyLocatable,
ztapi.provideAdapter(IMessage, IMailSubscriptions, MailSubscriptions)
  • Line 1-3 & 11-12: This adapter allows us to use the API to access parents of objects or even the entire object path.
    第 1-3 & 11-12 行: 该适配器允许我们使用 API 来访问父对象甚至是整个对象路径
  • Line 5-7 & 13: We simply register the mail subscription adapter that we just developed, so that the mailer can find the subscribers in the messages.
    第 5-7 & 13 行: 我们简单地注册我们刚刚开发的邮件订阅适配器,因此邮件收发器可以在消息中找到订户。
  • Line 10: The three dots stand for the existing content of the function.
    第 10 行: 三个点表示在函数中已经存在的内容

Now all the preparations are made and we can start writing the doctests. Let’s look at the getAllSubscribers() method tests. We basically want to produce a message and add a reply to it. Both messages will have a subscriber. When the getAllSubscribers() method is called using the reply message, the subscribers for the original message and the reply should be returned. Here is the test code, which you should simply place in the getAllSubscribers() docstring:
现在所有的准备工作已经就绪,我们可以开始编写 doctest。让我们看看测试的 getAllSubscribers() 方法。我们的基本思路是想生成一个消息并且回复它。这两个消息将有一个订户。当在回复消息调用 getAllSubscribers() 时,订阅了原始消息的订户也能得到回复的消息。下面是测试代码,你只须简单地将其放在 getAllSubscribers() 的 docnstring 中即可:

1  Here a small demonstration of retrieving all subscribers.
3  >>> from zope.interface import directlyProvides
4  >>> from zope.app.traversing.interfaces import IContainmentRoot
6  Create a parent message as it would be located in the message
7  board. Also add a subscriber to the message.
9  >>> msg1 = Message()
10  >>> directlyProvides(msg1, IContainmentRoot)
11  >>> msg1.<u>name</u> = 'msg1'
12  >>> msg1.<u>parent</u> = None
13  >>> msg1_sub = MailSubscriptions(msg1)
14  >>> msg1_sub.context.<u>annotations</u>[SubscriberKey] = ('[email protected]',)
16  Create a reply to the first message and also give it a subscriber.
18  >>> msg2 = Message()
19  >>> msg2_sub = MailSubscriptions(msg2)
20  >>> msg2_sub.context.<u>annotations</u>[SubscriberKey] = ('[email protected]',)
21  >>> msg1['msg2'] = msg2
23  When asking for all subscriptions of message 2, we should get the
24  subscriber from message 1 as well.
26  >>> mailer.getAllSubscribers(msg2)
27  ('[email protected]', '[email protected]')
  • Line 3-4: Import some of the general functions and interfaces we are going to use for the test.
    第 3-4 行: 导入一些我们在测试中常用的函数和接口。
  • Line 6-14: Here the first message is created. Note how the message must be a `IContainmentRoot` (line 10). This signalizes the traversal lookup to stop looking any further once this message is found. Using the mail subscription adapter (line 13-14), we now register a subscriber for the message.
    第 6-14 行: 在这里第一个消息被创建。注意消息必须是`IContainmentRoot`(第 10 行)。This signalizes the traversal lookup to stop looking any further once this message is found. Using the mail subscription adapter (line 13-14), we now register a subscriber for the message.
  • Line 16-21: Here we create the reply to the first message. The parent and name of the second message will be automatically added during the `setitem` call.
  • Line 23-27: The mailer should now be able to retrieve both subscriptions. If the test passes, it does.
    行 16-21 行: 在这里我们创建对第一个消息的回复。父消息和第二个消息的名称将在调用 `setitem` 时被自动添加。

Finally we test the `call`() method directly, which is the heart of this object and the only public method. For the notification to work properly, we have to create and register an `IMailDelivery` utility with the name “msgboard-delivery”. Since we do not want to actually send out mail during a test, it is wise to write a stub implementation of the utility. Therefore, start your doctests for the notify() method by adding the following mail delivery implementation to the docstring of the method:
最后我们直接测试 `call`() 方法,它是该对象的核心也是唯一的一个全局方法。为了让通知能正常工作,我们必须创建和注册一个名为“msgboard-delivery”的 `IMailDelivery` 实用程序 。因为我们并不想在测试时真的将邮件发送出去。因此,添加下列 mail delivery 的实现到 notify() 方法的 docstring 中,以开始你的 doctests。

1  >>> mail_result = []
3  >>> from zope.interface import implements
4  >>> from zope.app.mail.interfaces import IMailDelivery
6  >>> class MailDeliveryStub(object):
7  ...     implements(IMailDelivery)
8  ...
9  ...     def send(self, fromaddr, toaddrs, message):
10  ...         mail_result.append((fromaddr, toaddrs, message))
12  >>> from zope.app.tests import ztapi
13  >>> ztapi.provideUtility(IMailDelivery, MailDeliveryStub(),
14  ...                      name='msgboard-delivery')
  • Line 1: The mail requests are stored in this global variable, so that we can make test assertions about the supposedly sent mail.
    第 1 行: 邮件请求被保存在该全局变量中,因此我们可以做假想的邮件发送测试。
  • Line 6-10: Luckily the Mail utility requires only the send() method to be implemented and there we simply store the data.
    第 6-10 行: 幸运的是 Mail utility 只要求实现 send() 方法并且我们可以简单地在那里保存数据。
  • 12-14: Using the ztapi API, we can quickly register the utility. Be careful that you get the name right, otherwise the test will not work.
    第 12-14 行: 使用 ztapi API,我们可以快速注册 utility。注意得到正确的名字,否则测试不能正常运行

So far so good. Like for the previous test, we now have to create a message and add a subscriber.

1  Create a message.
3  >>> from zope.interface import directlyProvides
4  >>> from zope.app.traversing.interfaces import IContainmentRoot
6  >>> msg = Message()
7  >>> directlyProvides(msg, IContainmentRoot)
8  >>> msg.<u>name</u> = 'msg'
9  >>> msg.<u>parent</u> = None
10  >>> msg.title = 'Hello'
11  >>> msg.body = 'Hello World!'
13  Add a subscription to message.
15  >>> msg_sub = MailSubscriptions(msg)
16  >>> msg_sub.context.<u>annotations</u>[SubscriberKey] = ('[email protected]',)

This is equivalent to what we did before, so nothing new here. Finally, we create an modification event using the message and send it to the notify() method. We then problem the global mail_result variable for the correct functioning of the method.
这同我们以前所做的一样,这里没什么新的东西。最终,我们创建了一个修改事件,使用消息并将其发送到 notify() 方法。We then problem the global mail_result variable for the correct functioning of the method.

1  Now, create an event and send it to the message mailer object.
3  >>> from zope.app.event.objectevent import ObjectModifiedEvent
4  >>> event = ObjectModifiedEvent(msg)
5  >>> mailer(event)
7  >>> from pprint import pprint
8  >>> pprint(mail_result)
9  [('[email protected]',
10    ('[email protected]',),
11    'Subject: Modified: msg\n\n\nHello World!')]
  • Line 3-4: In this particular test, we use the object modification event. Any `IObjectEvent` can be initiated by passing the affected object as argument to the constructor of the event.
    第 3-4 行: 在这个特殊的测试中,我们使用对象修改事件。任何 `IObjectEvent` 都可以通过将被影响的对象作为事件构造函数的参数来被初始化。
  • Line 5: Here we notify the mailer that an object has been modified. Note that the mailer is an instance of the `MessageMailer` class and is initialized at the end of the module.
    第 5 行: 在这里我们通知邮件收发器有对象被修改。注意邮件收放器是 `MessageMailer` 类的一个实例并在模块后面被初始化。
  • Line 7-11: The pretty print ( pprint) module comes in very handy when outputting complex data structures.
    第 7-11 行: 美化打印(pprint)模块在输出复杂数据结构时非常方便。

We are finally done now. You should run the tests to verify your implementation and then head over to the next section to see how we can give this code a real swirl.
现在我们完成了。你可以运行测试来验证你的实现,然后到下一节看看how we can give this code a real swirl.

19.7 Step VII: Using the new Mail Subscription(19.7 步骤 VII: 使用新的邮件订阅 )

First of all, we have to restart Zope and make sure in boots up properly. Then you can go to the management interface and view a particular message. You might notice now that you have a new tab called Subscriptions, so click on it.
首先,我们必须重启 Zope 并确保得以正确启动,然后你可以进入管理界面,会看到一个特定的消息。你可能注意到现在已出现一个名为“Subscriptions”的新标签,点击它。

In the Subscriptions view, you will see a text area in which you can enter subscription E-mail addresses, which will receive E-mails when the message or any children are changed. When adding a test E-mail address, make sure this E-mail address exists and is your own, so you can verify its arrival. Click on the Add submit button to add the E-mail to the subscriber list. Once the screen returns, you will see this E-mail appear under “Current Subscriptions” with a checkbox before it, so you can delete it later, if you wish.
在Subscriptions视图里,你会看到一个文本框供你输入订阅的 E-mail 地址,当该消息或其回复发生改动时,该邮件地址就能收到E-mail。在添加一个测试用的 E-mail 地址时,确保该 E-mail 地址存在并且是你自己的,这样你才能确认收到了邮件。点击 Add 提交按钮把E-mail地址添加到订户列表。当屏幕返回时,你就会看到该E-mail地址出现在 “Current Subscriptions”下,同时在它前面还有一个checkbox,以便日后需要时你可以将其删除。

Next, switch to the Edit view and modify the `MessageBody` a bit and submit the change. You should notice that the screen returns almost immediately, but that your mail has not necessarily arrived yet. This is thanks to the Queued Mail Delivery Utility, which sends the mails on a separate thread. However, depending on the speed of your E-mail server, a few moments later you should receive an appropriate E-mail.
然后,切换到Edit(编辑)视图对消息内容稍加修改,然后提交这一改动。你会注意到屏幕几乎立即返回,但邮件未必立即到达。这要归功于 队列邮件发送工具(Queued Mail Delivery Utility),它会以一个独立的线程来发送邮件。不过,视你的邮件服务器的速度而异,过一会儿后你就应该收到相应的E-mail。

19.8 Step VIII: The Theory(19.8 步骤VIII: 原理 )

While this chapter demonstrates a classical use of events from an application developer point of view, it is not quite the whole story. So far we have discovered the use of the basic event system.

We did not explain how Zope uses this system. As mentioned before, the subscriber directive does not append the message mailer instance to the subscription list directly, as one may expect. Instead, it registers the message mailer as a “subscription adapter” that adapts the an event providing some event interface, i.e. `IObjectModifiedEvent`, to None, since it explicitly does not provide any special interface. The difference between regular and subscription adapters is that one can register several subscription adapters having the same required and provided provided interfaces. When requested, all matching adapters are returned. This allows us to have multiple subscribers for an event.
我们并没有说明Zope 是如何使用该系统的。按照前面所说的,订户指令并非如你所想,把消息邮件实例直接添加到订阅列表里。相反,它将消息邮件收发器注册成一个“订阅适配器”以适配一个提供事件接口的事件,如`IObjectModifiedEvent` ,to None,因为它明确并不提供任何特殊接口。规则和订阅适配器之间的区别在于一个可以注册几个有着相同要求并被提供接口的订阅适配器。当被请求时,将返回所有匹配的适配器。这允许我们对一个事件有多个订阅。

The Zope application server adds a special dispatch subscriber ( zope.app.event.dispatching) that forwards the notification to all adapter-based subscriptions. In the following diagram you can see how an event flows through the various parts of the system to the subscriber that will make use of the event. The example is based on the code developed in this chapter.
Zope 应用服务添加了一个特殊的急件订户(zope.app.event.dispatching)用以发送给所有基于适配器订阅的通知。在下图中你可以看到一个事件是如何流经系统的不同部分到达使用该事件的订户的。该示例基于本章的开发代码。

PIC Figure 19.1: Demonstration of an event being distributed to its subscribers.

A special kind of subscribers are event channels, which change an event and redistribute it or serve as event filters. You could think of them as middle men. We could have written this chapter using event channels by implementing a subscriber that forwards an event only, if the object provides IMessage. An implementation could look as follows:
一类特殊的订户是事件通道,它可以改变和重新分发事件或用于事件过滤。你可以把它们看成中间人。如果对象提供 IMessage,通过实现一个订户只发送一个事件,我们可以用事件通道来写本章。实现如下所示:

def filterEvents(event):
if IMessage.providedBy(event.object):
zope.event.notify(event.object, event)

The actual mailer would then be a multi-adapter that adapts from both the message and the event:

class MessageMailer:

<u>call</u>(self, message, event):

Multi-subscriptions-adapters are registered in ZCML as follows:

1  <subscriber
2    factory = ".message.mailer"
3    for = ".interface.IMessage
4           zope.app.event.interface.IObjectEvent" />

The modified sequence diagram can be seen below in figure 19.8.
被修改的顺序图如图 19.8 所示。

PIC Figure 19.2: Modification of the even publication using an event channel.

A final tip: Sometimes events are hard to debug. In these cases it is extremely helpful to write a small subscriber that somehow logs all events. In the simplest case this can be a one-line function that prints the string representation of the event in console. To subscribe a subscriber to all events, simply specify for="*" in the zope:subscriber directive.
最后的提示:有时事件是很难调试的。在这些例子里编写一个小的订户以记录全部事件将是非常有用的。最简单的,它可以是在控制台中打印出事件说明文字的一行函数。为了给一个订户订阅全部事件,在 Zope: subscriber directive 中简单的指定 for="*" 即可。


  • Finish the outlined implementation of the event channel above and rewrite the message mailer to be a multi-adapter.
  • Implement a subscriber that subscribes to all events and prints a line to the console for received event. Then extend this subscriber to use the common logging mechanism.