编写新的内容对象 难易程度: 初学者 技能要求: 开发人员应该具备Python的编程经验,需要了解一些面向对象的基本概念以及基于组件编程的基本原理; 对模型(schema)和接口程序包有一些了解。(可选) 问题/任务: 在Zope 3中实现新的内容对象对Zope3开发人员来讲其重要性不言而喻。为了让讲解更加具有可操作性,本章将使用一个留言簿范例来帮助大家理解。下面一章,我们将列出在Zope3中实现和注册一个新内容组件所需的主要步骤。 解决方案: 这章将是我们开发一个MessageBoard类型的一个起点。 在任务开始前,我们认为您已经安装好了Zope 3 , 还有您也已经具备了Python的编程技能,当然更重要的一点就在于您非常乐意花一些时间学习Zope框架。 第I步:准备工作 在开始之前,我们需要确信您已经做了如下工作: 您已经安装了Zope 3; 已经创建了principal.zcml文件(该文件通常位于您所创建的ZOPE3实例中的etc目录); 已经成功启动了Zope3。 如果上面工作都做了,那就让我们切入正题吧! 相对于Zope2, Zope3没有要求您必须在指定目录中添加程序包,您可以选择您认为方便的地方来添加它。最好的地方就是把它放到ZOPE3/src (ZOPE3 是您的Zope3安装目录)目录中, 因为这样的话,我们就不用担心和PYTHONPATH相混淆了。 另外,为了让书中所演示的内容显得更加整洁清晰,我们把所有的程序代码都放进一个名为book的程序包。 在Unix中,用下面的命令创建book 目录: 1 mkdir ZOPE3/src/book 把一个空的__init__.py文件放到新建的目录里,在Unix中,您可以使用如下命令来完成该工作: 1 echo "# Make it a Python package" >> ZOPE3/src/book/__init__.py 不过,您当然也能使用文本编辑器来创建__init__.py,但是您需要确信该文件中的Python 程序代码是正确的。通常,文件中应该至少包含一些空格,因为空文件常常搞乱一些存档文件。 现在我们又以类似方式在book里面创建另一个messageboard的程序包(不要忘记创建init.py 文件)。从现在开始,我们下面需要进行的所有工作都只需在messageboard包里进行,这个包位于ZOPE3/src/book/messageboard。 您可以通过如下地址下载我们所做的每一步的源码: 说明:其中包含许可,为了减少输入,我们在整个源程序中都省略了与许可有关的信息,但这并不代表这些源程序没有版权,实际上,版权仍然存在于这些源文件之中。 第II步:初步设计 就像前面我们所提到的,我们的初衷就是需要开发一个基于Web的留言簿应用程序,程序的界面并不要求十分漂亮,但功能则要求相当完备。 具体设计是,根对象MessageBoard可以容纳来自不同用户的记录或消息(Message)对象。另外,由于我们想让用户对不同的消息回复,因此我们就需要允许消息包含回复。 那就意味着我们有两个基于容器的组件: MessageBoard只包含能被添加到任何文件夹的消息或那些希望能包含消息的容器。MessageBoard应该有一个对主题讨论的描述(description)。 另一方面,Messages(消息)只被留言簿或其他消息包含。每一个消息都将有标题(title)和正文(body)。 设置应该包含我们制作这个可用对象所必需的东西。稍后我们还将联合许多其它与这些组件有关的元数据,并把他们更好的集成到zope3中,而且还将增加相关的辅助功能。 第III步:编写接口 编码阶段的第一步就是定义那些能代表您外部API函数的接口程序。您应该有这样的意识,那些建立在您程序包上的软件接口程序期望能够按照您指定的方式运行。尽管对于方法属性和参数这显得无足轻重,但是太多的开发人员常常忘记指定方法的预期返回值,或者忘记编写捕捉异常的功能模块。 在ZOPE3中,接口被一起存放在接口模块或程序包中。由于我们的程序包不是很大,我们准备使用一个基于文件的模块;因此在文本编辑器中编辑一个名叫interfaces.py的文件。 在程序开始,我们只是为留言簿本身(message board)定义一个接口,为单个消息(message)定义一个接口,代码如下(在interfaces.py文件中添加这些代码): 1 from zope.interface import Interface 2 from zope.schema import Text, TextLine, Field 3 4 from zope.app.container.constraints import ContainerTypesConstraint 5 from zope.app.container.constraints import ItemTypePrecondition 6 from zope.app.container.interfaces import IContained, IContainer 7 from zope.app.file.interfaces import IFile 8 9 10 class IMessage(Interface): 11 """A message object. It can contain its own responses.""" 12 13 def __setitem__(name, object): 14 """Add a IMessage object.""" 15 16 title = TextLine( 17 title=u"Title/Subject", 18 description=u"Title and/or subject of the message.", 19 default=u"", 20 required=True) 21 22 body = Text( 23 title=u"Message Body", 24 description=u"This is the actual message. Type whatever you wish.", 25 default=u"", 26 required=False) 27 28 29 class IMessageBoard(IContainer): 30 """The message board is the base object for our package. It can only 31 contain IMessage objects.""" 32 33 def __setitem__(name, object): 34 """Add a IMessage object.""" 35 36 __setitem__.precondition = ItemTypePrecondition(IMessage) 37 38 description = Text( 39 title=u"Description", 40 description=u"A detailed description of the content of the board.", 41 default=u"", 42 required=False) 43 44 45 class IMessageContained(IContained): 46 """Interface that specifies the type of objects that can contain 47 messages.""" 48 __parent__ = Field( 49 constraint = ContainerTypesConstraint(IMessageBoard, IMessage)) 50 51 52 class IMessageContainer(IContainer): 53 """We also want to make the message object a container that can contain 54 responses (other messages) and attachments (files and images).""" 55 56 def __setitem__(name, object): 57 """Add a IMessage object.""" 58 59 __setitem__.precondition = ItemTypePrecondition(IMessage, IFile) ? 第1行:导入Interface类。任何在其继承路径上包含该类型的对象都是一个接口,而非常规类。 第2行:一个对象的属性(attributes)和性质(properties)通过fields来描述。Fields包含关于被使用属性的元数据,它和其他东西一起被用来进行值的校验、创建自动生成的输入表单。绝大部分的fields在zope.schema包中定义。更详细的信息和fields的完整列表参见"Zope Schemas and Widgets (Zope模型和部件)"。 第4行:ContainerTypesConstraint条件让我们告诉系统,对象可以被添加到哪种容器中。例如,message只能被message board和被另一个message包含(当它作为父message的回复信息时)。用法如下文所示。 第5行:ItemTypePrecondition是容器类型限制条件相对立的限制条件,它可指定能够被某个容器包含的对象类型。用法如下文所述。 第6行:提供IContained接口的对象能被包括在Zope对象树中。我们在这里也导入了IContainer接口。该接口在第29行和第52行中被用作基接口。IContainer接口定义了让对象被Zope3识别为容器所有需要的方法。 注意:我们并没有直接继承Interface,因为 IContainer已经继承了,它会自动让IMessageBoard成为一个对象。 第10行:可能您已经注意到所有接口前面的"I",它是"Interface"的简写。这是Zope的约定,这样我们就不会将接口和类混淆起来,因为这两种不同的对象类型是不能够以同样的方式来使用的。 总的来说,message是具有标题(title)和正文(body)的简单对象,仅此而已。我们稍后会通过另外的接口和元数据来声明更多的语义。 第16-20行:是message的简单标题/主题的标题行。注意我们使用TextLine字段而不是用Text字段,这样就不能在其中插入换行符。因此标题相对较短,而且也能按照标题所需要的方式来进行显示。 第22-26行:正文(body)是message的实际内容。注意我们没有限制其长度,如果您担心您的留言簿被垃圾信息填满,您可能就需要设置其长度。 第33 -36行:我们不允许在message board中添加任何其他内容类型。实际上,我们只需要能够加入IMessage对象就好了。因此,我们为messageboard接口的 __setitem__()方法声明一个先决限制条件。只需要简单地把所有允许的接口的列表作为ItemTypePrecondition构造器的参数就可以了。 注意:尽管IContainer已经定义了__setitem__()方法,我们在这里还必须声明它,这样它就被限定在这个接口的作用范围之内,只针对IMessageBoard;否则,所有ICOntainer对象都会有这个限制条件。 第38-42行:对包含message board的描述信息(description)的接口声明一个属性。这个属性是一个典型的Text字段。值得一提的是:请注意,我们总是用unicode字符串(这是贯穿Zope3要求的一项约定)来表示易理解的文本,Zope3需要解决的主要问题之一就是国际化,unicdoe字符串是支持多语言应用的首要条件。 第45-49行:接口通过字段限制条件来描述哪种内容类型可用于包含message。显然,message board可以包含messages, 但是message也要能够包含其他messages(为回复信息),我们通常直接在主内容类型接口的父接口上指定该项限制(例如:IMessage), 由于该限制显示地引用IMessage,所以我们要等到该接口被定义完毕后才进行此项定义。 第52-59行:我们还想让message作为容器,这样它才能包含回复消息和附件。但是,我们不要其他任何对象被加入message,因此我们像 IMessageBoard接口哪样添加一个先决限制。同样地,我们必须在一个独立的接口中做此项工作,因为我们在该条件里引用到了IMessage。 第四步:编写单元测试 Zope 3中有两种方式来编写单元测试。第一种是通过使用专门unittest包的TestCase类,这是借鉴JUnit构建的测试模型。第二种技术是在doc strings中编写单元测试,这种方式常被称为doc tests。 在今后的Zope3 的开发中,文档测试(doc tests)将成为编写单元测试的编写方法。要了解这两种技术的详细信息,请参见"编写测试"章节,特别是"编写基础单元测试"和"Doctests:案例驱动的单元测试"章节。 通用的单元测试在需要重用抽象测试时具有很大的优点,我们把它作为各种容器测试的范例。因此,我们将采用单元测试进行容器测试,而对其他情况采用文档测试(doc tests)。 首先,在messageboard包中创建一个tests的包。注意:在Zope3中,按约定将此测试模块称为tests(基于文件的测试模块应当命名为tests.py),这样自动测试运行器就会运行该测试。 下一步,开始编辑test_messageboard.py文件,并插入以下代码: 1 import unittest 2 from zope.testing.doctestunit import DocTestSuite 3 4 from zope.app.container.tests.test_icontainer import TestSampleContainer 5 6 from book.messageboard.messageboard import MessageBoard 7 8 9 class Test(TestSampleContainer): 10 11 def makeTestObject(self): 12 return MessageBoard() 13 14 def test_suite(): 15 return unittest.TestSuite(( 16 DocTestSuite('book.messageboard.messageboard'), 17 unittest.makeSuite(Test), 18 )) 19 20 if __name__ == '__main__': 21 unittest.main(defaultTest='test_suite') 这里我们会遇见一些非常酷的东西。让我们仔细看看这些代码: 第1行:Python自带的unittest模块用来创建测试套件。 第2行:Zope提供了一个专用的DocTestSuite,该测试套件把文档测试(doc tests)集成到通用的单元测试框架中,并且使得文档测试可以通过测试运行器来运行。 第4行:免费的测试件总是好的,我们为容器导入一些基本测试。 第9-13行:定义容器测试。我们只需要提供要测试容器的实例作为makeTestObject()方法的返回值就可以了。 第15-19行:test_suite()方法收集所有已经定义的测试案例,并将他们编译进一个测试套件中。该方法必须按这样进行命名,这样测试运行器才能够识别到该套件。除了这个容器测试件以外,我们同时还注册了文档测试件(doc tests)。 第21-23行:我们还要让所有的测试模块能够自己执行。在此,我们只要告诉测试运行器执行所有通过test_suite()返回的测试套件的所有测试。在Zope3的任何测试模块中,这几行是通用的模板。 现在,我们要为IMessage组件编写第二个测试模块。我们把test_messageboard.py拷贝成test_message.py,然后把新文件改为: 1 import unittest 2 from zope.testing.doctestunit import DocTestSuite 3 4 from zope.app.container.tests.test_icontainer import TestSampleContainer 5 6 from book.messageboard.message import Message 7 8 9 class Test(TestSampleContainer): 10 11 def makeTestObject(self): 12 return Message() 13 14 def test_suite(): 15 return unittest.TestSuite(( 16 DocTestSuite('book.messageboard.message'), 17 unittest.makeSuite(Test), 18 )) 19 20 if __name__ == '__main__': 21 unittest.main(defaultTest='test_suite') 这两个测试模块没有任何本质的区别,因此我就不再逐点列出同样的代码说明了。 注意这些测试都还没有处理具体的实现细节,因为我们还不知道具体的实现细节会是什么样子呢。就像我们使用SampleContainer基础测试件一样,这些测试件还可以被其他包使用,原因在于这些测试仅仅依赖于API。当然,通常来说,测试应当涵盖具体实现的行为。 第五步:内容组件的具体实现 现在,我们终于准备好要实现这个软件包的内容组件了。这是本章节的核心部分。但是我们如何知道我们究竟应当诠释哪些方法和属性呢? 在ZOPE3/utilites中有一个名为pyskel.py的好工具,它能够生成一个框架。进入ZOPE3/src,然后键入: 1 python2.3 ../utilities/pyskel.py \ 2 book.messageboard.interfaces.IMessageBoard 预计的输出结果显示如下。该工具检查给定的接口并创建该接口的实现类的框架。它还会递归进入所有基础接口,并获取他们的方法。以下是生成的代码: 1 from zope.interface import implements 2 from book.messageboard.interfaces import IMessageBoard 3 4 class MessageBoard: 5 __doc__ = IMessageBoard.__doc__ 6 7 implements(IMessageBoard) 8 9 10 def __setitem__(self, name, object): 11 "See book.messageboard.interfaces.IMessageBoard" 12 13 # See book.messageboard.interfaces.IMessageBoard 14 description = None 15 16 def __getitem__(self, key): 17 "See zope.interface.common.mapping.IItemMapping" 18 19 def get(self, key, default=None): 20 "See zope.interface.common.mapping.IReadMapping" 21 22 def __contains__(self, key): 23 "See zope.interface.common.mapping.IReadMapping" 24 25 def __getitem__(self, key): 26 "See zope.interface.common.mapping.IItemMapping" 27 28 def keys(self): 29 "See zope.interface.common.mapping.IEnumerableMapping" 30 31 def __iter__(self): 32 "See zope.interface.common.mapping.IEnumerableMapping" 33 34 def values(self): 35 "See zope.interface.common.mapping.IEnumerableMapping" 36 37 def items(self): 38 "See zope.interface.common.mapping.IEnumerableMapping" 39 40 def __len__(self): 41 "See zope.interface.common.mapping.IEnumerableMapping" 42 43 def get(self, key, default=None): 44 "See zope.interface.common.mapping.IReadMapping" 45 46 def __contains__(self, key): 47 "See zope.interface.common.mapping.IReadMapping" 48 49 def __getitem__(self, key): 50 "See zope.interface.common.mapping.IItemMapping" 51 52 def __setitem__(self, name, object): 53 "See zope.app.container.interfaces.IWriteContainer" 54 55 def __delitem__(self, name): 56 "See zope.app.container.interfaces.IWriteContainer" 输出的结果已经非常好了,不过还需要另外一些内容;例如我们需要继承BTreeContainer基础组件,这样我们就不需要实现来自IReadMapping、IEnumberableMapping、IReadMapping、IItemMapping和 IWriteContainer接口中的方法。 打开一个名为messageboard.py的新文件进行编辑。包含文档测试(doc tests)的message board的实现如下所示: 1 from zope.interface import implements 2 from zope.app.container.btree import BTreeContainer 3 4 from book.messageboard.interfaces import IMessageBoard 5 6 class MessageBoard(BTreeContainer): 7 """A very simple implementation of a message board using B-Tree Containers 8 9 Make sure that the ``MessageBoard`` implements the ``IMessageBoard`` 10 interface: 11 12 >>> from zope.interface.verify import verifyClass 13 >>> verifyClass(IMessageBoard, MessageBoard) 14 True 15 16 Here is an example of changing the description of the board: 17 18 >>> board = MessageBoard() 19 >>> board.description 20 u'' 21 >>> board.description = u'Message Board Description' 22 >>> board.description 23 u'Message Board Description' 24 """ 25 implements(IMessageBoard) 26 27 # See book.messageboard.interfaces.IMessageBoard 28 description = u'' 第1行:implements()方法用来声明某个类实现了一个或多个接口,详见"接口介绍"。 第2行:与容器相关的所有东西都在zope.app.container中,BTreeContainers是IContainer接口的一个非常高效的实现,通常被用作其他容器对象的基类,例如message board的基类。 第7-24行:该类docstring的目的就是对为该类编写文档。按照Python文档的标准,所有的docstrings应当使用re-structured text格式。文档测试(doc tests)被认为是编写文档,因此应当按照某种叙述性的风格进行编写。 第12-14行,我们验证该MessageBoard组件实现的确实是ImessageBoard,这个验证函数会对该对象存在的指定属性和方法进行实际的检查。 第18-23行只是给出关于缺省描述值以及如何进行设置的演示。这个测试看起来微不足道,但是有时您可能需要使用property来修改这个描述(description)属性的实现并且这个测试应当仍然能够通过。 第25行:在此,我们告诉该类,它实现的是IMessage。该函数调用看起来有点神秘,因为可能有人会困惑这个函数是如何知道哪个类要指派给这个接口。告诉有兴趣的读者,它使用的是sys.getframe()。 第27 -28行:将描述(description)作为一个简单的属性。 注意:Python在这种方式下非常独特。在几乎所有的面向对象语言(例如Java)中,有人可能已经编写了一个 accessor(getDescription())和一个mutator(setDescription(desc))方法。但是,Python的属性和特征能够使这些可有可无,这样也使得代码更加简洁。 下一个任务是写消息对象,差不多是相同的代码。 因此我们将不在这里列出它,您可在 中去找到相关代码。 唯一不同的是,在该情况下消息组件一定要实现 IMessage, IMessageContained 和 IMessageContainer。 第六步:对实现进行单元测试 当功能实现完成后,我们需要确保所有的测试通过。这儿有一个名叫test.py的代码文件将运行所有的或您唯一指定的测试。为了运行测试检测您的实现,在Zope3根目录下面执行下面这行命令: 1 python2.3 test.py -vpu --dir src/book/messageboard -V选项:现在正在运行中的测试被显示; -P选项:允许我们观看测试运行的进度; -u选项:告诉测试者仅进行单元测试。 对于所有可使用的运行脚本选项列表可以通过那- h参数获得相关帮助信息。 您应该看到26个测试通过。测试结束时的输出结果应该象这样: 1 Configuration file found. 2 Running UNIT tests at level 1 3 Running UNIT tests from /opt/zope/Zope3/Zope3-Cookbook 4 26/26 (100.0%): test_values (....messageboard.tests.test_messageboard.Test) 5 ---------------------------------------------------------------------- 6 Ran 26 tests in 0.346s 7 8 OK 一些测试很可能失败或者测试套件由于语法错误甚至不运行。这完全是正常的,同时这也是我们写测试的理由,继续修改存在的问题直到所有的测试都通过为止。 第七步:注册开发好的内容组件 现在我们已经开发了我们自己的组件,告诉Zope3该如何与他们互动是必需的。通用的方法就是用Zope自己的配置语言ZCML。这个配置文件按约定被存放在configure.zcml文件中。我们现在开始编辑这个文件而且增加如下的ZCML代码: 3 4 8 9 10 13 16 20 24 28 29 30 34 35 36 39 42 46 50 54 55 56 ]]> 第1-2行:配置文件使用XML,在ZCML文件中所有的配置必须有配置元素组成。在配置元素的最开始,我们列出所有我们将要使用和定义的缺省ZCML命名空间。当前情况下我们仅需要一个普通的zope命名空间。在接下来各章中,随着我们开发新功能的日益增多,您将接触到更多的命名空间。 第4-7行:对接口分类有时候是必要的。其中type将指定哪个接口为Zope3提供内容类型。zope:interface指令用来指派接口上的这些类型。另外需要考虑的是接口就是组件,并且组件能够提供其它接口。 第9-28行:zope:content指令以内容组件注册了一个MessageBoard类。这个元素总是只有一个attribute 、class、使用虚Python路径指向组件的类。 第10-12行:为了让对象有一个创建和修改日期以及其它元数据(例如都柏林核心集),我们需要告诉系统这个对象能和它本身联合注解。这虽不是必需要求,但确是一个良好的习惯。请参看"使用注解存储元数据"获得更多的信息。注解存储被被当作元数据的附加数据,因为它对于对象本身的正确功能是不必需的数据。然而,元数据能让一个对象能够更好的与系统集成。同样,注解也被大量的用在了Zope 3中。 通常,zope:implements子指令允许您在一个类上声明新的应用接口。这完全同于Python中的classImplements(Class,ISomeInterface)。但我们为什么想使用ZCML代替Python声明接口呢?原因之一,使用Python方式会弄乱Python代码,并且使组件的实际功能发生了转移。同时,当处理第三方Python软件包时候,我们并不想接触这些代码,但仍想对这些对象做声明,使用ZCML方式就能够使他们不需要对代码做任何修改就能在Zope 3里使用。 通常通过ZCML声明的只有"marker interfaces(标记接口)"没有方法或属性,因此,接口实现不需要附加的Python代码。 标记接口:普通的接口的通常目的是保证类实现了某个或某组方法。而标记接口没有方法。 第13-15行:IcontentContainer接口是另一个标记接口的实例。它声明的全部内容是:容器在内容空间包含普通内容,这在我们的留言簿中得到了清晰的体现。 容器:一个为组件提供生命周期管理、安全、配置和运行时服务的实体。 第16-19行:zope:factory子指令允许我们为MessageBoard组件注册一个名叫book.messageboard.MessageBoard的工厂。 每个工厂需要一个id(第一个指令参数)来标明哪个工厂能被接入和执行。然而,您没被硬性要求一定要指定一个id;如果没有指定,zope:content类属性文字字符串值将被使用,本例中将是messageboard.MessageBoard。 zope:factory指令也支持两个易读的信息串,标题(title)和描述(description),它们被用于用户界面。 第20-27行:在Zope 3 中分为信任和不信任环境。信任环境没有安全或者能轻易绕过安全。此例是就是基于文件的Python代码,属于总是被信任。而不信任环境恰恰相反,在不信任环境中,安全每时每刻都存在于系统中。所有的Web和FTP事务都被认为是不可信任的。 当然,我们想通过Web使用我们的留言簿,因为这是Zope 3缺省的的用户接口。为了让它可用,我们必须为我们的组件指派最小的安全声明。做安全声明使用zope:require和zope:allow指令。 require指令通常表示开始指定一个权限。然后我们必须决定我们想保护这些声明。下面是您的选择: 如果用户有指定的权限,Attributes允许我们指定能被访问的属性和方法(注意方法就是被调用的属性)。 set_attributes允许您指定能被修改或变化的个别属性。注意您不应该在这儿列出任何的方法,否则别人可以插人恶意代码覆盖其中的方法。 如果您使用interface属性指定一个或多个接口,指令将自动选取所有的这些接口声明的方法和属性并且授访问权限给它们。 当您使用set_schema属性指定一组模式(schemas)时,所有在它里面定义的属性都被授予了修改权限。在模式(schema)中列出的方法将被忽略。 注意:正如您所期待的,ZCML中列表记号是以空格分开而不是用逗号。 like_class属性略微不同上述选择,它必须是不需要任何的许可就被指定。如果用, 它只是把所有的安全声明从指定的类传递到装入安全声明zope:content指令里指定的类。当前我们的MessageBoard组件,指令的用法如下: ]]> 这儿的MessageBoard将只是继承(inherit)有益于消息组件的安全声明。 第二个指令zope:allow也可以操作任何一组属性或接口。每个人都可以访问所有经过指定的属性。这与要求某些人有zope.Public权限是等价的,每个主体(principal)访问该系统会自动拥有。 现在很容易就可以解释这个两个安全声明的意思。如果用户有zope.ManageContent权限,我们基本上给予了ImessageBoard接口了读和写入权限(包括了所有的Icontainer方法和description属性)。 第 30 行-54行: 这与上面的消息内容组件相同。 第八步:配置基本视图 尽管内容组件现在已经被注册了, 可程序看上去并不是那么吸引人,原因在于这里只存在通过程序方式增加和编辑新组件,而普通用户则需要通过用户界面来操作。因此,我们将着手定义通过以浏览器界面方式接入的基本视图。 首先在messageboard包里创建一个browser的包(不要忘记了__init__.py文件),在该包下面新增一个配置文件configure.zcml,在该文件中插入如下内容: 3 4 12 13 20 21 29 30 36 37 45 46 53 54 63 64 70 71 ]]> 第2行:在这个配置文件我们不使用zope, 因为我们想要配置browser包的具体功能。并且注意browser包是缺省的命名空间, 我们的指令不需要命名空间前缀。在这本书里常提到命名空间是http://namespaces.zope.org/。 第4-11行:为Message Board注册一个自动生成"Add form"。 第5行:这个标签是显示在屏幕顶部的小文本。 第6行:这个视图的名称,名称字符串实际上是URL的一部分。 第7行:Schema定义将被用于产生表单,Schema中的字段将为创建有实际意义的表单元素提供所必需的元数据。 第8行:内容工厂(The content factory)用于创建新的内容组件。 第9行:fields是被显示在表单里的字段名称列表,它允许您创建基于schema的字段子集的表单并且可以改变字段在表单的顺序。 第10行:指定能创建和新增内容组件所需要的权限。 第13-19行:当创建了一个新增视图后,我们现在必须以browse:addMenuItem指令注册新增菜单。标题(title)显示在菜单的条目上,重要的是视图必须与新增表单的名称相匹配。 第21-28行:声明一个编辑表单(edit form)的工作与定义一个新增表单(add form)非常相似,最主要的不同之处在于我们不需要指定专门的内容工厂(content factory),因为相关的内容组件已经存在。 For属性为组件的编辑表单(edit form)指定接口。所有的视图指令(除开browser:addform)都要求for属性。如果您愿意为一个具体的实现注册一个视图,您也能为for属性指定类。 在27行我们可以看见我们用菜单和标题属性直接为编辑视图(edit views)指定菜单,zmi_views菜单是在缺省的web界面创建tabs。它包含指定对象的所有视图。 第30-35行:Message board是容器并且将使用browser:containerViews指令快速注册所有必要的与容器相关的视图,这个指令非常灵活并且稍后您可以编写规则视图代替它。 第37-69行:这些确切地说是相同的指令, 不过是针对IMessage 接口。 为了能让系统知道视图的配置, 我们需要参考配置文件messageboard/configure.zcml。 也包括视图配置, 增加如下信息: ]]> 第九步:注册软件包 我们现在已经有了一个完整的包,然而比起Zope2,您必须明确地注册一个新包,那意味着您必须集成组件到Zope3,接下来您需要在ZOPE3/package-includes中创建名为messageboard-configure.zcml的新文件。这个文件名称不是任意的,必须是形式为*-configure.zcml的文件,这个文件应该包含如下指令: ]]> 当Zope3 启动时,它将遍历在这个目录中的每个文件并且在每个文件中执行ZCML指令,通常这些文件就指向包的配置中。 第十步:测试内容组件 恭喜恭喜! 您已经完成Zope3 的一个小应用, 尽管小但您仍需要花些时间在这上面。现在是您付出辛勤劳动后收获成功果实的时候。请启动您的Zope3服务器,最好从Zope3根目录运行makerun.在使用时,如果由于Python版本而引起一些问题,请编辑Makefile并且进入Python正确的路径执行。其它错误有可能是由错别字或错误的配置而引起的。ZCML翻译器将以Emacs-friendly格式显示错误指令所在的行号和列号。请试着反复启动Zope3直到您已经修正了所有的错误并且Zope3能正确运行您的成果。 1 ------ 2 2003-12-12T23:14:58 INFO PublisherHTTPServer zope.server.http (HTTP) started. 3 Hostname: localhost 4 Port: 8080 5 ------ 6 2003-12-12T23:14:58 INFO PublisherFTPServer zope.server.ftp started. 7 Hostname: localhost 8 Port: 8021 9 ------ 10 2003-12-12T23:14:58 INFO root Startup time: 11.259 sec real, 5.150 sec CPU 注意您可能会得到一些国际化警告,不过暂时您可以忽略它。 一旦服务器已经运行,请使用您的浏览器运行如下地址: 这时将会弹出一个要您输入用户名和密码的认证框,用户将在principals.zcml文件中列出。如果您未增加任何特别用户, 请使用gandalf作为登陆名、123作为密码。认证结束后您将进入到Zope3的web用户界面。现在您可以看到"Message Board"条目。 您可以自由增加和编辑message board对象 一旦您创建了一个message board,您能单击它以及进入它。现在注意了, 系统只允许您在这儿添加Message对象。之所以选择有限是由于我们在接口中的指定了条件。另外,缺省视图是编辑表单(Edit form),该表单允许您改变留言簿描述(description)。第二个视图允许您管理留言簿中的消息内容。 先增加一个Message,一旦您增加了一则消息, 它将出现在Contents视图中。 然后您能单击查看您添加的消息。系统允许您修改消息数据以及增加新消息(回复)。 好了,我们就此打住,您现在能创建一棵完整的留言簿树并且也能通过Web界面来访问该留言簿。 不过,这儿仍然存在着一些需要您来修正的错误。经常会是安全问题,这也许能使查找的范围变得小些。不幸地是NotFoundError 通常被转换成ForbiddenAttributeError, 因此如果您看到这种问题后请小心行事。 注:其它缺陷是标准的错误界面不显示traceback 。 然而,为了让界面调试变得更为得心应手, 可以用代替,代替后HTML 和traceback 将被显示。 如果您在您的包内做了数据结构方面的变动, 在objects/components中删除旧的实例就变得很有必要了,不过,有时光删除实例还不够,甚至您还必须删除父文件夹或者最好是删除数据库文件Data.fs (ZODB)。这些都是更新一个新版本对象可以采取的一些有效的方法。不过最好是在开发期间列出所有的方法,这样就会使得开发过程更加简单和快速。 下面地址提供了该示例的代码: