Zope3宝典/映射对象到文件系统

来自Ubuntu中文
跳到导航跳到搜索

Chapter 22: Object to File System mapping using FTP as example (第 22 章:使用FTP为例介绍对象到文件系统的映射)


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

原文作者:StephanRichter, zope.org

授权许可:创作共用协议

翻译人员:

校对人员:Leal

适用版本:Zope 3

文章状态:校正阶段



Difficulty(难度)

Newcomer (初学者)

Skills(技能)

  • Be familiar with the Message Board Demo package up to this point.
    熟悉留言薄演示包.
  • Good understanding of the Component Architecture, especially Adapters.
    对组件架构有很好的理解,尤其是适配器.
  • Feel comfortable with writing ZCML-based configuration.
    熟练编写基于 ZCML 的配置.
  • Basic knowledge of the filesystem interfaces. Optional.
    文件系统接口的基本知识. 可选。

Problem/Task(问题/任务)

Zope provides by default an FTP server , which is a filesystem based protocol. This immediately raises the question about the way objects are mapped to the filesystem representation and back. To accomplish the mapping in a flexible and exchangeable way, there exists a set of interfaces that can be implemented as adapters to provide a representation that the FTP Publisher understands. This chapter shows how to implement some of the interfaces for a custom filesystem representation.
Zope 缺省提供了 FTP 服务,一个基于文件系统的协议。这就立即引发了一个关于对象到文件系统表示映射及反向映射的方式。为了能以一种灵活且可替换的方式来完成映射,有一系列的接口能够作为适配器被实现,用来提供一种能让 FTP 发布者理解的表示。本章展示了如何为自定义的文件系统表示实现一些接口。

Solution(解决方案)

At this point you might wonder: “Why in the world would we have to write our own filesystem support? Is Zope not providing any implementations by default?” Well, to answer the latter question: Yes and no. There is an adapter registered for IContainer to IReadDirectory and IWriteDirectory. However, they are not very useful, since our Message Board and Message objects are not only containers but also contain content that is not displayed anywhere. Just start Zope 3 and check it out yourself. Thus it will be the goal of this chapter to create a representation that handles the containment and the content at the same time.
有一点你可能比较迷惑:“到底为什么我们必须要写自己的文件系统支持呢?Zope 不能缺省提供一些实现吗?” 好吧,对于后一个问题的回答是:能也不能。是有一个适配器将 IContainer 注册到 IReadDirectory 和 IWriteDirectory。然而,它们并不是很有用,因为我们的留言薄和消息对象不仅是个容器而且它还包含了无论哪里都无法显示的内容。只需启动 Zope 3 并自己测试一下。因而创建一个表达方式以便可以同时处理包含和内容将是本章的目标。

Since the core has already a lot of code presenting IContainer implementations as directories, we should reuse this part of the framework. The content of an object could be simply represented by a virtual file called contents, which contains all relevant data normalized as simple plain text. Note also that we will not need to have a complete mapping between the object and the filesystem representation, since we do not need or want to expose annotations for example. I suggest that the contents of the MessageBoard object simply contains the data of the description attribute and for the Message I propose the following format:
因为核心已经有许多代码提供了 IContainer 作为目录的实现,所以我们应该复用框架的这部分。对象的内容可以简单地通过虚拟文件调用内容来表示,它包含了所有的以纯文本方式表示的相关数据。同时请注意我们并不需要一个已经完成的对象和文件系统表示之间的映射,例如我们并不需要与不想使 annotation 可见。我建议留言薄对象的内容只是简单地包含描述属性的数据,并且我提议消息用以下格式表示:

1  Title: <message title>
2  
3  <message body>

This way we can also parse easily the title when new contents is submitted, since we want to implement the read and write interfaces of the filesystem representation. One of the main goals is to keep the VirtualContentsFile class as generic as possible, so that we can use it for both message boards and messages. To do that the virtual file must delegate the request to create the plain text representation to a component that knows about the specifics of the respective content object. For this task, we will have a simple IPlainText adapter that provides the plain text representation of each content component’s contents.
通过这种方式我们可以在新内容提交时很容易地分析标题,因为我们想实现文件系统表示的读写接口。主要目标之一是尽可能保持 VirtualContentsFile 类的通用性,以便我们可以在留言薄和消息中使用它。为了做到这一点,虚拟文件必须发送请求创建纯文本表示到组件,以便了解各个内容对象特性。为了这个任务,我们将使用一个简单的 IPlainText 适配器以便提供每个内容组件内容的纯文本表示。

22.1 Step I: Plain Text Adapters(22.1 步骤 I: 纯文本适配器)

As said above, IPlainText adapters are responsible to return and accept the plain text representation of the object’s content and just do the right thing with the data. They are very simple objects, having two methods, one for providing and one for processing the text.
如上所述, IPlainText 适配器是负责返回和接受对象内容的纯文本表示和just do the right thing with the data. 它们是非常简单的对象,有着两个方法,一个负责提供文本而另一个负责处理文本。

22.1.1 (a) The Interface(22.1.1 (a) 接口)

The interface is simple, it defines a getText() and a setText() method:
接口是简单的,它定义了一个 getText() 方法和一个 setText() 方法

#!python
class IPlainText(Interface):
"""This interface allows you to represent an object's content in plain
text."""

def getText():
"""Get a pure text representation of the object's content."""

def setText(text):
"""Write the text to the object."""

This interface should be placed in the interfaces module of the messageboard. In the doc strings I refer to the object’s “content” without further qualifying it. With content I mean all data (property values) that should be represented via the plain text representation.
该接口将代替留言薄的模块接口。在 doc strings 中所讲的对象的 “内容”是再合适不过的了。我所讲的内容是指那些所有可以用纯文本表示来表示的数据(属性值)。

The setText() method could in theory become quite complex, depending on how many properties you want to map and how you represent them. You will see that in our two cases it will be still fairly simple.
setText() 方法从理论上讲根据你有多少属性要映射以及你要如何表示它们,是可以变得相当复杂的。但在我们的两个例子中你可以看到它仍然是相当的简单。

22.1.2 (b) The Implementation(22.1.2 (b) 实现)

We need two implementations, one for the Message and one for the MessageBoard class. Note that I skipped writing tests at this point, since the functionality of these adapters are carefully tested in the following code.
我们需要两个实现,一个是为消息而另一个是为留言薄类的。注意我在这次没有编写测试,因为这些适配器的功能已经在下列代码中被仔细测试过了。

First, we write the message board adapter, so open the messageboard.py file and add the following code:
首先,我们编写留言薄适配器,因此打开 messageboard.py 文件并添加以下代码:

#!python
from book.messageboard.interfaces import IPlainText

class PlainText:

implements(IPlainText)

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

def getText(self):
return self.context.description

def setText(self, text):
self.context.description = unicode(text)

This is an extremely simple implementation of the IPlainText interface, since we map only one attribute.
这是非常简单的 IPlainText 接口的实现,因为我们只映射了一个属性。

  • Line 14: Make sure that the incoming text is unicode, which is very important for the system’s integrity.
    第 14 行: 确保输入的文本是 unicode 编码的,这对于保持系统的整体性是非常重要的。

The implementation for the Message (put it in message.py) looks like this:
消息的实现 (写入 message.py 文件) 如下所示:

from book.messageboard.interfaces import IPlainText

class PlainText:

implements(IPlainText)

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

def getText(self):
return 'Title: %s\n\n%s' %(self.context.title,
self.context.body)

def setText(self, text):
if text.startswith('Title: '):
title, text = text.split('\n', 1)
self.context.title = title[7:]

self.context.body = text.strip()

This implementation is more interesting, since we map two properties to the plain text.
该实现更有趣,因为我们映射了两个属性到纯文本。

  • Line 11-12: In typical MIME-header style, define a field with the pattern <name>:<value> for the title and place the body as content. Note that the standard requires an empty line after the headers too.
    第 11-12 行: 典型的 MIME-header 类型,定义一个域用 <name>:<value> 方式来表示域标题并将内容来表示域体。注意该标准同样要求在域头之后要有个空行。
  • Line 15-17: Check whether a title was specified in this upload. If so, extract the title from the data and store the title; if not just ignore the title altogether. Finally store the rest of the text as the body.
    第 15-17 行: 检查在上传时是否指定了标题。如果是的话,从数据中分离出标题并保存;如果不是的话就完全忽略标题。最后将剩余的文本作为域体保存。

