Zope3宝典/定制模式域和表单部件

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

Chapter 15:Custom Schema Fields and Form Widgets(第 15 章:定制模型域和表单部件)


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

原文作者:StephanRichter, zope.org

授权许可:创作共用协议

翻译人员:

校对人员:Leal, FireHare

适用版本:Zope 3

文章状态:校正阶段



Difficulty(难度)

Sprinter(进阶者)

Skills(技能)

  • Be familiar with the results achieved in the previous two chapters.
    需要熟悉前面两章实现的内容;
  • You should be comfortable with presentation components (views) as introduced in the previous chapter.
    您应该对前面章节所介绍的表示组件(视图)有所认识。


Problem/Task(问题/任务)

So far we have created fairly respectable content components and some nice views for them. Let’s now look at the fine print; currently it is possible that anything can be written into the message fields, including malicious HTML and Javascript. Therefore it would be useful to develop a special field (and corresponding widget) that strips out disallowed HTML tags.
到现在为止我们已经创建了让人羡慕的内容组件并且也为它们配备了相当不错的视图。现在让我们来关注细节;当前任何东西都可能被写入到message域里,当然包括恶意的HTML和Javascript代码。因此开发一个剔除不被允许的HTML标记的特殊field(和相应的widget)将会非常有用。

Solution(解决方案)

Creating custom fields and widgets is a common task for end-user applications, since these systems have often very specific requirements. It was a design goal of the schema/form sub-system to be as customizable as possible, so it should be no surprise that it is very easy to write your own field and widget.
创建常用域和部件对最终用户程序而言是一个较为普通的工作,由于这些系统时常有非常特殊的需求,因此定制schema/form将会帮助我们到达我们的设计目标,通过定制能非常轻易编写出自己想要的界面。


15.1 Step I: Creating the Field(15.1 步骤 I:创建域)

The goal of the special field should be to verify input based on allowed or forbidden HTML tags. If the message body contains HTML tags other than the ones allowed or contains any forbidden tags, then the validation of the value should fail. Note that only one of the two attributes can be specified at once.
特定域的目的是基于"允许或禁止HTML标记"来验证该域的输入的。如果消息文本超出了允许的HTML标记或包含了禁用的HTML标记,那么数值确认应该失败。注意只能指定两个属性中的一个。

It is often not necessary to write a field from scratch, since Zope 3 ships with a respectable collection already. These serve commonly also as base classes for custom fields. For our HTML field the Text field seems to be the most appropriate base, since it provides most of the functionality for us already.
在ZOPE3中,自从引入了令人称道的集合后,我们就不需要从头开始写一个域,由于它们作为基础类普遍服务于常用域。并且它已经提供了大部份功能给了我们,因此,我们的HTML域文本域也把它们作为我们域基础。

We will extend the Text field by two new attributes called allowed_tags and forbidden_tags. Then we are going to modify the _validate() method to reflect the constraint made by the two new attributes.
我们将通过allowed_tags 和 forbidden_tags 的两个新属性来扩充文本域。然后我们将要修改_validate()方法来实现约束。

15.1.1 (a) Interface(15.1.1 (a) 接口)

As always, the first step is to define the interface. In the messageboard’s interfaces module, add the following lines:
照旧,第一步是定义接口。 在 messageboard's 的接口模块中,增加下列的行:

#!python
from zope.schema import Tuple
from zope.schema.interfaces import IText

class IHTML(IText):
"""A text field that handles HTML input."""

allowed_tags = Tuple(
title=u"Allowed HTML Tags",
description=u"""\
Only listed tags can be used in the value of the field.
""",
required=False)

forbidden_tags = Tuple(
title=u"Forbidden HTML Tags",
description=u"""\
Listed tags cannot be used in the value of the field.
""",
required=False)
  • Line 1: The Tuple field simply requires a value to be a Python tuple.
    第1行:Tuple域需要数值是一个Python元组。
  • Line 2 & 4: We simple extend the IText interface and schema.
    第2-4行:我们简单的扩充了IText 接口和 schema。
  • Line 7-12 & 14-19: Define the two additional attributes using the field Tuple.
    第7-12行&14-19行:用field元组定义两个附加属性


