个人工具

简单的TODO应用程序

来自Ubuntu中文

跳转至: 导航, 搜索

简单的 TODO 应用程序


本文出处:http://worldcookery.com/files/jeffshell-todo/

本文作者:Jeffrey Shell

维护人员:Philipp von Weitershausen

翻译人员:FireHare

校对人员:Leal

适用版本:Zope 3


Preface(前言)

Welcome to Jeffrey Shell's Zope 3 tutorial. Its goal is to give web developers familiar with Python an jump start at Zope 3's powerful Component Architecture by implementing a very simple application, a TODO list manager. The tutorial's five steps initially started out as five entries on Jeff's former blog. Since this site is no longer operational, Jeff was so kind to let me take over the maintaince of this tutorial.
欢迎学习 Jeffrey Shell 的 Zope 3 教程。本教程的目的是通过一个简单的应用程序,TODO列表管理,的实现,来给熟练的 Python 网站开发人员一个了解 Zope 3 强大组件架构的跳板。本教程最初在 Jeff 先前博客中分为五部分,因为该站点不再运行,所以 Jeff 十分友好地站我来接管本教程的维护。

To give you an idea about Jeff's motivation on writing a series of introduction level texts on Zope 3, here's an excerpt from his posting on 5 March 2005:
为了让你了解 Jeff 编写 Zope 3 入门水平系列文章的动机,下面摘录了他在 2005年3月5日 所写帖子的部分内容:

I'd like to remind the audience that this is based on How to make a todo list program with Rails 0.9. It's a simple and well written application, and I'm wanting to show Python programmers that they needn't necessarily copy Rails - that there are existing solutions for Python that, in my opinion, fit the Python mindset better. If you want to program in Rails, by all means just learn Ruby and use Rails and enjoy it. But me - I like Python, I have existing applications and libraries in it, and as I start to really explore Zope 3, I'm getting really excited about using it, which hopefully I can do in near-future work related projects.
我想要提醒读者这是一个基于如何使用 Rails 0.9 来生成 todo 列表的程序,它是一个简单而又好写的应用程序,我想用它来向 Python 程序员展示他们并不需要拷贝 Rails - 已有的 Python 解决方案。因为以我看来,那更符合 Python 思路。如果你只想在用 Rails 来编程,那么当然可以只学 Ruby、并使用 Rails 而且乐在其中。但是我 - 我喜欢 Python,我有已经编好的应用程序和其中的库, 当我开始真正探索 Zope 3时,我真得感到兴奋,并满怀希望能够在不久的相关项目中使用它。

Using the Rails Todo List example is how I'm finally pushing myself to investigate building a simple but complete Zope 3 application. There may be better ways to accomplish some of what I try here. I'm no Zope 3 expert. I have followed its development and had a hand in some of the concepts early on (many years ago), but have not had the chance to build anything substantial until now. So I thank the authors of the "How to make a todo list program with rails" document for generating some excitement and giving me an idea of something to build, and I hope that they don't mind my usage of what they document as basis for my explorations.
使用 Rails 的 Todo 列表例子是想看看我自己最终对构造一个简单但完整的 Zope 3 应用程序来说能达到何种程序。也许有更好的来完成我在这所尝试的方法。我并不是 Zope 3 专家。我只是追从着它的开发,并(在许多年前)参与它的一些早期构想,但到目前为止也没有机会构建其中任何一个部分。因此我感谢《如何用 rails 编写 todo 列表程序》文章的作者给我构造的想法并让我为此感到兴奋,我希望他们不介意我用他们的文章作为我探索的基础。

I hope you'll enjoy going through this tutorial as much as I did. If you have any questions or problems, please don't bother Jeff but email me directly. I'm looking forward to receiving feedback.
我希望你能和我一样对该教程感兴趣。如果你有什么问题,请不要打扰 Jeff,你可以直接给我发邮件。我期待收到反馈。

Philipp von Weitershausen, Zope 3 developer and author of Web Component Development with Zope 3
Philipp von Weitershausen, Zope 3 开发者及《用Zope 3 开发网站组件》的作者。

A short note about the status of this work(本教程的说明)

I gave this tutorial the way I received it from Jeff the version number "1.0". I have not yet edited anything, neither grammatically nor technically. Updates will follow, including an editorial review and a compatability check for Zope 3.1, and be announced on worldcookery.com.
我从 Jeff 那儿获得本教程时版本号是 “1.0”。我没有做任何编辑,既没有做语法修饰也没有做技术修正。在随后的更新中将包括一个可编辑的复审和一个对 Zope 3.1 兼容性的检查,并将在 worldcookery.com 上公示。

Contents(目录)

Step 1: Basic interfaces and classes(步骤1:基本接口和类)

A significant design choice of Zope 3 is that it's interface driven. Interfaces are used not only to document, but to adapt. Interfaces are Zope 3's type system. Unlike class based types, anything can implement them or adapt to them, and all proper code in the Zope 3 system should ask for an interface implementation for a given object.
Zope 3 的一个重要设计就是它的接口驱动。接口不仅用于文档,也用于适配。接口是 Zope 3 的类型系统。不象基于类型的类,任何东西都可以实现或适配它们,并且在 Zope 3 系统中的所有适用代码都可以请求一个接口以实现给定的对象。

