跳转至: 导航, 搜索

Chapter 14:Adding Views(第 14 章:添加视图)

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

原文作者:StephanRichter, zope.org



校对人员:Leal, FireHare

适用版本:Zope 3





  • Knowledge gained in the “Writing a new Content Object” chapter.
  • Some understanding of the presentation components. Optional.


Now that we have two fully-functional content objects, we have to make the functionality available to the user, since there are currently only three very simple views: add, edit and contents. In this chapter we will create a nice message details screen as well as a threaded sub-branch view for both messages and the message board.
既然我们已经有了两个功能完整的内容对象, 接下来要做的就是尽可能让这些功能对用户可用。不过,现在这儿只有三个非常简单的视图:增加(add),编辑(edit)和内容(contents)。这章我们将为消息和留言簿创建一个非常不错的详细留言界面和子线程视图。


This chapter revolves completely around browser-based view components for the `MessageBoard` and Message classes. Views, which will be mainly discussed here, are secondary adapters. They adapt IRequest and some context object to some output interface (often just zope.interface.Interface).
本章的内容将围绕browser-based视图组件展开,视图将是我们在这儿主要讨论的知识,其次就是适配器。They adapt IRequest and some context object to some output interface (often just zope.interface.Interface)

There are several ways to write a view. Some of the dominant ones include:

  • We already learned about using the browser:addform, browser: editform and browser:containerViews directive. These directives are high-level directives and hide a lot of the details about creating and registering appropriate view components.
    我们已经学习了browser:addform、browser: editform和browser:containerViews 指令的使用。

    Forms can be easily configured via ZCML, as you have done in the previous chapter. Forms are incredibly flexible and allow you any degree of customization.
  • There is a browser:page and a browser:pages directive, which are the most common directives for creating browser views and groups of views easily. We will use these two directives for our new views.
  • The zope:view directive is very low-level and provides functionality for registering multi-views, which the other directives are not capable of doing. However, for the average application developer the need to use this directive might never arise.

14.1 Step I: Message Details View(14.1 步骤 I:详细消息视图)

Let’s now start with the creation of the two new browser views, which is the goal of this chapter. While we are able to edit a message already, we currently have no view for simply viewing the message, which is important, since not many people will have access to the edit screen.

The view displaying the details of a message should contain the following data of the message: the title, the author, the creation date/time, the parent title (with a link to the message), and the body.
消息视图应显示的详细资料如下:标题(title)、作者(author), 创建日期/时间(creation date/time), 父标题(链接到消息)(parent title)、内容(body)。

Writing a view usually consists of writing a page template, some supporting Python view class and some ZCML to insert the view into the system. We are going to start by creating the page template.

14.1.1 (a) Create Page Template(14.1.1 (a) 创建页面模板)

Create a file called details.pt in the browser package of messageboard and fill it with the following content:

1  <html metal:use-macro="views/standard_macros/view">
2    <body>
3      <div metal:fill-slot="body">
5        <h1>Message Details</h1>
7          <div class="row">
8              <div class="label">Title</div>
9              <div class="field" tal:content="context/title" />
10          </div>
12          <div class="row">
13              <div class="label">Author</div>
14              <div class="field" tal:content="view/author"/>
15          </div>
17          <div class="row">
18              <div class="label">Date/Time</div>
19              <div class="field" tal:content="view/modified"/>
20          </div>
22          <div class="row">
23              <div class="label">Parent</div>
24              <div class="field" tal:define="info view/parent_info">
25                <a href="../details.html"
26                    tal:condition="info"
27                    tal:content="info/title" />
28              </div>
29          </div>
31          <div class="row">
32              <div class="label">Body</div>
33              <div class="field" tal:content="context/body"/>
34          </div>
36      </div>
37    </body>
38  </html>
  • Line 1-3 & 36-38: This is some standard boilerplate for a Zope page template that will embed the displayed data inside the common Zope 3 UI. This will ensure that all of the pages have a consistent look and feel to them and it allows the developer to concentrate on the functional parts of the view.
    第1-3行& 36-38行:这是一些针对Zope页面模板的标准样板文件,已经被嵌入到了显示数据中。这样做的好处就在于所有的页面有一致的界面,并且允许开发者专注于视图的功能部分。

  • Line 9: The title can be directly retrieved from the content object (the Message instance), which is available as context.

  • Line 14 & 19: The author and the modification date/time are not directly available, since they are part of the object’s meta data (Dublin Core). Therefore we need to make them available via the Python-based view class, which is provided to the template under the name view. A Python-based view class’ sole purpose is to retrieve and prepare data to be in a displayable format.
    第14 &19行:作者(author)和修改日期(modification date/time )不是直接可用的,因为它们只是对象元数据(都柏林核心数据集)的一部分。因此我们需要通过基于Python的视图类让它们可用。基于Python的视图类的作用在于它能重新得到和准备数据。

  • Line 24-27: While we probably could get to the parent via a relatively simple TALES path expression, it is custom in Zope 3 to make this the responsibility of the view class, so that the template contains as little logic as possible. In the next step you will see how this information is collected.