15.1.2 (b) Implementation(15.1.2 (b) 实现)

As previously mentioned, we will use the Text field as base class, since it provides most of the functionality we need. The main task of the implementation is to rewrite the validation method.
先前同样地提到, 我们将使用文本域作为基本的类, 它提供了我们需要的大部份的功能。实现的主要任务是重写确认方法。

Let’s start by editing a file called fields.py in the messageboard package and inserting the following code:
让我们在messageboard 软件包里开始编辑fields.py文件,并且插入如下代码:

#!python
import re

from zope.schema import Text
from zope.schema.interfaces import ValidationError

forbidden_regex = r'</?(?:%s).*?/?>'
allowed_regex = r'</??(?!%s[ />])[a-zA-Z0-9]*? ?(?:[a-z0-9]*?=?".*?")*/??>'

class ForbiddenTags(ValidationError):
<u>doc</u> = u"""Forbidden HTML Tags used."""


class HTML(Text):

allowed_tags = ()
forbidden_tags = ()

def <u>init</u>(self, allowed_tags=(), forbidden_tags=(), **kw):
self.allowed_tags = allowed_tags
self.forbidden_tags = forbidden_tags
super(HTML, self).<u>init</u>(**kw)

def _validate(self, value):
super(HTML, self)._validate(value)

if self.forbidden_tags:
regex = forbidden_regex %'|'.join(self.forbidden_tags)
if re.findall(regex, value):
raise ForbiddenTags(value, self.forbidden_tags)

if self.allowed_tags:
regex = allowed_regex %'[ />]|'.join(self.allowed_tags)
if re.findall(regex, value):
raise ForbiddenTags(value, self.allowed_tags)
  • Line 1: Import the Regular Expression module ( re); we will use regular expressions to do the validation of the HTML.
    第1行:导入Regular Expression(re)模块,我们将使用regular expressions来验证HTML。
  • Line 3: Import the Text field that we will use as base class for the HTML field.
    第3行:导入文本,我们将用它作为HTML域的基础类。
  • Line 4 & 10-11: The validation method of the new HTML field will be able to throw a new type of validation error when an illegal HTML tag is found.
    第4行&10-11行:当一些不合法的HTML 标志被发现的时候,新的HTML域的确认方法将能抛出一种新类型的确认错误。

    Usually errors are defined in the interfaces module, but since it would cause a recursive import between the interfaces and fields module, we define it here.
    通常错误被定义在接口模块里,但是由于在接口和域模块之间导入会引起循环,因此我们把它定义在这儿。
  • Line 7-9: These strings define the regular expression templates for detecting forbidden or allowed HTML tags, respectively. Note that these regular expressions are quiet more restrictive than what the HTML 4.01 standard requires, but it is good enough as demonstration. See exercise 1 at the end of the chapter to see how it should be done correctly.
    第7-9行:这些字符串将为检测禁止的HTML标记或允许HTML标记定义一个规则的表达式模板。注意,这些规则表示式已经远远超过了 HTML 4.01 标准要求的内容,但它确能很好的做好示范作用。请看本章后面练习1中它如何正确的工作。
  • Line 16-19: In the constructor we are extracting the two new arguments and send the rest to the constructor of the Text field (line 21).
    第16-19行:在构造器里我们提取了两个新参数并且把余下来的给文本字符段的构造器(21行)。
  • Line 22: First we delegate validation to the Text field. The validation process might already fail at this point, so that further validation becomes unnecessary.
    第22行:首先我们给委派文本域确认。此时确认过程失败,所以进一步的确认就变得不必要了。
  • Line 24-27: If forbidden tags were specified, then we try to detect them. If one is found, a ForbiddenTags error is raised attaching the faulty value and the tuple of forbidden tags to the exception.
    第24-27行:如果禁用标志被指定,然后我们试着发现他们。如果一个被发现,一个禁止标记错误异常将会连着失败值和禁止标记的元组一起被引发。
  • Line 29-32: Similarly to the previous block, this block checks that all used tags are in the collection of allowed_tags otherwise a ForbiddenTags error is raised.
    第29-32行:与前面的程序块有些相似,这块在allowed_tags集合里检查所有有用的标记,否则一个禁止标记的错误将被引发。