Interface design has been taken beyond basic method definitions to include object schema and even pre / post conditions. This data can be introspected to build user interfaces, which is how much of the standard Zope 3 management interface gets built. In fact, I'll show how this is done next in the series.
接口设计已经超过基本方法定义,包括对象 schema,甚至是预设/提交条件。该数据能够被自省以构建用户接口,它是众多得到构建标准 Zope 3 管理接口之一。实际上,我将展示如何在一系列接口中生成下一个。

So the basic todo application uses the following interfaces. These are just the basic model objects. The Todo List is just a container that only contains items that implement the ITodo interface. A Todo doesn't do much beyond store some simple data. So here they are, from the file interfaces.py in the package todo:
因此基本 todo 应用程序使用下列接口。这只是基本模型对象。 Todo 列表只是一个包含实现 ITodo 接口项的容器。Todo 除了保存一些简单数据外并不做其它。因此在包 todo 中的 interfaces.py 文件中如下所示:

#!/usr/bin/env python
from zope.interface import Interface
import zope.schema

# These will aid in making a special "todo" container by specifying the
# constraint in the interface.
from zope.app.container.constraints import ItemTypePrecondition
from zope.app.container.interfaces import IContained, IContainer

class ITodo(Interface):
""" A todo item doesn"t need to do much... """
description = zope.schema.TextLine(
title=u"To Do",
required=True,
)
details = zope.schema.Text(
title=u"Details",
required=False,
)

done = zope.schema.Bool(title=u"Done")


class ITodoList(IContainer):
""" A todo list contains todo objects.  """
def <u>setitem</u>(name, object):
""" Add an ITodo object. """

<u>setitem</u>.precondition = ItemTypePrecondition(ITodo)

The implementation is even simpler. Since this is a small application, I put the implementation in `todo/init.py`:
实现甚至更简单。因为这是个小应用程序,所以我把实现放到了 `todo/init.py` 文件中:

#!/usr/bin/env python
from zope.interface import implements
from zope.app.container.btree import BTreeContainer
from persistent import Persistent

from interfaces import ITodoList, ITodo

class TodoList(BTreeContainer):
implements(ITodoList)


class Todo(Persistent):
implements(ITodo)

description = u""
details = u""
done = False

And that's it. Really! Now what needs to be done next is to wire these into Zope 3. But already, one can see that this is much simpler than what one might have to do to write some data objects for Zope 2.
就这样。真的!现在下一步需要做的就是将这样写到 Zope 3 中。这看起来比为 Zope 2 编写一些数据对象要简单得多。

Step 2: Wiring the Todo into Zope 3(步骤2:在 Zope 3 中编写 Todo)

After the basic code and interfaces, comes the trick of wiring the content objects into Zope 3. This is done via ZCML, an extensible configuration language that defines and describes components for use in Zope 3. ZCML may appear to be verbose, and I have not always been its biggest fan, but I have learned that it is almost always better to be explicit than implicit. Zope 2 often made many implicit assumptions. For the most part, this wasn't a problem. But when it was a problem, it was usually something hard to find and debug. When found, it was usually something in the system that was doing something clever and assumptive that the developer didn't expect it to be clever and assumptive about.
在编写基本编码和接口之后,接下来就是要将内容对象写入 Zope 3 了。这可以通过 ZCML 来实现,ZCML 是一个扩展配置语言,用于在 Zope 3 中定义和描述组件。ZCML 也许显得有点繁杂,我也不总是它的狂热份子,但我认识到清晰明白几乎总是要比晦涩不明来得好。Zope 2 经常会有很多隐含的设定。这在大多数情况下没有什么问题。但当它成为一个问题时,那通常是很难找到和调试的。当找到时,它通常正被系统智能处理而开发人员则并不希望系统智能处理它。

So now that our interfaces and content classes are in, it's time to wire them in. Using ZCML, we can define behavior and interfaces for objects outside of code. For example, declaring `IAttributeAnnotatable` support allows other Zope 3 components to add annotations. Annotations include things like Dublin Core support, which lets Zope 3 track title, description, modified time, etc, without the developer needing to code in support themselves like they did (or inherited) in Zope 2. Much of the security declarations that filled in Zope 2 python based product code is moved to ZCML. Other elements of ZCML might be recognized as similar to the Python code in the Product def initialize(context): code:
那么现在我们的接口和内容类已经有了,是使它们工作的时候了。使用 ZCML, 我们可以为代码以外的对象定义行为和接口了。例如,声明 `IAttributeAnnotatable` 允许其他 Zope 3 组件添加 annotations。Annotations 包含象都柏林核心的支持,这可以让 Zope 3 跟踪标题、描述、修改时间等等相关信息,而不需要象在 Zope 2 中所做(或继承)的那样需要开发人员编写支持程序。Zope 2 中大部分基于 python 产品的安全声明代码都被移至 ZCML。ZCML 的其它元素也许在产品的 def initialize(context) 中被组织成同 Python 代码相似的格式:

<configure xmlns="http://namespaces.zope.org/zope">
<interface
interface=".interfaces.ITodoList"
type="zope.app.content.interfaces.IContentType"
/>
<content class="todo.TodoList">
<implements
interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
/>
<implements
interface="zope.app.container.interfaces.IContentContainer"
/>
<factory id="todo.TodoList" description="Todo List"/>
<require
permission="zope.ManageContent"
interface=".interfaces.ITodoList"
/>
</content>