14.1.2 (b) Create the Python-based View class(14.1.2 (b) 创建基于Python的视图类)

From part (a) we know that we need the following methods (or attributes/properties) in our view class: author(), modified(), and parent_info(). First, create a new file called message.py in the browser package. Note that we will place all browser-related Python code for IMessage in this module.
从 (a)部分我们可以知道, 在我们的视图类中我们需要以下方法(或属性):author()、 modified()、 parent_info().首先我们在browser包中创建名为message.py的新文件,注意在这个模块里我们将为IMessage安置所有与browser相关的Python 代码。

Here is the listing of my implementation:

from zope.app import zapi
from zope.app.dublincore.interfaces import ICMFDublinCore

from book.messageboard.interfaces import IMessage

class MessageDetails:

def author(self):
"""Get user who last modified the Message."""
creators = ICMFDublinCore(self.context).creators
if not creators:
return 'unknown'
return creators[0]

def modified(self):
"""Get last modification date."""
date = ICMFDublinCore(self.context).modified
if date is None:
date = ICMFDublinCore(self.context).created
if date is None:
return ''
return date.strftime('%d/%m/%Y %H:%M:%S')

def parent_info(self):
"""Get the parent of the message"""
parent = zapi.getParent(self.context)
if not IMessage.providedBy(parent):
return None
return {'name': zapi.name(parent), 'title': parent.title}
  • Line 1: Many of the fundamental utilties that you need, are available via the zapi module. The zapi module provides all crucial component architecture methods, such as getParent(). All the core servicenames are also available. Furthermore you can access traversal utilities as well. See ZOPE3/src/zope/app/interfaces/zapi.py for a complete list of available methods via the zapi module.
    第1行:通过zapi模块可以得到您所需要的大多数基础的程序,zapi模块提供了所有至关重要的组件构建方法,比如说getParent(),还有所有可利用的核心servicenames 。参见ZOPE3/src/zope/app/interfaces/zapi.py就可以得到通过zapi模块的一个完整的可利用的方法列表。

  • Line 2: The ICMFDublinCore interface is used to store the Dublin Core meta data. Using this interface we can get to the desired information.

  • Line 7: Note that the view class has no base class or specifies any implementing interface. The reason for this is that the ZCML directive will take care of this later on, by adding the BrowserView class as a base class of the view.

    In some parts of Zope 3 you might still see the view class to inherit from `BrowserView`.
    在Zope3的某些部分您仍然能够看到从`BrowserView` 继承的视图类。

  • Line 12-16: The code tries to get a list of creators (which I refer to as authors) from the Dublin Core meta data. If no creator is found, return the string “unknown”, otherwise the first creator in the list should be returned, which is the owner or the original author of the object. Note that we should usually have only one entry, since Messages are not edited (as of this stage of development).

  • Line 20-28: Finding the modification date is a bit more tricky, since during the creation only the created field is populated and not the modified field. Therefore we try first to grab the modified field and if this fails we get the created field. If the created date/time does not exist, we return an empty string.

    Finally, if a date object was found, then we convert it to a string and return it.

  • Line 30-33: Getting the parent is easy, just use the getParent() method. But then we need to make sure that the parent is also an IMessage object; if it is not, then we have a root message, and we return None. The name and the title of the parent are stored in an information dictionary, so that the data can be easily retrieved in a page template.