We have an HTML field, but it does not implement IHTML interface. Why not? It is due to the fact that it would cause a recursive import once we use the HTML field in our content objects. To make the interface assertion, add the following lines to the interfaces.py module:
我们有一个 HTML 域,但是它不实现 IHTML 接口。为什么不呢?理由是:一旦在我们的内容对象里使用HTML域,事实上它将引起一个循环导入。为了声明接口,在interfaces.py模块中添加如下行:

#!python
from zope.interface import classImplements
from fields import HTML
classImplements(HTML, IHTML)

At this point we should have a working field, but let’s write some unit tests to verify the implementation.
现在该域已经可以用来工作了,但我们还是写一些单元测试程序来验证我们的实现是否正确。


15.1.3 (c) Unit Tests(15.1.3 (c) 单元测试)

Since we will use the Text field as a base class, we can also reuse the Text field’s tests. Other than that, we simply have to test the new validation behavior.
既然我们将会以文本域作为一个基本类,当然我们也能重用文本域的测试。除了那些重复代码之外,我们还要另外测试新的校验行为。

In messageboard/tests add a file test_fields.py and add the following base tests. Note that the code is not complete (abbreviated sections are marked by ...). You can find it in the source repository though.
在messageboard/tests添加test_fields.py文件并且添加如下的基本测试。注意这些代码不是完整的(省略的区域被标以...),您能在代码库中发现这些代码。

#!python
import unittest
from zope.schema.tests.test_strfield import TextTest

from book.messageboard.fields import HTML, ForbiddenTags

class HTMLTest(TextTest):

_Field_Factory = HTML

def test_AllowedTagsHTMLValidate(self):
html = self._Field_Factory(allowed_tags=('h1','pre'))
html.validate(u'<h1>Blah</h1>')
...
self.assertRaises(ForbiddenTags, html.validate,
u'<h2>Foo</h2>')
...

def test_ForbiddenTagsHTMLValidate(self):
html = self._Field_Factory(forbidden_tags=('h2','pre'))
html.validate(u'<h1>Blah</h1>')
...
self.assertRaises(ForbiddenTags, html.validate,
u'<h2>Foo</h2>')
...

def test_suite():
return unittest.TestSuite((
unittest.makeSuite(HTMLTest),
))

if <u>name</u> == '<u>main</u>':
unittest.main(defaultTest='test_suite')
  • Line 2: Since we use the Text field as base class, we can also use it’s test case as base, getting some freebie tests in return.
    第2行:既然我们以文本域作为基本类,我们就使用它的测试范例作为基本类,获得一些免费的代码。
  • Line 8: However, the TextTest base comes with some rules we have to abide to. Specifying this _Field_Factory attribute is required, so that the correct field is tested.
    第8行:然而,`TextTest`要求我们必须遵守一些规则,它要求我们必须指定_Field_Factory属性,以致于正确的域能被测试。
  • Line 10-16: These are tests of the validation method using the allowed_ tags attribute. Some text was removed some to conserve space. You can look at the code for the full test suite.
    第10-16行:这儿使用了allowed_ tags属性的测试校验方法。一些文本被一些空格移除,您能在完整的测试套件中看到这些代码。
  • Line 18-24: Here we are testing the validation method using the forbidden_tags attribute.
    第18-24行:这里我们正在尝试使用forbidden_tags 属性的校验方法。


15.2 Step II: Creating the Widget(15.2 步骤 II:创建部件)

