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

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.

现在我们开始以创建两个新的browser视图为本章的目标。我们现在已经能编辑消息,可当前我们没有通过简单视图的方式来查看该消息,这是非常重要的,因为不是很多人都有权利进入编辑页面的。

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.

消息视图应显示的详细资料如下:

标题、作者, 创建日期/时间, 父标题(链接到消息)、内容。

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.

写视图通常包括写页面模板,一些支持Python的视图类和一些ZCML视图放入系统,我们将开始创建页面模板。 14.1.1 (a) Create Page Template

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

在messageboard的browser中创建名为details.pt的文件,在该文件中填充如下内容:

1 <html metal:use-macro="views/standard_macros/view"> 2 <body> 3 <div metal:fill-slot="body"> 4 5 <h1>Message Details</h1> 6 7 <div class="row"> 8 <div class="label">Title</div> 9 <div class="field" tal:content="context/title" /> 10 </div> 11 12 <div class="row"> 13 <div class="label">Author</div> 14 <div class="field" tal:content="view/author"/> 15 </div> 16 17 <div class="row"> 18 <div class="label">Date/Time</div> 19 <div class="field" tal:content="view/modified"/> 20 </div> 21 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> 30 31 <div class="row"> 32 <div class="label">Body</div> 33 <div class="field" tal:content="context/body"/> 34 </div> 35 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.

这是一些针对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.

  • 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

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:

下面是我们的执行清单:

1 from zope.app import zapi 2 from zope.app.dublincore.interfaces import ICMFDublinCore 3 4 from book.messageboard.interfaces import IMessage 5 6 7 class MessageDetails: 8 9 def author(self): 10 """Get user who last modified the Message.""" 11 creators = ICMFDublinCore(self.context).creators 12 if not creators: 13 return 'unknown' 14 return creators[0] 15 16 def modified(self): 17 """Get last modification date.""" 18 date = ICMFDublinCore(self.context).modified 19 if date is None: 20 date = ICMFDublinCore(self.context).created 21 if date is None: 22 return 23 return date.strftime('%d/%m/%Y %H:%M:%S') 24 25 def parent_info(self): 26 """Get the parent of the message""" 27 parent = zapi.getParent(self.context) 28 if not IMessage.providedBy(parent): 29 return None 30 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.

通过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.

ICMFDublinCore接口被用于存储都柏林核心数据,我们可以使用这个借口得到我们需要得到的信息

  • 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.

注意:这个视图类没有基础类或指定任何应用接口,理由是稍后可以通过ZCML指令为视图添加BrowserView基础类

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

这段代码试着从都柏林核心集获得创建者(涉及到作者)列表。如果创建者没有发现,就返回字符串“unknown”,否则列表中的第一个创建者将被返回,也就是这个对象的拥有者或原始作者。注意当消息没有被编辑(作为开发步骤)时我们通常应该仅仅有一个条目,

  • 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.

使用getParent()方法我们可以比较轻易的得到父对象。但是我们需要确认父对象仍然是一个Imessage对象。如果不是并且还有一个根消息,我们就返回None。如果是的话这个父对象的名称和标题将被存放在字典中,以至于能够比较轻易的在页面模板中找回数据。 14.1.3 (c) Registering the View

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:

最后的工作就是用ZCML注册这个新的视图。在browser目录中打开配置文件(configure.zcml)并添加如下各行:

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.

The name attribute is required.

名称属性指定了可以通过如下地址访问这个视图:

http://localhost:8080/board/message1/@@details.html

名称属性是必须的。

  • 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.

For属性将告诉系统这个视图是针对Imessage对象。如果这个属性没有被指定,表明视图将注册的接口适合全部对象。

  • 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.

为该视图创建MessageDetails类和details.pt页面模板;details.pt页将作为视图被提供和用作MessageDetails的实例。

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.

通常你为一个规则的页面指定页面模板时,你也许宁愿存在视图基于Python视图类属性的情形。这样的话,你能指定代替模板的属性。指定的属性/方法应该以unicode字符串的形式返回并最终输出。

  • 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.

Permission属性指定了查看该页面被要求的权限。现在我们想设置任何该站点的用户都有打开详细页的权限,于是我们设定了zope.Public权限。

  • 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”.

为了避免使用另外单独的菜单条目指令,我们可以利用菜单和名称属性来告知系统,网页应当隶属于哪一个菜单。此处我们使用一个标签(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.

为了在界面上看到“Parent”条目,需要使用“Contents”视图在当前消息下面新增另一条消息。现在你应该能看到一个能够链接到父消息的“Parent”条目。 14.1.4 (d) Testing the View

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.

在继续下一项之前,我们应该开发一些功能测试来确保视图正确工作。功能测试通常是一直向前的,因为它们类似用户通过UI所采取的步骤。唯一感到棘手的问题是如何正确设置所有的变量。

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 init.py file.

在下面的功能测试中将确信消息能够增加并且所有的详细信息将被显示在“Preview”中。所有的功能测试都习惯被存放在名叫ftests的字模块中。当我们决定写这些测试时,让我们通过创建目录和增加init.py文件来制作测试模块。

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

现在我们创建一个名为test_message.py的文件并且加上如下测试代码:

1 import unittest 2 from zope.app.tests.functional import BrowserTestCase 3 4 class MessageTest(BrowserTestCase): 5 6 def testAddMessage(self): 7 response = self.publish( 8 '/+/AddMessageBoard.html=board', 9 basic='mgr:mgrpw', 10 form={'field.description': u'Message Board', 11 'UPDATE_SUBMIT': 'Add'}) 12 self.assertEqual(response.getStatus(), 302) 13 self.assertEqual(response.getHeader('Location'), 14 'http://localhost/@@contents.html') 15 response = self.publish( 16 '/board/+/AddMessage.html=msg1', 17 basic='mgr:mgrpw', 18 form={'field.title': u'Message 1', 19 'field.body': u'Body', 20 'UPDATE_SUBMIT': 'Add'}) 21 self.assertEqual(response.getStatus(), 302) 22 self.assertEqual(response.getHeader('Location'), 23 'http://localhost/board/@@contents.html') 24 25 def testMessageDetails(self): 26 self.testAddMessage() 27 response = self.publish('/board/msg1/@@details.html', 28 basic='mgr:mgrpw') 29 body = response.getBody() 30 self.checkForBrokenLinks(body, '/board/msg1/@@details.html', 31 basic='mgr:mgrpw') 32 33 self.assert_(body.find('Message Details') > 0) 34 self.assert_(body.find('Message 1') > 0) 35 self.assert_(body.find('Body') > 0) 36 37 38 def test_suite(): 39 return unittest.TestSuite(( 40 unittest.makeSuite(MessageTest), 41 )) 42 43 if name == 'main': 44 unittest.main(defaultTest='test_suite')

  • 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.

为了简化写browser-based功能测试,BrowserTestCase被用于测试基类,这是用于下面最为方便的方法。

  • 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.

在我们准备在一个消息上面测试视图之前,我们必须创建一个消息。我们可能用一个底层API函数创建一个消息,这是一个完善编写测试的机会。

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.

  1. 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.
  2. 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).

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.

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.