22.1.3 (c) The Configuration(22.1.3 (c) 配置)

The last step is to register the two adapters with the Component Architecture. Just add the following two adapter directives to configure.zcml:
最后一步就是在组件架构中注册这两个适配器。只需将下列两个适配器语句添加到 configure.zcml:

1  <adapter
2      factory=".messageboard.PlainText"
3      provides=".interfaces.IPlainText"
4      for=".interfaces.IMessageBoard" />
5  
6  <adapter
7      factory=".message.PlainText"
8      provides=".interfaces.IPlainText"
9      for=".interfaces.IMessage" />

We are now done. Even though we have two new fully-functional components at this point, we gained no new functionality yet, since these adapters are not used anywhere. We have to complete the next two sections to see any results.
现在我们已经做好了。尽管我们目前已经有了两个新的完整功能的组件,但我们也不能获得新的功能,因为这些适配器还没在任何地方使用。我们还必须完成接下来的两节才可以看到结果。

22.2 Step II: The “Virtual Contents File” Adapter(22.2 步骤 II: “虚拟内容文件” 适配器)

How we implement the virtual contents file is fully up to us. However, there are benefits of choosing one way over another, since it will save us some work. The best method is to create a new interface IVirtualContentsFile, which extends zope.app.file.interfaces.IFile. The advantage is that there are already filesystem-specific adapters (implementing zope.app.filerepresentation.interfaces.IReadFile and zope.app.filerepresentation.interfaces.IWriteFile) for the above mentioned interface. IFile might not be the best and most concise interface for our needs, but the advantages of using it are very convincing.
我们如何实现虚拟内容文件是完全由我们决定的。然而,我们可以选择比较好的方式而舍弃其它的,因为它可以省我们不少事。最好的方法就是创建一个新的从 zope.app.file.interfaces.IFile 扩展而来的接口 IVirtualContentsFile 。这样做的好处在于已经有明确文件系统的适配器 (实现了 zope.app.filerepresentation.interfaces.IReadFile 和 zope.app.filerepresentation.interfaces.IWriteFile)来处理上述接口。IFile 也许不是满足我们需要的最佳和最简洁的接口,但用它的好处在于令人信服。

22.2.1 (a) The Interface(22.2.1 (a) 接口)

When you look through the Zope 3 source code, you will notice that the IFile and IFileContent interfaces go hand in hand with each. Thus, our virtual contents file interface will extend both of these interfaces.
当你查看 Zope 3 源码时,你将注意到 IFile 和 IFileContent 接口是相互联系的。因此,我们的虚拟内容文件接口要将这些接口一起扩展。

#!python
from zope.app.file.interfaces import IFile, IFileContent

class IVirtualContentsFile(IFile, IFileContent):
"""Marker Interface to mark special Message and Message Board
Contents files in FS representations."""

22.2.2 (b) The Implementation(22.2.2 (b) 实现)

Now the fun begins. First we note that IFile requires three properties, contentType, data, and size. While data and size are obvious, we need to think a bit about contentType. Since we really just want to return always text/plain, the accessor should statically return text/plain and the mutator should just ignore the input.
现在有趣的事开始了。首先我们注意到 IFile 要求三个参数, contentType, data, 和 size。当然 data 和 size 是很明显的,我们需要考虑的是 contentType。因为我们真正想返回的总是 text/plain 类型,所以 accessor 应该静态地返回 text/plain 且 mutator 则应忽略输入。

To make a long story short, here is the code, which you should place in a new file called filerepresentation.py:
长话短说,将下面的代码放在你新建的 filerepresentation.py 文件中:

#!python
from zope.interface import implements
from interfaces import IVirtualContentsFile, IPlainText

class VirtualContentsFile(object):

implements(IVirtualContentsFile)

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