<interface
interface=".interfaces.ITodo"
type="zope.app.content.interfaces.IContentType"
/>
<content class="todo.Todo">
<implements
interface="zope.app.annotation.interfaces.IAttributeAnnotatable"
/>
<implements
interface="zope.app.container.interfaces.IContentContainer"
/>
<factory id="todo.Todo" description="Todo"/>
<require
permission="zope.ManageContent"
interface=".interfaces.ITodo"
/>
<require
permission="zope.ManageContent"
set_schema=".interfaces.ITodo"
/>
</content>
<include package=".browser"/>
<include package=".browser.skin"/>
</configure>

The last two lines, <include package=".browser"/> and <include package=".browser.skin"/>, I'll get to shortly. That is where the web interface is wired in. So far in developing this little project, I've only learned and done the basic add and edit forms, which are really easy in Zope 3. This is where the interface schema items will come into play. Similar, I assume, to how Ruby On Rails has Scaffolding.
最后两行,<include package=".browser"/> 和 <include package=".browser.skin"/>,我简短地说明一下这里是引入网页界面的地方。到目前为止开发这个小项目,我只了解和使用了基本的添加和编辑表单,它们在 Zope 3 中真的很容易。在那儿接口模式项得以运行。同样的,我假设,那也是 Ruby On Rails 的支架。

But what we've done so far is setup the Model objects and wired them into the basic Zope 3 application. Next - standard views and controllers.
但到目前为止我所做的就是设置模型对象并将它们引入到基本的 Zope 3 应用程序中。接下来是 - 标准视图和控制。

Step 3: Configuring basic add/edit views for the ZMI(步骤3:为 ZMI 配置基本添加/编辑视图)

At the time of this writing, this is the farthest I've gotten with this experiment in Zope 3. What this file, browser/configure.zcml, does is wire up some basic views for the Todo List and Todo items. At this point, we have not written any HTML templates or any view controllers. At this stage, we don't really need to. This is all that is needed to use automatic add and edit forms, complete with code validation. The interfaces we defined go a long way to providing these forms with their display. We also define some containerViews for the Todo List. Since the `TodoList` subclasses and sub-interfaces a standard Zope 3 container implementation (think Folder, but much simpler. More like a dictionary), it would be nice to reuse the basic container views for the Todo List. This is how we'll get away with adding Todo items:
在写这一节时,是我在这次 Zope 3 实验中得到最多的。 browser/configure.zcml 这个文件导入了 Todo 列表和 Todo 列表项。在这里,我们没有写任何 HTML 模板或控制视图。眼下我们真的不需要。所需要做的就是使用自动 add 和 edit 表单,检查代码的有效性。我们所定义的接口提供并显示这些表单。我们还为 Todo 列表定义了一些 containerViews。由于 `TodoList` 子类和子接口是一个标准的 Zope 3 container 的实现(想成是文件夹,不过要简单得多,更象是一个目录),因此对于 Todo 列表复用基本 container 视图来讲是很好的。下面我们将开始添加 Todo 列表项:

<configure xmlns="http://namespaces.zope.org/browser">
<!-- Allow adding of the Todo List -->
<addMenuItem
class="todo.TodoList"
title="Todo List"
description="A Todo List"
permission="zope.ManageContent"
/>

<!-- Allow basic Zope 3 container views for the Todo List -->
<containerViews
for=".interfaces.ITodoList"
index="zope.View"
contents="zope.View"
add="zope.ManageContent"
/>

<!-- And now add / edit forms for the Todo list -->
<addform
label="Add Todo"
name="addtodo.html"
schema="todo.interfaces.ITodo"
content_factory="todo.Todo"
permission="zope.ManageContent"
/>
<addMenuItem
class="todo.Todo"
title="Todo"
description="A Todo Item"
permission="zope.ManageContent"
view="addtodo.html"
/>

<editform
schema="todo.interfaces.ITodo"
for="todo.interfaces.ITodo"
label="Edit Todo"
name="edit.html"
permission="zope.ManageContent"
menu="zmi_views" title="Edit"
/>
</configure>

There is more that these ZCML declarations can do - filter out specific fields from a Schema definition, wire into custom classes that override widget definitions or provide other help, or use alternate templates than the ones the system uses. This allows the add/edit functionality to be used in custom applications that don't make use of the ZMI but could otherwise benefit by what these editform and addform view controllers provide.
这些 ZCML 声明可以做得更多 - 从 Schema 定义中过滤指定域,导入自定义类以使 widget 定义无效或提供其他帮助,还可以更换系统所用的模板。这里允许 add/edit 功能用于自定义应用程序,这些应用程序并不使用 ZMI而是从那些 editform 和 addform 提供的视图控制器中得到好处。

Screen shots of the views generated by these declarations are in the extended entry.
由这些声明所生成视图的屏幕抓图。

attachment:todo_list_add_list.jpg

Above we have the ZMI add lists. The first one shows the root list, including 'Todo List' and 'Todo', from the `addMenuItem` interfaces. The second one shows the add list from inside the 'Todo List', which is restricted to just Todo. This is from the `ITodoList.setitem.precondition` configuration.
总之我们可以在 ZMI 中的添加列表了。第一个显示的是继承自 `addMenuItem` 接口的根列表,包含 'Todo List' 和 'Todo'。第二个显示的是从 'Todo List' 中添加列表,该列表也只能添加 Todo。这是由 `ITodoList.setitem.precondition` 配置的。

attachment:todo_error.jpg

This shows the automatically generated add form with an input error. You can also see the optional Object Name field which gives the object its identifier within its container. If not supplied, Zope 3 can choose the 'name' (equivalent to the Zope 2 'id').
上图显示了自动生成的添加表单,该表单有一个输入错误。你也可以看到一个可选的对象名域,可以在它的 container 中给该对象标识符。如果不支持的话,Zope 3 也可以选择 'name' (相当于 Zope 2 的 'id')。

attachment:todo_list_contents.jpg

This shows the todo list contents. One entry had a name manually entered by me, the other one was chosen by Zope 3. This is done via a Name Chooser component (interface `INameChooser`). In the future, I hope to look at how to write a custom name chooser that automatically generates the todo's name based on the main 'to do' item content. In effect, this should mean just writing an `INameChooser` adapter for `ITodoList` objects.
上图显示了 todo 列表内容。一项是由我们手工输入名字的,另一项则是由 Zope 3 自动选择的,这是通过一个名字选择组件(`INameChooser`接口)来完成的。将来,我希望能考虑一下写一个自定义的名字选择器,它可以基于主 'to do' 项内容自动生成 todo 的名字。实际上,这将意味着为 `ITodoList` 对象写一个 `INameChooser` 适配器。

Step 4: Making a Skin for the Todo App(步骤 4:为 Todo 应用编写皮肤)

When we last visited the little todo application for Zope X3, we had made some very basic views for the Zope Management Interface (ZMI). At this point, no HTML had been written. No other view/controller code had been written. And we didn't have much of a todo application, but we did have editable Todo objects being stored in a Todo List in the object database. But what it needs now is its own look. To do this, we need to make a skin.
当我们最后访问 Zope X3 的 todo 小应用时,我们为 Zope 管理接口(ZMI)编写了一些非常基础的视图。在这里,没有编写任何的 HTML。也没有编写其他的视图/控制器代码。除了在对象数据库中有一些可编辑的 Todo 对象正保存在 Todo 列表中外,Todo 应用并没多少。但它现在需要它自己的外观。为了做到这一点,我们需要编写一个皮肤。

Zope 3 has a concept of View Components, which could be any sort of view on an object - HTML, FTP, XMLRPC, or even native widgets for an operating system. Views and controllers specific to an HTML interface are typically thought of as "browser" views. There's a browser ZCML namespace, where most web views are configured, and it seems to be a common practice to group code for web browsers in a python package or module called browser to connote the type of view that it is. XMLRPC views are typically bundled in a module or package called xmlrpc, etc. There is no enforcement of these names.
Zope 3 有一个视图组件的概念,它可以是基于对象的任何类型的视图 - HTML、FTP、XMLRPC 甚至可以是操作系统自身的部件。视图和控制器指定一个HTML接口是作为 "browser(浏览器)" 视图的典型思路。这里有一个浏览器 ZCML 名字空间,在其中配置大多数的 Web 视图,似乎它常常将为 一个叫 browser 的 python 包或模块所包含的那些视图类型的代码进行分类。XMLRPC 视图就是典型的被捆绑在名为 xmlrpc 之类的模块或包中。这些名字不具备强制性。

So what we're interested in now is making a custom look for a web browser. Zope 3's browser UI system uses a concept of skins, which are made up of layers. Browser views are assigned to layers. When a view needs to be looked up, the Zope 3 framework goes through the layers defined for the current skin and looks up the requested view until it finds it. This is similar, I suppose, to how a CSS class is looked up. The HTML code may say to use class 'highlight', and the style sheets are gone through in order until 'highlight' is found. Replacement style sheets may be used (such as print or presentation) that display the 'highlight' class differently.
因为我现在感兴趣的是为 web 浏览器编写一个自定义的外观。Zope 3 的浏览器 UI 系统使用皮肤这个虚似层的概念。浏览器视图被指定给层。当一个视图需要被查看时,Zope 3 框架就为当前皮肤遍历已定义的层,并在找到后将该层显示出来。我猜想这同查找一个 CSS 类是相类似的。HTML 代码可能要使用类 'highlight',然后样式表就被遍历直到 'highlight' 被找到。替换可能被用的样式表(如 print 或 presentation)将显示不同的 'highlight' 类。

To make the todolist's skin, I made a new directory in the todo package named browser, and made it into a Python package by adding an empty `init.py`. I then made a sub-package of browser called skin, and added an empty `init.py` there as well.
为了编写 todolist 的皮肤,我在 todo 包中生成了一个名为 browser 的新目录,并通过添加一个空的 `init.py` 文件将其纳入 Python 包。然后我在 browser 目录中生成一个 skin 的子包,同样将空的 `init.py` 文件添加其中。

Once there, I made a new file called template.pt. Using the 'Developing New Skins' chapter of the Zope 3 Developers Book for guidance, I created the following template (Note that this is done using Zope Page Templates):
然后,我新建一个 template.pt 文件。用 [[../Zope3Book|Zope 3 宝典]] 中的 “开发新皮肤”这一章作为参考,我创建了下面的模板(注意它是使用的是 Zope 页面模板):

<metal:block define-macro="page">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title metal:define-slot="title">Todo List for Zope X3</title>
<style type="text/css" media="all"
tal:content="string: @import url(${context/++resource++todo.css});;">
@import url(todo.css);
</style>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>

<body>
<div id="todo_header">Todo List for Zope 3</div>
<div id="workspace">
<div metal:define-slot="message" id="message"></div>

<div id="content">
<metal:block define-slot="body">
This is the content.
</metal:block>
</div>
</div>

<div id="footer">
<div id="actions">
<metal:block define-slot="actions" />
</div>
</div>
</body>
</html>
</metal:block>

I followed that with a CSS file called todo.css, with the following content:
接下来生成一个 todo.css 的 CSS 文件,内容如下:

body {
font-family: Verdana, Arial, Helvetica, sans-serif;
background: white;
color: black;
margin: 0;
padding: 0px;
}

h1, h2, h3, h4, h5, h6 {
font-weight: bold;
color: black;
}

#todo_header {
background: black;
color: white;
padding: 1em;
font-size: 150%;
}

#footer {
background: black;
color: white;
padding: 1em;
font-size: 150%;
}

Now for the important part - wiring these two files into Zope so that we can use them in the todo application. To do this, we need to define a couple of things:
现在到了重要的部分了 - 将这两个文件写入 Zope 以便我们可以在 todo 应用程序中使用它们。为了做到这一点,我们需要几个定义:

  • A new layer - the template and CSS defined above will be attached to this new browser UI layer, as well as the custom views to add and complete 'todo' items in the list.
    一个新层 - 上面的模板和 CSS 定义将放入这个新的浏览器 UI 层,同时也包括在列表中添加和完成 'todo' 项的自定义视图。
  • A new skin - Remember, a skin is composed of layers. The new layer we define above will be added on top of other layers in the system so that other views and resources can be picked up if needed.
    一个新皮肤 - 记住,一个皮肤是由层组成的。我们上面定义的新层将被添加到系统其他层的顶层以便在需要时其他视图和资源可以被获取。
  • Resources and Pages - The CSS file, since it's not dynamic, can be defined as a resource, while the template can be defined as a page (basically a complete HTML view).
    资源和页面 - 当模板被定义成页面时(大致上是一个完整的 HTML 视图),CSS 文件,因为它不是动态的,可以被定义成资源。

With that list in mind, we add a file in todo/browser/skin called configure.zcml with the following contents:
为了实现想法中的列表,我们在 todo/browser/skin 目录中新建一个 configure.zcml 文件,并填入以下内容:

<configure xmlns="http://namespaces.zope.org/browser">
<layer name="todo"/>
<skin name="todo" layers="todo rotterdam default"/>

<resource name="todo.css" file="todo.css" layer="todo"/>

<page
for="*"
name="skin_macros"
permission="zope.View"
layer="todo"
template="template.pt"
/>
</configure>

As you can see, it pretty much maps to the list of items above. A layer is made, named todo, and a skin of the same name is made with the layers todo, rotterdam, and default. When the todo skin is used, views will be looked up in that order, starting from the beginning of the chain. After that, we define the CSS file as a resource to be used in the todo layer. And then we define a page for the template, also in the todo layer. Note that there's a for="*" in the page declaration. That means the page is a browser view that can be used for any object in the system. We want this because this page defines the basic ZPT METAL macros that all other pages/templates will use.
正如你所见,它几乎全部实现了上面所谈到的几条。生成一个名为 todo 的层,一个由 todo、rotterdam 和 default 层组成的同样名字的皮肤。当 todo 皮肤使用时,视图将按照这个顺序遍历,从层链开始处开始。在那之后,我们将 CSS 文件定义成资源在 todo 层中使用。然后我们为模板定义了一个页面,也是在 todo 层中。注意在页面声明中有一个 for="*"。这意味着浏览器视图可以在系统中的其他对象使用。我们这样做的原因是因为这个页面所定义基本 ZPT METAL 宏是其他所有页面/模板都要用的。

With that, we have a custom skin defined - a simple template to use, and a named layer to start writing custom views for. The next post will talk about implementing some of those browser views.
这样,我们就有一个自定义的皮肤 - 一个简单的模板可以用,并且有个被命名的层以便可以开始写自定义的视图了。下一节就是讨论实现那些浏览器视图。

Step 5: Browser Views(步骤 5:浏览器视图)

Now that a simple skin has been established, it's time to add some views to the todo list. In a style inspired by the article on how to build a todo list in Rails, we're going to present the todo list in two parts on the same page: Incomplete Items, and Completed Items. In the middle will be a form to add a new item. Looking back at the code and interfaces for the basic todo list, you'll notice that there's no real logic here. Just basic 'model' stuff. The list doesn't really care about separating complete and incomplete todo items - that's a detail specific to our browser view.
现在一个简单的皮肤已经确定下来了,是为 todo 列表添加视图的时候了。从文章《如何用 Rails 构建一个 todo 列表》的样式中得到启发,我们打算把 todo 列表在一页上分为两部分:待完成项和完成项。在中间是一个添加新项的表单。回头看看基本 todo 列表的代码和接口,你会注意到这里并没有真正的逻辑,只是基本 "模块" 的堆砌。列表并不是真的在乎区分完成和未完成项 - 那对于我们的浏览器视图来说只是细节特性。

So one of the first things we need to do is write a Python class that will be a view component for an `ITodoList`. Its responsibilities will be:
因此我们首先要做的事情就是需要为 `ITodoList` 类写一个视图组件的 Python 类。它负责:

  • Listing completed items.
    列出已完成项
  • Listing incomplete items.
    列出未完成项
  • Listing all items (if desired).
    列出所有项(如果需要的话)
  • Returning information about the todo items that are interesting to the browser view - such as the items id.
    给浏览器视图返回感兴趣的 todo 项信息 - 如项 ID 号。
  • A片下载