Widgets are simply views of a field. Therefore we place the widget code in the browser sub-package.
部件只是一个域的视图。因此我们把部件代码放在browser子软件包里。

Our `HTMLSourceWidget` will use the `TextAreaWidget` as a base and only the converter method _convert(value) has to be reimplemented, so that it will remove any undesired tags from the input value (yes, this means that the validation of values coming through these widgets will always pass.)
我们的 `HTMLSourceWidget` 将会以 `TextAreaWidget` 作为基础并且只有转换器方法_convert(value)必须被重新实现,所以它将会把任何的不想要的标志从输入值移开( 是的,这意味值校验将贯穿于整个部件)。

15.2.1 (a) Implementation(15.2.1 (a) 实现)

Since there is no need to create a new interface, we can start right away with the implementation. We get started by adding a file called widgets.py and inserting the following content:
由于这里不需要我们创建一个新的接口,我们现在就开始我们的实现。我们开始添加widgets.py文件并插入如下内容:

#!python
import re
from zope.app.form.browser import TextAreaWidget
from book.messageboard.fields import forbidden_regex, allowed_regex

class HTMLSourceWidget(TextAreaWidget):

def _toFieldValue(self, input):
input = super(HTMLSourceWidget, self)._toFieldValue(input)

if self.context.forbidden_tags:
regex = forbidden_regex %'|'.join(
self.context.forbidden_tags)
input = re.sub(regex, '', input)

if self.context.allowed_tags:
regex = allowed_regex %'[ />]|'.join(
self.context.allowed_tags)
input = re.sub(regex, '', input)

return input
  • Line 2: As mentioned above, we are going to use the `TextAreaWidget` as a base class.
    第2行:依照上面提到的,我们将以 `TextAreaWidget` 作为一个基本类。
  • Line 3: There is no need to redefine the regular expressions for finding forbidden and non-allowed tags again, so we use the field’s definitions. This will also avoid that the widget converter and field validator get out of sync.
    第3行:这儿不需要为禁用和不被允许的标记再次重新定义规则表达式,因此,我们使用域定义。这也将会避免部件转换器和域校验器不同步。
  • Line 8: We still want to use the original conversion, since it takes care of weird line endings and some other routine cleanups.
    第8行:我们仍然想要使用源转换,因为它会顾及终止行和一些其它常规清除。
  • Line 10-13: If we find a forbidden tag, simply remove it by replacing it with an empty string. Notice how we get the forbidden_tags attribute from the context (which is the field itself) of the widget.
    第10-13行:如果我们发现一个被禁止的标记,只是用一些空串更换移除它。注意我们如何从部件的context(域本身)中获得forbidden_tags属性。
  • Line 15-18: If we find a tag that is not in the allowed tags tuple, then remove it as well.
    第15-18行:如果我们发现一个不在被允许标志元组中的标志,然后移除它。

Overall, this a very nice and compact way of converting the input value.
这个转换输入值的方法很好并且也很轻巧。


15.2.2 (b) Unit Tests(15.2.2 (b) 单元测试)

While we usually do not write unit tests for high-level view code, widget code should be tested, particularly the converter. Open test_widgets.py in browser/tests and insert:
我们通常不为高层视图代码编写单元测试,不过部件代码应该被测试,特别是转换器。在browser/tests里打开test_widgets.py文件并插入:

#!python
import unittest
from zope.app.form.browser.tests.test_textareawidget import TextAreaWidgetTest
from book.messageboard.browser.widgets import HTMLSourceWidget
from book.messageboard.fields import HTML

class HTMLSourceWidgetTest(TextAreaWidgetTest):

_FieldFactory = HTML
_WidgetFactory = HTMLSourceWidget


def test_AllowedTagsConvert(self):
widget = self._widget
widget.context.allowed_tags=('h1','pre')
self.assertEqual(u'<h1>Blah</h1>',
widget._toFieldValue(u'<h1>Blah</h1>'))
...
self.assertEqual(u'Blah',
widget._toFieldValue(u'<h2>Blah</h2>'))
...