def setContentType(self, contentType):
'''See interface IFile'''
pass

def getContentType(self):
'''See interface IFile'''
return u'text/plain'

contentType = property(getContentType, setContentType)

def edit(self, data, contentType=None):
'''See interface IFile'''
self.setData(data)

def getData(self):
'''See interface IFile'''
adapter = IPlainText(self.context)
return adapter.getText() or u''

def setData(self, data):
'''See interface IFile'''
adapter = IPlainText(self.context)
return adapter.setText(data)

data = property(getData, setData)

def getSize(self):
'''See interface IFile'''
return len(self.getData())

size = property(getSize)
  • Line 11-13: As promised, the mutator ignores the input totally and is really just an empty method.
    第 11-13 行: 根据约定,mutator 完全忽略输入成为真正的空方法。
  • Line 15-17: Make sure we always return “text/plain”.
    第 15-17 行: 确保我们始终返回 "text/plain"。
  • Line 25-28 & 30-33: Now we are making use of our previously created PlainText adapters. We simply use the two available API methods.
    第 25-28 & 30-33 行: 现在我们可以利用先前创建的 PlainText 适配器了。我们只需要简单地使用两个可用的 API 方法即可。

This was pretty straightforward. There are really no surprises here.
这相当直接,没什么诧异的。

22.2.3 (c) The Tests(22.2.3 (c) 测试)

Since even the last coding step did not provide a functional piece of code, it becomes so much more important to write some careful tests for the VirtualContentsFile component. Another requirement of the tests are that this adapter is being tested with both MessageBoard and Message instances. To realize this, we write an a base test and then realize this test for each component. So in the tests folder, create a new file called test_filerepresentation.py and add the following content:
因为甚至在编程的最后一步都没有提供代码的功能片断,所以这使得为 VirtualContentsFile 组件精心编写代码就变得非常重要。另一个测试的需求就是该适配器必须被留言薄和消息实例测试。认识到这点,我们编写一个基本测试,然后为每个组件实现它。所以在测试文件夹里,新建一个名为 test_filerepresentation.py 的文件,并添加以下内容:

#!python
import unittest
from zope.interface.verify import verifyObject
from zope.app import zapi
from zope.app.tests import ztapi
from zope.app.tests.placelesssetup import PlacelessSetup

from book.messageboard.interfaces import \
IVirtualContentsFile, IPlainText, IMessage, IMessageBoard
from book.messageboard.message import \
Message, PlainText as MessagePlainText
from book.messageboard.messageboard import \
MessageBoard, PlainText as MessageBoardPlainText
from book.messageboard.filerepresentation import VirtualContentsFile

class VirtualContentsFileTestBase(PlacelessSetup):

def _makeFile(self):
raise NotImplemented

def _registerPlainTextAdapter(self):
raise NotImplemented

def setUp(self):
PlacelessSetup.setUp(self)
self._registerPlainTextAdapter()

def testContentType(self):
file = self._makeFile()
self.assertEqual(file.getContentType(), 'text/plain')
file.setContentType('text/html')
self.assertEqual(file.getContentType(), 'text/plain')
self.assertEqual(file.contentType, 'text/plain')

def testData(self):
file = self._makeFile()

file.setData('Foobar')
self.assert_(file.getData().find('Foobar') >= 0)
self.assert_(file.data.find('Foobar') >= 0)

file.edit('Blah', 'text/html')
self.assertEqual(file.contentType, 'text/plain')
self.assert_(file.data.find('Blah') >= 0)

def testInterface(self):
file = self._makeFile()
self.failUnless(IVirtualContentsFile.providedBy(file))
self.failUnless(verifyObject(IVirtualContentsFile, file))


class MessageVirtualContentsFileTest(VirtualContentsFileTestBase,
unittest.TestCase):

def _makeFile(self):
return VirtualContentsFile(Message())

def _registerPlainTextAdapter(self):
ztapi.provideAdapter(IMessage, IPlainText, MessagePlainText)