14.1.3 (c) Registering the View(14.1.3 (c) 注册视图)

The final task is to register the new view using ZCML. Open the configuration file in the browser sub-package and add the following lines:

1  <page
2      name="details.html"
3      for="book.messageboard.interfaces.IMessage"
4      class=".message.MessageDetails"
5      template="details.pt"
6      permission="zope.Public"
7      menu="zmi_views" title="Preview"/>
  • Line 1: The browser:page directive registers a single page view.

  • Line 2: The name attribute specifies the name as which the view will be accessible in the URL:

The name attribute is required.

  • Line 3: The for attribute tells the system that this view is for IMessage objects. If this attribute is not specified, the view will be registered for Interface, which means for all objects.

  • Line 4-5: Use the just created MessageDetails class and details.pt page template for the view; for this page details.pt will be rendered and uses an instance of MessageDetails as its view.

    Note that not all views need a supporting view class; therefore the class attribute is optional.

    While you will usually specify a page template for regular pages, there are situations, where you would prefer a view on an attribute of the Python view class. In these cases you can specify the attribute attribute instead of template. The specified attribute/method should return a unicode string that is used as the final output.

  • Line 6: The permission attribute specifies the permission that is required to see this page. At this stage we want to open up the details pages to any user of the site, so we assign the zope.Public permission, which is special, since every user, whether authenticated or not, has this permission.

  • Line 7: In order to save ourselves from a separate menu entry directive, we can use the menu and title attribute to tell the system under which menu the page will be available. In this case, make it a tab ( zmi_views menu) which will be called “Preview”.
    第7行:为了避免使用另外单独的菜单条目指令,我们可以利用菜单和名称属性来告知系统,网页应当隶属于哪一个菜单。此处我们使用一个标签(zmi_views menu),并将其命名为Preview(预览)。

All you need to do now is to restart Zope, add a Message content object (if you have not done so yet) and click on it. The “Preview” tab should be available now. Note that you will have no “Parent” entry, since the message is not inside another one.
现在您必须重新启动Zope,如果您以前没有添加一个消息对象的话请单击添加它。"Preview" tab菜单条目现在应该有效。假如这个消息没有包含在另一个消息里面,请注意界面上将没有"Parent"条目显示。

To see a “Parent” entry, add another message inside the current message by using the “Contents” view. Once you added the new message, click on it and go to the Details view. You should now see a “Parent” entry with a link back to the parent message.

14.1.4 (d) Testing the View((d) 测试视图)

Before moving to the next item on the list, we should develop some functional tests to ensure that the view works correctly. Functional tests are usually straight forward, since they resemble the steps a user would take with the UI. The only possibly tricky part is to get all the form variables set correctly.

To run the functional tests the entire Zope 3 system is brought up, so that all side-effects and behavoir of an object inside its natural environment can be tested. Oftentimes very simple tests will suffice to determine bugs in the UI and also in ZCML, since all of it will be executed during the functional test startup.
整个Zope3 系统提出了进行功能测试,以便一个对象的所有副作用和行为在它处的环境里面都可能被测试出来。经常进行一些简单的测试也能在UI和ZCML里发现出致命的错误,这是由于功能测试起动后所有的过程都将被执行。

The following functional tests will ensure that messages can be properly added and that the all the message details information are displayed in the “Preview”. By convention all functional tests are stored in a sub-module called ftests. Since we plan to write many of these tests, let’s make this module a package by creating the directory and adding an <u>init</u>.py file.