def test_ForbiddenTagsConvert(self):
widget = self._widget
widget.context.forbidden_tags=('h2','pre')

self.assertEqual(u'<h1>Blah</h1>',
widget._toFieldValue(u'<h1>Blah</h1>'))
...
self.assertEqual(u'Blah',
widget._toFieldValue(u'<h2>Blah</h2>'))
...

def test_suite():
return unittest.TestSuite((
unittest.makeSuite(HTMLSourceWidgetTest),
))

if <u>name</u> == '<u>main</u>':
unittest.main(defaultTest='test_suite')
  • Line 2: Of course we are reusing the `TextAreaWidgetTest` to get some freebie tests.
    第2行:当然我们正在重复使用 `TextAreaWidgetTest`,我们重用 `TextAreaWidgetTest` 测试时可以省掉很多事情。
  • Line 8-9: Fulfilling the requirements of the `TextAreaWidgetTest`, we need to specify the field and widget we are using, which makes sense, since the widget must have the field (context) in order to fulfill all its duties.
    第8-9行:实现 `TextAreaWidgetTest` 的需求, 我们需要指定域和我们正在使用的部件,这很有道理 ,因为widget必须有相应的field(内容)。
  • Line 12-31: Similar in nature to the field tests, the converter is tested. In this case however, we compare the output, since it can differ from the input based on whether forbidden tags were found or not.
    第12-31行:测试转换器与域测试很相似。当前情况下我们比较输出,由于它不同于输入,因为输入基于禁止标记是否被发现。

15.3 Step III: Using the HTML Field(15.3 步骤 III:使用 HTML 域)

Now we have all the pieces we need. All that’s left is to integrate them with the rest of the package. There are a couple of steps involved. First we register the `HTMLSourceWidget` as a widget for the HTML field. Next we need to change the IMessage interface declaration to use the HTML field.
现在我们有我们需要的所有东西。剩下的工作就是把他们与程序包中的其它部分进行整和。这里牵涉的如下步骤:

15.3.1 (a) Registering the Widget(15.3.1 (a) 注册部件)

To register the new widget as a view for the HTML field we use the zope namespace view directive. Therefore you have to add the zope namespace to the configuration file’s namespace list by adding the following line int he opening configure element:
我们使用zope命名空间view指令为HTML域注册一个新的widget作为view。因此您必须在配置文件的命名空间列表中加一个zope namespace,如下:

#!plain
1  xmlns:zope="http://namespaces.zope.org/zope"

Now add the following directive:
现在加入如下指令:

1  <zope:view
2      type="zope.publisher.interfaces.browser.IBrowserRequest"
3      for="book.messageboard.interfaces.IHTML"
4      provides="zope.app.form.interfaces.IInputWidget"
5      factory=".widgets.HTMLSourceWidget"
6      permission="zope.Public"
7      />
  • Line 2: Since the zope:view directive can be used for any presentation type (for example: HTTP, WebDAV and FTP), it is necessary to state that the registered widget is for browsers (i.e. HTML).
    第2行:由于zope:view 指令能被用于任何传输类型(例如:HTTP, WebDAV和FTP),因此注册widget给browsers (i.e. HTML)它是必需的。
  • Line 3: This widget will work for all fields implementing IHTML.
    第3行:部件将服务于所有域应用IHTML。
  • Line 4: In general presentation component, like adapters, can have a specific output interface. Usually this interface is just zope.interface. Interface, but here we specifically want to say that this is a widget that is accepting input for the field. The other type of widget is the `DisplayWidget`.
    第4行:通常表示组件就像适配器一样,有一个特定的输出接口。通常这一个接口就是 zope.interface 。 在这里我们明确的想说这个部件正是为了接受域的输入,其它部件是显示部件(`DisplayWidget`)。
  • Line 5: Specifies the factory or class that will be used to generate the widget.
    第5行:指定用于产生widget的factory 或class。
  • Line 6: We make this widget publically available, meaning that everyone using the system can use the widget as well.
    第6行:我们让widget公开可用,也就意味着每个用这个系统的人都可以使用这个部件。