def testMessageSpecifics(self):
file = self._makeFile()
self.assertEqual(file.context.title, '')
self.assertEqual(file.context.body, '')
file.data = 'Title: Hello\n\nWorld'
self.assertEqual(file.context.title, 'Hello')
self.assertEqual(file.context.body, 'World')
file.data = 'World 2'
self.assertEqual(file.context.body, 'World 2')


class MessageBoardVirtualContentsFileTest(
VirtualContentsFileTestBase, unittest.TestCase):

def _makeFile(self):
return VirtualContentsFile(MessageBoard())

def _registerPlainTextAdapter(self):
ztapi.provideAdapter(IMessageBoard, IPlainText,
MessageBoardPlainText)

def testMessageBoardSpecifics(self):
file = self._makeFile()
self.assertEqual(file.context.description, '')
file.data = 'Title: Hello\n\nWorld'
self.assertEqual(file.context.description,
'Title: Hello\n\nWorld')
file.data = 'World 2'
self.assertEqual(file.context.description, 'World 2')

def test_suite():
return unittest.TestSuite((
unittest.makeSuite(MessageVirtualContentsFileTest),
unittest.makeSuite(MessageBoardVirtualContentsFileTest),
))

if <u>name</u> == '<u>main</u>':
unittest.main(defaultTest='test_suite')
  • Line 5: Since we are going to make use of adapters, we will need to bring up the component architecture using the PlacelessSetup.
    第 5 行: 因为我们想用适配器,所以我们需要用 PlacelessSetup 提出组件架构。
  • Line 7-13: Imports all the relevant interfaces and components for this test. This is always so much, since we have to register the components by hand (instead of ZCML).
    第 7-13 行: 为本测试导入所有相关的接口和组件。这总是非常多的,因为我们必须手工注册组件(代替 ZCML)
  • Line 17-18: The implementation of this method should create a VirtualContentsFile adapter having the correct object as context. Since the context varies, the specific test case class has to take of its implementation.
    第 17-18 行: 本方法的实现将创建一个有着正确对象作为上下文的 VirtualContentsFile 适配器。因为上下文的变化,具体测试案例类必须take of its implementation.
  • Line 20-21: Since there is a specific adapter registration required for each case (board and message), we will have to leave that up to the test case implementation as well.
    第 20-21 行: 因为每个案例(栏和消息)都要求注册一个相应的适配器,所以我们同样也必须使之适应测试案例实施。
  • Line 27-32: We need to make sure that the plain/text setting can never be overwritten.
    第 27-32 行: 我们需要确保 plain/text 设置永远不被覆盖。
  • Line 34-43: We can really just make some marginal tests here, since the storage details really depend on the IPlainText implementation. There will be stronger tests in the specific test cases for the message board and message (see below).
    第 34-43 行: 这里我们可以真正做点最低限度的测试,因为存储的细节真正有赖于 IPlainText 实现。在为留言薄和消息进行的具体测试中还会有更多的测试(参见下面)。
  • Line 45-48: Always make sure that the interface is completely implemented by the component.
    第 45-48 行: 始终确保接口被组件完全实现。
  • Line 51: This is the beginning of a concrete test case that implements the base test. Note that you should only make the concrete implementation a TestCase.
    第 51 行: 这是实现基本测试的具体测试案例的开始。注意你应该只做 TestCase 的具体实现。
  • Line 54-55: Just stick a plain, empty Message instance in the adapter.
    第 54-55 行: 仅仅在适配器中生成一个纯文本的、空的消息实例。
  • Line 60-68: Here we test that the written contents of the virtual file is correctly passed and the right properties are set.
    第 60-68 行: 在这里我们测试虚拟文件所写内容被正确通过,同时正确的属性被设置。
  • Line 71-88: Pretty much the same that was done for the Message test.
    第 71-88 行: 同消息测试相同。
  • Line 90-97: The usual test boilerplate.
    第 90-97 行: 常用测试模板

You can now run the test and verify the functionality of the new tests.
你现在可以运行测试并确认新测试的功能。

22.2.4 (d) The Configuration(22.2.4 (d) 配置)