Unlike Zope 2 objects, or even items in most RDBMS designs, Zope 3 strives for a design where if all an ITodo item cares about is its visual name and whether its done or not, it shouldn't know what its 'id' is. The 'id' is the key or name used to get the todo item out of its container. A general container in Zope 3 is not much more than a Python dictionary. So when listing objects out of the container, we want to bring its key along so that other views off of the listing can access the item.
不同于 Zope 2 的对象或是在大多数关系数据库设计中的项,Zope 3 是基于一种设计,如果一个 ITodo 项关心的是它的可见名和是否被做,那么它将不知道它的 'id' 是什么。'id' 是一个关键字或在容器外可以获得 todo 项的名字。在 Zope 3 中通用容器并不比 Python 字典多多少。因此在容器外列出对象时,我们要使用它的关键字以便不在列表的其他视图可以访问到项。

I put my view code for this part of the project in `todo/browser/init.py`. The imports that will be used are:
我在 `todo/browser/init.py`中为项目的这部分帖上我的视图代码。如下所示:

#!python
from zope.app import zapi
from zope.event import notify
from zope.app.event.objectevent import ObjectCreatedEvent, ObjectModifiedEvent

from zope.app.publisher.browser import BrowserView
from zope.app.container.browser.adding import BasicAdding

from todo.interfaces import ITodo, ITodoList
from todo import Todo

zapi is a module which contains common API's for using the component architecture, as well as object traversal. The `ObjectCreatedEvent` and zope.event.notify will be used when we get to writing the code to handle adding a new todo object. `BrowserView` and `BasicAdding` are classes that will be subclassed. The remaining items are from the 'todo' application itself.
zapi 是一个包含着使用组件架构通用 API 的模块,除对象遍历外。当我们去编写处理新添 todo 对象时要用到 `ObjectCreatedEvent` 和 zope.event.notify。 `BrowserView` 和 `BasicAdding` 是子类,其它项都来自 todo 应用程序本身。

The `TodoListView` implementation looks like this:
实现 `TodoListView`,如下所示:

#!python
class TodoListView(BrowserView):
<u>used_for</u> = ITodoList

def _extractInfo(self, item):
"""
Extract some useful information for use in a list view and return a
mapping.

:type item: tuple (name, ITodo)
"""
id, obj = item
info = {
'id': id,
'object': obj,
'done': obj.done,
}
return info

def listAll(self):
return map(self._extractInfo, self.context.items())

def listIncomplete(self):
all = self.listAll()
return [ todo for todo in all if not todo['done'] ]

def listComplete(self):
all = self.listAll()
return [ todo for todo in all if todo['done'] ]

A rundown of responsibilities, which could very nicely be written into an Interface, are:
大概的作用,可以很好地写入接口,如下:

  • _extractInfo(item) takes a key and value pair and returns a Python dictionary with some keys that will be useful in the page template we'll use do display this information - the id, a shortcut to the todo item's done attribute, and the todo item itself.
    _extractInfo(item) 得到一对关键字和值组并返回一个 Python 字典,其中包含一些在页面模板有用的关键字,我们将用于显示的信息 - id,todo 项的 done 属性的快捷方式并 todo 项本身。
  • listAll() calls self.context.items() and wraps up the results in _extractInfo(). 'self.context' is the object that this browser view is operating on, which will be an `ITodoList`. Since `ITodoList` is a container, which is a Python mapping, items() returns a sequence of (key, value) pair tuples.
    listAll() 调用 self.context.items() 并将结果包含在 _extractInfo() 中。'self.context' 是该浏览器视图操作的对象,是一个 `ITodoList`。因为 `ITodoList` 是一个容器,是一个 Python 的映射,所以 items() 返回一系列(key, value)对元组。
  • listIncomplete() - Takes the results of 'listAll' and filters out the items that are not done; listComplete does the opposite.
    listIncomplete() - 得到 'listAll' 的结果并过滤出没做的项;listComplete 则正好相反。

All of this using fairly natural Python - there's no extra query language needed beyond what Python already gives, since the database engine itself is persistent Python objects. No SQL queries need to be built. Note that this is fairly basic Python. It could be written using generators, generator comprehensions (Python 2.4), or could even be modified to take advantage of Zope 3's cache components with just a couple of lines of code. However, all of these options only really become useful when optimizing for potentially large lists. In fact, a different view could be written and substituted for this one
所有这些都很自然的使用了Python - 不需要超过 Python 所提供的额外的查询语言,因为数据库引擎本身也是 Python 对象。不需要创建 SQL 查询。注意这是相当基本的 Python。它可以用生成器,推理生成器(Python 2.4)来生成,甚至利用只有二三个的 Zope 3 缓冲组件来修改。然而,这些选项只有当为可能出现的大型列表优化时才会变得有用。实际上,可以写一个不同的视图来代替这个。

Another view object we're going to need is something to add new Todo items with using only a single field, description. The basic adding view for Zope 3 allows for objects to be added to a container without knowing too much information. We know what we want to add - a Todo. So we subclass `'BasicAdding'` with the following implementation:
我们想要的另一个视图对象是使用单个域 description 去新增一个 Todo 项。这个基本的添加视图是为了 Zope 3 允许可以不知道太多信息而将对象添加到容器中。我们知道我们要添加的 - Todo 项。所以我们在下面用子类 `'BasicAdding'` 来实现。

#!python
class TodoAdder(BasicAdding):
""" Allows adding of ITodo items only. Highly specialized. """
def nextURL(self):
"""See zope.app.container.interfaces.IAdding"""
return zapi.absoluteURL(self.context, self.request)

def addTodo(self, description=''):
if not description:
return self.request.response.redirect(self.nextURL())

todo = Todo()
todo.description = description
notify(ObjectCreatedEvent(todo))