15.3.2 (b) Adjusting the IMessage interface(15.3.2 (b) 调整 IMessage 接口)

The final step is to use the field in the IMessage interface. Let’s go to the interfaces module to decide which property is going to become an HTML field. The field is already imported.
最后一步是在IMessage 接口中使用域。 让我们决定在接口模块中哪一个属性将成为一个 HTML 域。 当然,这个域同样被导入了。

Now, we definitely want to make the body property of IMessage an HTML field. We could also do this for description of `IMessageBoard`, but let’s not to do that for reasons of keeping it simple. So here are the changes that need to be done to the body property declaration (starting at line 24):
现在,我们想要使 IMessage 的body属性成为一个 HTML 域。当然我们也可以让 `IMessageBoard` 的描述(description)域也成为HTML 域, 但是为了尽可能简单一些我们现在不想做这些额外的工作。因此这儿我们只需对body属性声明做一些改变(开始于第24行):

#!python 
body = HTML(
title=u"Message Body",
description=u"This is the actual message. Type whatever!",
default=u"",
allowed_tags=('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'a',
'br', 'b', 'i', 'u', 'em', 'sub', 'sup',
'table', 'tr', 'td', 'th', 'code', 'pre',
'center', 'div', 'span', 'p', 'font', 'ol',
'ul', 'li', 'q', 's', 'strong'),
required=False)
  • Line 5-9: Here is our new attribute that was added in the IHTML interface. This is my choice of valid tags, so feel free to add or remove whatever tags you like.
    第5-9行:在这里我们把新属性增加到 IHTML 接口里。 这是我选择的有效标记, 不过您可以自由的增加或移除其中任何标记。

And that’s it! You are done. To try the result of your work, restart Zope 3, start editing a new message and see if it will accept tags like html or body. You should notice that these tags will be silently removed from the message body upon saving it.
您已经完成所有的工作了。现在要做的就是尝试您工作的结果,重启Zope3,开始编辑一个新消息,并且看它是否接受像html或body这样的标记。您应该会发现当您保存它们的时候,这些标记已经从消息中被移除了。

Exercises(练习)

  • Instead of using our own premature HTML cleanup facilities, we really should make use of Chris Wither’s HTML Strip-o-Gram package which can be found at http://www.zope.org/Members/chrisw/StripOGram. Implement a version of the HTML field and `HTMLSourceWidget` widget using this package.
    可以用其它的来代替我们自己HTML清除装置,我们可以利用Chris Wither的HTML Strip-o-Gram 软件包,该软件包可以在 http://www.zope.org/Members/chrisw/StripOGram 中找到,用这个软件包来实现HTML域和`HTMLSourceWidget` 部件的新版本。
  • Sometimes it might be nice to also allow HTML for the title of the messages, therefore you will also need an HTML version for the `TextLine` field and the `TextWidget`. Abstract the current converter and validation implementation, so that it is usable for both, message title and body.
    有时在消息的标题中允许HTML可能很不错,因此您需要一个HTML版本给 `TextLine` 域和 `TextWidget`,对当前转换器和校验器的实现进行改造,让消息标题和body可以使用HTML。
  • Using only HTML as input can be boring and tedious for some message board applications. In the zwiki for Zope 3 packge we make use of a system ( zope.app.renderer) that let’s you select the type of input and then knows how to render each type of input for the browser. Insert this type of system into the message board application and merge it with the HTML validation and conversion code.
    一些留言簿程序仅能使用HTML做为输入,因此显得很无聊和沉闷。在zwiki里,我们能利用一个系统(zope.app.renderer),系统让您选择输入的类型并且也知道如何在浏览器里渲染每种类型的输入。插入这种类型系统到留言簿程序中并且把HTML校验和转换代码与它合并。