This section would not be complete without a registration. While we do not need to register the file representation component, we are required to make some security assertions about the object’s methods and properties. I simply copied the following security assertions from the File content component’s configuration.
本部分没有注册就没有完成。然而我们并不需要注册文件表示组件,我们被要求做一些关于对象方法和属性的案例声明。我只是简单地将下列案例声明从文件内容的配置中拷过来。

1  <content class=".filerepresentation.VirtualContentsFile">
2  
3    <implements interface="
4        zope.app.annotation.interfaces.IAttributeAnnotatable" />
5  
6    <require
7        permission="book.messageboard.View"
8        interface="zope.app.filerepresentation.interfaces.IReadFile" />
9  
10    <require
11        permission="zope.messageboard.Edit"
12        interface="zope.app.filerepresentation.interfaces.IWriteFile"
13        set_schema="zope.app.filerepresentation.interfaces.IReadFile" />
14  
15  </content>
  • Line 3-4: We need the virtual file to be annotable, so it can reach the DublinCore for dates/times and owner information.
    第 3-4 行: 我们需要虚拟文件是可注解的,so it can reach the DublinCore for dates/times and owner information.

22.3 Step III: The IReadDirectory implementation(22.3 步骤 III: IReadDirectory 的实现)

After all the preparations are complete, we are finally ready to give our content components, MessageBoard and Message, a cool filesystem representation.
在所有准备完成后,我们最终准备给我们的内容组件,留言薄和消息,一个酷的文件系统表示。

22.3.1 (a) The Implementation(22.3.1 (a) 实现)

The first fact we should notice is that zope.app.filerepresentation.ReadDirectory has already a nice implementation, except for the superfluous SiteManager and the missing contents file. So we simply take this class (subclass it) and merely overwrite keys(), get(key,default=None), and `len`(). All other methods depend on these three. So our code for the ReadDirectory looks something like that (place in filerepresentation.py):
我们应该注意的第一个事实是 zope.app.filerepresentation.ReadDirectory 已经有一个好的实现,除了多余的 SiteManager 和缺少的内容文件。因此我们只需简单的使用该类(它的子类)并且只要重写 keys(), get(key,default=None) 和 `len`() 即可。所有其它方法都有赖于这三个。所以我们为 ReadDirectory 所写的代码看上去如下所示(放在 filerepresentation.py 文件中):

#!python
from zope.app.filerepresentation.interfaces import IReadDirectory
from zope.app.folder.filerepresentation import \
ReadDirectory as ReadDirectoryBase

class ReadDirectory(ReadDirectoryBase):
"""An special implementation of the directory."""

implements(IReadDirectory)

def keys(self):
keys = self.context.keys()
return list(keys) + ['contents']

def get(self, key, default=None):
if key == 'contents':
return VirtualContentsFile(self.context)
return self.context.get(key, default)

def <u>len</u>(self):
l = len(self.context)
return l+1
  • Line 10-12: When being asked for a list of names available for this container, we get the list of keys plus our virtual contents file.
    第 10-12 行: 当这容器要求可用名字列表时,我们得到一个附加我们虚拟内容文件的关键字列表。
  • Line 14-17: All objects are simply found in the context (MessageBoard or Message) itself, except the contents. When the system asks for the contents, we simply give it a VirtualContentsFile instance that we prepared in the previous section and we do not have to worry about anything, since we know that the system knows how to handle zope.app.file.interfaces.IFile objects.
    第 14-17 行: 除了内容,所有对象都可以在上下文(MessageBoard 或 Message)中被简单的发现。当系统要求内容时,我们可以简单地给它一个我们在上一节准备好的 VirtualContentsFile 实例。同时我们不用担心任何事,因为我们知道系统知道要如何处理 zope.app.file.interfaces.IFile 对象。
  • Line 19-21: Obviously, we pretend to have one more object than we actually have.
    第 19-21 行: 很明显,我们假定拥有比我们真正拥有的对象还多一个。

Now we are done with our implementation. Let’s write some unit tests to ensure the functionality and then register the filesystem components.
现在我们完成了实现。让我们写一些单元测试以保证功能,然后注册文件系统组件。

22.3.2 (b) The Tests(22.3.2 (b) 测试)