Now create a file called test_message.py and add the following testing code:

import unittest
from zope.app.tests.functional import BrowserTestCase

class MessageTest(BrowserTestCase):

def testAddMessage(self):
response = self.publish(
form={'field.description': u'Message Board',
self.assertEqual(response.getStatus(), 302)
response = self.publish(
form={'field.title': u'Message 1',
'field.body': u'Body',
self.assertEqual(response.getStatus(), 302)

def testMessageDetails(self):
response = self.publish('/board/msg1/@@details.html',
body = response.getBody()
self.checkForBrokenLinks(body, '/board/msg1/@@details.html',

self.assert_(body.find('Message Details') > 0)
self.assert_(body.find('Message 1') > 0)
self.assert_(body.find('Body') > 0)

def test_suite():
return unittest.TestSuite((

if <u>name</u> == '<u>main</u>':
  • Line 2: In order to simplify writing browser-based functional tests, the BrowserTestCase can be used as a test case base class. The most important convenience methods are used in the code below.

  • Line 6-23: Before we are able to test views on a message, we have to create one. While it is possible to create a message using a lower-level API, this is a perfect chance to write tests for the adding views as well.
    • Line 7-11: The publish() method is used to publish a request with the publisher. The first arguments is the URL (excluding the server and port) to be published. Commonly we also include the basic argument, which specifies the username and password. The system knows only about the user zope.mgr with username “mgr” and password “mgrpw”. The role zope.Manager has been granted for this user, so that all possible screens should be availale.

      In the form you specify a dictionary of all variables that are submitted via the HTTP form mechanism. The values of the entries can be already formatted Python objects and do not have to be just raw unicode strings. Note that the adding view requires a field named UPDATE_SUBMIT for the object to be added. Otherwise it just thinks this is a form reload.

    • Line 12-14: The adding view always returns a redirect (HTTP code 302). We can also verify the destination by looking at the “Location” HTTP header.
      第12-14行:添加视图总是返回重定向(HTTP 代码 302)。我们也能够通过查看"Location"HTTP头来查证目的地。

    • Line 15-23: Here we repeat the same procedure; this time by adding a message named “msg1” to the message board.

  • Line 25-35: After creating the message object (line 26), the details view is simply requested and the HTML result stored in body (line 27-29).
    第25-35行: 在创建消息对象 (第 26 行) 之后,详细视图被请求并且HTML结果存放在body里(27-29行)。

    One of the nice features of the BrowserTestCase is a method called checkForBrokenLinks() that parses the HTML looking for local URLs and then tries to verify that they are good links. The second argument of the method is the URL of the page that generated the body. This is needed to determine the location correctly. We should also specify the same authentication parameters, as used during the publication process, since certain links are only available if the user has the permission to access the linked page.

    In the last the tests (line 33-35) we simply check that some of the expected information is somewhere in the HTML, which is usally efficient, since a faulty view usually causes a failure during the publishing process.

  • Line 38-44: As always, we have to have the usual boilerplate.
    第 38 行-44行: 按惯例,我们必须有这些通用的样板代码。

Now that the tests have been developed, we can run them like the unit tests, except that for using the -u option (unit tests only), we now specify the -f option (functional tests only).

1  python2.3 test.py -vpf --dir src/book/messageboard

Since you already looked at the pages before, all tests should pass easily, unless you have a typo in your test case. Once the tests pass, feel free to go on to the next task.
既然以前您已经看过本页,所有的测试应该很容易地通过, 除非您的测试范例有书写问题。一旦测试通过,您就可以继续下一个任务了。

14.2 Step II: Specifying the Default View(14.2 步骤 II:指定缺省视图)

If you try to view a message using http://localhost:8080/board/msg1 at this point, you will get the standard container index.html view. This is rather undesirable, since you default view should really show the contents of the message.
此时如果您试着用 http://localhost:8080/board/msg1" 查看一个消息,您将会获得一个标准的index.html视图。由于缺省视图只是显示出了消息的内容,因此界面就不是那么合乎用户的需求了。

There is a special directive for declaring a default view. All you need to add are the following lines to your browser package configuration file:

1  <defaultView
2      for="book.messageboard.interfaces.IMessage"
3      name="details.html"/>
  • Line 2: Here we tell the system that we are adding a default view for the components implementing IMessage.

  • Line 3: We make the “Preview” screen the default view. However, you can choose whatever view you like. Naturally, these views are usually views that display data instead of asking for input. It is also advisable to make the least restrictive and most general view the default, so that users with only a few permissions can see something about the object.
    第 3 行:我们用"Preview"界面来做缺省视图。不过您也可以选择任何您喜欢的视图。当然这些视图通常是用来数据显示,而不是用来数据输入的。同时也建议至少对视图做一些限制并且设为缺省,以便于仅拥有少量权限的用户才能查看到这个对象的某些信息。

14.3 Step III: Threaded Sub-Tree View(14.3 步骤 III:线程树型视图)

Creating a nice and extensible thread view is difficult, since the problem is recursive in nature. We would also like to have all HTML generation in Page Templates, since it allows us to enhance the functionality of the view later; however, Page Templates do not like recursion.

14.3.1 (a) Main Thread Page Template(14.3.1 (a) 主线程页面模板)

So let’s tackle the problem by starting to create the main view template for thread.html, which we call thread.pt:

1  <html metal:use-macro="views/standard_macros/view">
2    <body>
3      <div metal:fill-slot="body">
5        <h1>Discussion Thread</h1>
7        <div tal:replace="structure view/subthread" />
9      </div>
10    </body>
11  </html>

Almost everything is boiler plate really, but there is enough opportunity here to add some more functionality later, if we desire to do so.

  • Line 7: Being blind about implementation, we simply assume that the Python-based view class will have a subthread() that can magically generate the desired sub-thread for this message or even the message board.
    第 7 行:现在我们随便做些应用, 我们简单地假定基于Python的视图类有一个能为这个消息或者留言簿产生我们需要的子线程的方法subthread()。

14.3.2 (b) Thread Python View Class(14.3.2 (b) 线程Python视图类)

Next we have to build our Python view class. We start by editing a file called thread.py and insert the following code:

from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from book.messageboard.interfaces import IMessage

class Thread:

def <u>init</u>(self, context, request, base_url=''):
self.context = context
self.request = request
self.base_url = base_url

def listContentInfo(self):
children = []
for name, child in self.context.items():
if IMessage.providedBy(child):
info = {}
info['title'] = child.title
url = self.base_url + name + '/'
info['url'] = url + '@@thread.html'
thread = Thread(child, self.request, url)
info['thread'] = thread.subthread()
return children

subthread = ViewPageTemplateFile('subthread.pt')
  • Line 1: The ViewPageTemplateFile class is used to allow page templates to be attributes/methods of a Python class. Very handy.
    第 1 行: ViewPageTemplateFile类允许页面模板有Python类的属性和方法,非常便利。

  • Line 2: Import the IMessage interface, since we need it for object identification later.
    第 2 行: 导入IMessage 接口, 因为我们稍后为对象标识需要它。

  • Line 25: Here is our promised subthread() method, which is simply a page template that knows how to render the thread. Note: You might want to read part (c) first, before proceeding.
    第 25 行: 这里有我们承诺的subthread()方法,它只是一个简单知道怎样描绘线程的页面模板。注意:在进行之前,您可能想先阅读part (c)。

  • Line 12-23: This method provides all the necessary information to the subthread page template to do its work. For each child it generates an info dictionary. The interesting elements of the dictionary include the url and the thread values. The URL is built up in every iteration of the recursive process. We could also use the zope.app.traversing framework to generate the URL, but I think this is a much simpler this way.

    The second interesting component of the info, the thread value, should contain a string with the HTML describing the subthread. This is were the recursion comes in. First we create a Thread instance (view) for each child. Then we are asking the view to return the subthread of the child, which is certainly one level deeper, which in return creates deeper levels and so on. Therefore the thread value will contain a threaded HTML representation of the branch.
    首先我们为每个child创建一个Thread实例(view),然后我们要求视图返回到child的subthread,确定了下一层, 当返回后创建更深的层等等。因此thread值将包含一个分支线程的HTML表示。

14.3.3 (c) Sub-Thread Page Template((c) 子线程页面模板)

This template, named subthread.pt as required by the view class, is only responsible of creating an HTML presentation of the nested Message children using the information provided; therefore the template is very simple (since it contains no logic):
我们把view类需要的这个模板命名为subthread.pt,模板仅负责用提供的信息创建款套Message children,因此模板是很简单的(因为没包含逻辑):

1  <ul>
2    <li tal:repeat="item view/listContentInfo">
3      <a href=""
4          tal:attributes="href item/url"
5          tal:content="item/title">Message 1</a>
6      <div tal:replace="structure item/thread"/>
7    </li>
8  </ul>
  • Line 1 & 8: Unordered lists are always good to create threads or trees.
    第1行:UL总是有益于创建threads 或trees。

  • Line 2: Thanks to the Thread view class, we simply need to iterate over the children information.
    第2行:由于Thread view类,我们只是需要反复children信息。

  • Line 3-5: Make sure we show the title of the message and link it to the actual object.

  • Line 6: Insert the subthread for the message.

14.3.4 (d) Register the Thread View(14.3.4 (d) 注册Thread View)

Registering the thread view works like before:

1  <page
2      name="thread.html"
3      for="book.messageboard.interfaces.IMessage"
4      class=".thread.Thread"
5      template="thread.pt"
6      permission="zope.View"
7      menu="zmi_views" title="Thread"/>

You should be familiar with the page directive already, so the above code should be easy to understand.

You also have to register the same view for IMessageBoard, so that you can get the full thread of the entire messageboard as well.
您也必须为 IMessageBoard 注册相同的视图,以便于您能得到整个messageboard 的完整线程。

14.3.5 (e) Message Board Default View(14.3.5 (e) 留言簿缺省视图)

Since the message board does not have a default view yet, let’s make the thread view the default:

1  <defaultView
2      for="book.messageboard.interfaces.IMessageBoard"
3      name="thread.html"/>

This is, of course, very similar to the default view we registered for IMessage before.

14.4 Step IV: Adding Icons(14.4 步骤 IV:添加图标)

Now that we have some text-based views, let’s look into registering custom icons for the message board and message. Icons are also just views on objects, in this case our content components. However, to make life easier the browser namespace provides a convenience directive called icon to register icons.
既然我们有一些基于文本的视图,让我们研究为留言簿和消息定制图标,图标也就是在当前情况下我们内容组件上的视图。然而,browser命名空间提供一个更为方便的 icon 指令来注册图标。

Simply add the following directive for each content type in the browser package configuration file:

1  <icon
2      name="zmi_icon"
3      for="book.messageboard.interfaces.IMessage"
4      file="message.png" />

The code should be self-explanatory at this point. Instead of a template, we are specifying a file here as the view, which is expected to be binary image data and not just ASCII text.
这段代码此时应该不需要说明。替代上面的模板,我们指定一个文件作为视图,这是图象数据而不是 ASCII文本文件。

Now you should be all set. Restart Zope 3 and see whether the new features are working as expected.
现在您已经做了所有的设置。重新启动Zope 3,然后看新功能是否按照您的预期在开展工作。

The code is available in the Zope SVN under http://svn.zope.org/book/trunk/messageboard/step02.
代码可在Zope SVN http://svn.zope.org/book/trunk/messageboard/step02 中得到。


  • For the message details screen it might be also useful to display the author of the parent message. Expand the returned information dictionary of parent_info to include the author of the parent and display it properly using the template.

  • It would be great if there was a Reply, Modify, and Delete link (maybe as an image) behind each message title and make the actions work. Note that you should be able to reuse a lot of existing code for this.