self.add(todo)
return self.request.response.redirect(self.nextURL())

'addTodo' is the main meat here, and it's pretty simple. If no 'description' is passed in, we redirect right out of the situation and don't do anything. Otherwise, we directly instantiate a todo.Todo object, set its description, and fire off an event to anything that may be listening. Then we call `BasicAdding`'s 'add' method and redirect off to the next URL (in our case, the absolute url of 'self.context', which again will be the TodoList container object).

`BasicAdding`'s 'add' method doesn't do much, and we probably could have stripped out the code that matters and used it here. Essentially, instead of self.add(todo), we could have done something like:

#!python
# Get an INameChooser implementation for the container
container = self.context
chooser = INameChooser(container)
# The name chooser will generate a key that can be used to
# store / retrieve the todo out of its container.
name = chooser.chooseName('', container)

container[name] = todo

`BasicAdding.add` also checks preconditions, but our current design knows that we're adding a Todo to a `TodoList`, which is the only precondition we have set on this container. `INameChooser` is an adapter which looks at the keys in a container and generates a usable one. By default, this will be a string like 'Todo' or 'Todo-1'. A future post will look at how to generate a name for an ITodo item based on the value of the description attribute.

The last bit of view code we're going to need is one that operates on a Todo item itself. It will respond to checkbox clicks, and will toggle the 'done' attribute's boolean value. Its code looks like this:

#!python
class TodoToggler(BrowserView):
<u>used_for</u> = ITodo

def nextURL(self):
""" Return the next url after toggling is done.  """
todolist = zapi.getParent(self.context)
todolist_url = zapi.absoluteURL(todolist, self.request)
return todolist_url

def toggleDoneAndReturnToList(self):
"""
Toggles the 'done' boolean to true on the context object (an ITodo
instance) and returns to the parent's default view.
"""
# Toggle the 'done' bit
self.context.done = not self.context.done
# Fire off an event signifying change.
notify(ObjectModifiedEvent(self.context))

# Redirect on to the next step.
return self.request.response.redirect(self.nextURL())
  • nextURL() in this case returns the absolute url of the ITodo's parent. We want to return to the `TodoList` when done.
  • `toggleDoneAndReturnToList()` ... well, it should be pretty obvious what it does. Note that it also calls 'modified', which is basically a handy function that calls `notify(ObjectModifiedEvent(self.context))`.

Now what we need to do is define the template. As I was building this, the template and the lister came first, but now that it's all complete, I wanted to get the Python code out of the way to show how simple it is. I have also extracted a common Page Template macro out, which may be a more advanced concept than some readers are familiar with, so I'll cover it quickly. 'METAL' is part of Zope's Page Templates that allows us to write reusable code. The full template for the todo list will use the 'page' macro, and write content into 'slots' in that macro. Inside the todo list, the template will use the following macro to render table rows for the list:

<metal:block define-macro="todolist">
<tr valign="top">
<td>
<input type="checkbox" name="ids:list"
tal:attributes="
onclick string:document.location.href='${item/object/@@absolute_url}/@@toggle';
value item/id;
checked item/done" />
</td>
<td tal:content="item/object/description">todo item</td>
</tr>
</metal:block>

This macro will be used in a loop that uses either listComplete or listIncomplete as defined in one of our view classes above, and operates on item. Each item is the result of _extractItemInfo, which as defined returns a mapping with keys id, done, and object, with object referring to the actual ITodo item.

Notice the @@ characters in the paths. A significant Zope 3 philosophy is that code should be separated from data. Somewhere along the way, this became muddied in Zope 2, with the ability and ease to add in page templates, DTML, and python scripts in the same places in the ZODB as one might put service type objects and data objects. In Zope 3, there are namespaces in URLs that allow access to other objects - skins, virtual hosting, resources, site configuration, and views to name a few. @@ is a shortcut to the view namespace. What item/object/@@absolute_url translates into is the path item/object/++view++absolute_url, which means "get the view named absolute_url for item.object or item['object']". The TAL statement:

onclick string:document.location.href='${item/object/@@absolute_url}/@@toggle';

uses the string expression syntax, returning a string value with ${item/object/@@absolute_url} expanded into the primary URL to accessing a Todo item. After that, in the string, will come @@toggle, meaning that when the checkbox is clicked, the users web browser will go directly to that particular todo item's toggle view. In a moment, we'll be defining these views in ZCML. But basically Zope will query and instantiate the TodoToggler class above and visit the toggleDoneAndReturnToList() method, which will flip the 'done' bit and return to the TodoList's primary view.

Now to that primary view. This is the template that will use the above macro to list todo items. It includes the Completed list, the Incomplete list, and an add form. I put this in the file todo/browser/todolist.pt.

<html metal:use-macro="context/@@standard_macros/view">
<body>
<div metal:fill-slot="body">
<h2>Incomplete Items</h2>
<table class="incomplete" border="0" cellspacing="0" cellpadding="4">
<tal:loop repeat="item view/listIncomplete">
<metal:macro use-macro="context/@@list_macros/todolist"/>
</tal:loop>
</table>

<h3>Add Item</h3>
<form name="addtodo" method="post"
tal:attributes="action string:${context/@@absolute_url}/@@+/">
<input type="text" name="description" size="35" />
<input type="submit" value="Add Item" />
</form>

<h2>Complete Items</h2>
<table class="complete" border="0" cellspacing="0" cellpadding="4">
<tal:loop repeat="item view/listComplete">
<metal:macro use-macro="context/@@list_macros/todolist"/>
</tal:loop>
</table>
</div>
</body>
</html>