For testing the ReadDirectory implementation, we again need to test it with the MessageBoard and Message components. So similar to the previous tests, we have a base test with specific implementations. Also note that it will not be necessary to test all IReadDirectory methods, since they are already tested in the base class tests. So we are just going to test the methods we have overridden:
为了测试 ReadDirectory 的实现,我们还是需要用 MessageBoard 和 Message 组件来对它进行测试。所以同前个测试一样,我们对具体实现做一个基本测试。注意,并不需要测试 IReadDirectory 的所有方法,因为它们已经在基本类测试中测试过了。因此我们打算只测试我们没测试过的方法。

#!python
from book.messageboard.filerepresentation import ReadDirectory

class ReadDirectoryTestBase(PlacelessSetup):

def _makeDirectoryObject(self):
raise NotImplemented

def _makeTree(self):
base = self._makeDirectoryObject()
msg1 = Message()
msg1.title = 'Message 1'
msg1.description = 'This is Message 1.'
msg11 = Message()
msg11.title = 'Message 1-1'
msg11.description = 'This is Message 1-1.'
msg2 = Message()
msg2.title = 'Message 1'
msg2.description = 'This is Message 1.'
msg1['msg11'] = msg11
base['msg1'] = msg1
base['msg2'] = msg2
return ReadDirectory(base)

def testKeys(self):
tree = self._makeTree()
keys = list(tree.keys())
keys.sort()
self.assertEqual(keys, ['contents', 'msg1', 'msg2'])
keys = list(ReadDirectory(tree['msg1']).keys())
keys.sort()
self.assertEqual(keys, ['contents', 'msg11'])

def testGet(self):
tree = self._makeTree()
self.assertEqual(tree.get('msg1'), tree.context['msg1'])
self.assertEqual(tree.get('msg3'), None)
default = object()
self.assertEqual(tree.get('msg3', default), default)
self.assertEqual(tree.get('contents').<u>class</u>,
VirtualContentsFile)

def testLen(self):
tree = self._makeTree()
self.assertEqual(len(tree), 3)
self.assertEqual(len(ReadDirectory(tree['msg1'])), 2)
self.assertEqual(len(ReadDirectory(tree['msg2'])), 1)


class MessageReadDirectoryTest(ReadDirectoryTestBase,
unittest.TestCase):

def _makeDirectoryObject(self):
return Message()


class MessageBoardReadDirectoryTest(ReadDirectoryTestBase,
unittest.TestCase):

def _makeDirectoryObject(self):
return MessageBoard()
  • Line 5-6: Return an instance of the object to be tested.
    返回要被测试的对象实例
  • Line 8-22: Create an interesting message tree on top of the base. This will allow some more detailed testing.
    创建一个有趣的基准之上的消息树。这将允许再多的细节测试。
  • Line 24-31: Make sure this contents file and the sub-messages are correctly listed.
    确保该内容文件和子消息被正确列出。
  • Line 33-40: Now let’s also make sure that the objects we get are the right ones.
    现在让我们也确保我们得到的对象是对的。
  • Line 42-46: A simple test for the number of contained items (including the contents).
    简单测试所含项目数(包括内容)。
  • Line 49-60: The concrete implementations of the base test. Nothing special.
    基本测试的混和实现。没有特殊。

After you are done writing the tests, do not forget to add the two new TestCases to the TestSuite.
在你完成测试之后,不要忘了添加两个 TestCases 到 TestSuite中去。

22.3.3 (c) The Configuration(22.3.3 (c) 配置)

Finally we simply register our new components properly using the following ZCML directives:
最终我们适当地使用下列 ZCML 语句来注册我们的新的组件:

1  <adapter
2     for=".interfaces.IMessageBoard"
3     provides="zope.app.filerepresentation.interfaces.IReadDirectory"
4     factory=".filerepresentation.ReadDirectory"
5     permission="zope.View"/>
6  
7  <adapter
8     for=".interfaces.IMessage"
9     provides="zope.app.filerepresentation.interfaces.IReadDirectory"
10     factory=".filerepresentation.ReadDirectory"
11     permission="zope.View"/>

That’s it. You can now restart Zope and test the filesystem representation with an FTP client of your choice. In the following sequence diagram you can see how a request is guided to find its information and return it properly.
就是这样。你现在可以重启 Zope 并用你选的FTP客户端来测试文件系统表示。从下图的顺序图中你可以看到一个请求是如何被指引找到它的信息并以适当方式返回的。

PIC Figure 22.1: Collaboration diagram of the inner working from requesting the contents “file” to receiving the actual data.
图 22.1: 从请求内容“文件”到收到实际数据的内部运行的关联图

22.4 Step IV: The Icing on the Cake - A special Directory Factory(22.4 步骤 IV: 蛋糕上的冰 - 一个特殊的目录生成器)

While you were playing around with the new filesystem support, you might have tried to create a directory to see what happened and it probably just caused a system error, since no adapter was found from IMessage/ IMessageBoard to IDirectoryFactory. Since such a behavior is undesirable, we should create a custom adapter that provides IDirectoryFactory. The IWriteDirectory adapter of any container object then knows how to make use of this factory adapter. So we add the following trivial factory to our filesystem code:
当你使用新的文件系统支持时,你也许会试着创建一个目录看看会发生什么,它也许会引发一个系统错误,因为从 IMessage/IMessageBoard 到 IDirectoryFactory 都没有发现适配器。这样是不合规则的,我们应该创建一个自定义适配器以提供 IDirectoryFactory。任何容器对象的 IWriteDirectory 适配器就知道如何利用这个生成适配器。因此我们在我们的文件系统代码中添加这个生成器:

#!python
from zope.app.filerepresentation.interfaces import IDirectoryFactory
from message import Message

class MessageFactory(object):
"""A simple message factory for file system representations."""

implements(IDirectoryFactory)

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

def <u>call</u>(self, name):
"""See IDirectoryFactory interface."""
return Message()

Registering the factory is just a matter of two adapter directives (one for each content component):
注册该生成器只需两个适配器语句(每个内容组件一个)

1  <adapter
2     for=".interfaces.IMessageBoard"
3     provides="zope.app.filerepresentation.interfaces.IDirectoryFactory"
4     factory=".filerepresentation.MessageFactory"
5     permission="zope.View" />
6  
7  <adapter
8     for=".interfaces.IMessage"
9     provides="zope.app.filerepresentation.interfaces.IDirectoryFactory"
10     factory=".filerepresentation.MessageFactory"
11     permission="zope.View" />

Now we have finally made it. The filesystem support should be very smooth and usable at this point. You should be able to view all relevant content, upload new contents data and create new messages. The only problem that might remain is that some FTP clients (like KDE’s FTP support) try to upload the contents file as contents.part and then rename it to contents. Since our filesystem code does not support such a feature, this will cause an error; see exercise 2 for details.
现在我们最终完成它。这时文件系统支持应该是非常平滑和有用的。你应该能够看到所有的相关内容,上传新内容数据和创建新消息。剩下的唯一问题也许是一些 FTP 客户端(象 KDE 的 FTP支持)试图将内容文件作为 contents.part 上传,然后将其重命名为内容。因为我们的文件系统代码还不支持该功能,所以这将会引发一个错误;具体细节请参见练习 2。

Exercises(练习)

  • Currently there is no creation/modification date/time or creator defined for the virtual contents file. This is due to the fact that the respective Dublin Core properties were not set. The virtual file should really receive the same Dublin Core the `MessageBoard` or Message has. The easiest way would be simply to copy the Dublin Core annotation. Do that and make sure the data and the user are shown correctly.
    当前代码中没有创建/修改日期或时间,也没有定义虚拟内容文件的创建者。这归根于各自的 Dublin 核心参数没有被设置这一事实。虚拟文件将
  • In the final remarks of this chapter I stated that the current code will not work very well with some clients, such as Konqueror. Fix the code to support this behavior. The best would be to store the temporary file in an annotation and delete it, once it was moved.
    在本章结束时我提及当前在一些客户端下无法很好的工作,如 Konqueror,修复代码以支持这项功能。最好是在一个 annotation 中保存临时文件并在该文件被移动时删除它。