In this template, you can see the use of TodoListView methods listComplete and listIncomplete. When we wire this template to the TodoListView class, the instance of that class will be bound to the name view. Like the TodoListView class, context is bound to the object that this view is being used on, which will be an ITodoList object.

And now it comes time to wire it up in ZCML. Remember that ZCML is the language used to register and wire up all of these components for use in Zope. In todo/browser/configure.zcml, start off with:

<zope:configure
xmlns:zope="http://namespaces.zope.org/zope"
xmlns="http://namespaces.zope.org/browser">

<page
name="list_macros"
template="listmacros.pt"
for="todo.interfaces.ITodoList"
layer="todo"
permission="zope.View"
/>

Since we're configuring browser views, we'll use the browser namespace as default. The next directive, 'page', is used in this situation to make the listmacros.pt page template available to ITodoList objects under the name 'list_macros', as is seen above in the todolist.pt code. The list macros are bound to the todo skin layer which was created in the last post.

Now we need to turn 'todolist.pt' into a View object. It's a single page that wants to be bound to the TodoListView class. There are numerous ways of doing this - in Python code, it could be an attribute - an object that's an instance of 'ViewPageTemplateFile'. We could configure TodoListView as the main view object and configure one or more pages inside of it. Or we could configure it as a 'page' type view object using TodoListView. This last route is what we'll use, since we'll get to give the view the name index.html, which Zope has configured as the default view object to use (see zope/app/browser.zcml). Of course, this default view is overridable. There's no magic name or attribute one needs to use like index_html or _q_index. To configure the todo list view, with some security assertions and everything, and bind it to the todo skin, I added the following to the configure.zcml file:

<page
name="index.html"
template="todolist.pt"
for="todo.interfaces.ITodoList"
class="todo.browser.TodoListView"
layer="todo"
permission="zope.View"
allowed_attributes="listAll listIncomplete listComplete"
/>

This is similar to the use of 'page' defined above, but with some extra configuration attributes. Specifying the class here binds this page to the TodoListView class. Some systems might separate these out into separate view/controller objects, but in so many cases, like this one, the so-called 'controller' is so integrated with the 'view' that it just makes sense to keep them close. 'allowed_attributes' explicitly exposes those attributes to the security system. I don't know if that's really needed here, I just saw it in some examples that I lifted this configuration from.

Now we need to configure the Todo adder for this skin. This time, I've actually used a 'view' ZCML declaration:

<view
for="todo.interfaces.ITodoList"
name="+"
layer="todo"
class="todo.browser.TodoAdder"
permission="zope.View"
allowed_attributes="addTodo nextURL"
>
<page name="index.html" attribute="addTodo"/>
</view>

It's not much different than what has been seen so far - the interface declaration (again, allowing anything implementing ITodoList to use this view), the name (how the view is queried), the layer, the class, and the permissions. Inside the view declaration, I bound the name 'index.html' to the 'addTodo' attribute. This means that when Zope 3 traverses to todolist/@@+, it looks up the view named '+', and then looks up the default view for it. 'index.html' is the regular default view name for Zope, so that attribute (in this case, a method) gets published. Again - this keeps funny names out of Python code.

The last view that needs registering is the Toggler for todo items. It's similar to what was just done for the TodoAdder, but bound to ITodo items instead:

<view
for="todo.interfaces.ITodo"
name="toggle"
layer="todo"
class="todo.browser.TodoToggler"
permission="zope.View"
allowed_attributes="toggleDoneAndReturnToList"
>
<page name="index.html" attribute="toggleDoneAndReturnToList"/>
</view>

This time, 'toggleDoneAndReturnToList' is used as the default view within this view, and is what allows traversal to todolist/MyTodo/@@toggle in a URL. Finally, the configure.zcml file is closed out with a

</zope:configure>

Now, in the primary configuration file for 'todo', which is 'todo/configure.zcml', add the following line near the bottom of the file:

<include package=".browser"/>

On using the skin and these views - nothing has been done to set up a view to add and list 'todolist' objects in this skin. In my limited experiments, I had the Rotterdam ZMI configured as my primary skin (the default), and had added a TodoList using its user interface named 'mytodo'. To view 'mytodo' under the custom skin without configuring a new default skin, or site, or anything to that nature -- basically just wanting to see the results of this work -- I traversed to a URL with the ++skin++ declaration on the URL path:

http://localhost:8080/++skin++todo/mytodo/

The results, with some todo items, looks like this: todo_skin.jpg

Clicking on any of the checkboxes triggers the javascript that goes to the @@toggle view, which redirects back to this list after toggling the item. After toggling, the Todo item is (naturally) in the other list.

That's about all it takes to get a really really basic todo application with its own UI up and running. There's more that can be done which I hope to get to soon. But the Python code and even the template code are pretty simple, and the ZCML to wire it all up isn't all that bad.

This is still a very basic application. Zope 3 is capable of much more, and can be configured and reconfigured as needed to get larger jobs done. The main thing that I wanted to demonstrate is that Zope 3 is really quite clean and nice to work with. The only part that people might have issue with is ZCML. But I plan on addressing that issue shortly (in short - for all people ballyhoo about keeping 'content and presentation separate! business logic away from display logic!', I think a lot of people punt on configuration - especially configuration in a way that allows loose coupling and easy use/reuse - and mix configuration and registration code too freely. ZCML style configuration is one way of avoiding the troubles that can come up in that system).