自定义模型字段和表单
难易程度: 进阶者
技能要求:
需要熟悉前面两章实现的内容;
您应该对前面章节介绍的表示组件(表现组件)有些认识。
问题/任务:
到现在为止我们已经创建了让人羡慕的内容组件并且也为它们配备了相当不错的视图。现在让我们来关注细节;当前任何东西都可能被写入到message字段里,当然包括恶意的HTML和Javascript代码。因此开发一个剔除不被允许的HTML标记的特殊field(和相应的widget)将会非常有用。
解决方案:
创建常用字段和部件对最终用户程序而言是一个较为普通的工作,由于这些系统时常有非常特殊的需求,因此定制schema/form将会帮助我们到达我们的设计目标,通过定制能非常轻易编写出自己想要的界面。
第I步: 创建字段
应该确认的目标是:这个特殊字段的输入基于"允许或禁止HTML标记"。如果消息文本超出了允许的HTML标记或包含了禁用的HTML标记,那么数值确认应该失败。
在ZOPE3中,自从引入了令人称道的集合后,我们就不需要从头开始写一个字段,由于它们作为基础类普遍服务于常用字段。并且它已经提供了大部份功能给了我们,因此,我们的HTML域文本字段也把它们作为我们字段基础。
我们将通过allowed_tags 和 forbidden_tags 的两个新属性来扩充文本字段。然后我们将要修改_validate()方法来实现约束。
(a) 接口
照旧,第一步是定义接口。 在 messageboard's 的接口模块中,增加下列的行:
第1行:Tuple字段需要数值是一个Python元组。
第2-4行:我们简单的扩充了IText 接口和 schema。
第7-12行&14-19行:用field元组定义两个附加属性
(b) 实现
先前同样地提到, 我们将使用文本字段作为基本的类, 它提供了我们需要的大部份的功能。实现的主要任务是重写确认方法。
让我们在messageboard 软件包里开始编辑fields.py文件,并且插入如下代码:
'
7 allowed_regex = r'??(?!%s[ />])[a-zA-Z0-9]*? ?(?:[a-z0-9]*?=?".*?")*/??>'
8
9 class ForbiddenTags(ValidationError):
10 __doc__ = u"""Forbidden HTML Tags used."""
11
12
13 class HTML(Text):
14
15 allowed_tags = ()
16 forbidden_tags = ()
17
18 def __init__(self, allowed_tags=(), forbidden_tags=(), **kw):
19 self.allowed_tags = allowed_tags
20 self.forbidden_tags = forbidden_tags
21 super(HTML, self).__init__(**kw)
22
23 def _validate(self, value):
24 super(HTML, self)._validate(value)
25
26 if self.forbidden_tags:
27 regex = forbidden_regex %'|'.join(self.forbidden_tags)
28 if re.findall(regex, value):
29 raise ForbiddenTags(value, self.forbidden_tags)
30
31 if self.allowed_tags:
32 regex = allowed_regex %'[ />]|'.join(self.allowed_tags)
33 if re.findall(regex, value):
34 raise ForbiddenTags(value, self.allowed_tags)
]]>
第1行:导入Regular Expression(re)模块,我们将使用regular expressions来验证HTML。
第3行:导入文本,我们将用它作为HTML字段的基础类。
第4行&10-11行:当一些不合法的HTML 标志被发现的时候,新的HTML字段的确认方法将能抛出一种新类型的确认错误。
通常错误被定义在接口模块里,但是由于在接口和字段模块之间导入会引起循环,因此我们把它定义在这儿。
第7-9行:这些字符串将为检测禁止的HTML标记或允许HTML标记定义一个规则的表达式模板。注意,这些规则表示式已经远远超过了 HTML 4.01 标准要求的内容,但它确能很好的做好示范作用。请看本章后面练习1中它如何正确的工作。
第16-19行:在构造器里我们提取了两个新参数并且把余下来的给文本字符段的构造器(21行)。
第22行:首先我们给委派文本字段确认。此时确认过程失败,所以进一步的确认就变得不必要了。
第24-27行:如果禁用标志被指定,然后我们试着发现他们。如果一个被发现,一个禁止标记错误异常将会连着失败值和禁止标记的元组一起被引发。
第29-32行:与前面的程序块有些相似,这块在allowed_tags集合里检查所有有用的标记,否则一个禁止标记的错误将被引发。
我们有一个 HTML 字段,但是它不实现 IHTML 接口。为什么不呢?理由是:一旦在我们的内容对象里使用HTML字段,事实上它将引起一个循环导入。
为了声明接口,在interfaces.py模块中添加如下行:
现在该字段已经可以用来工作了,但我们还是写一些单元测试程序来验证我们的实现是否正确。
(c) 单元测试
既然我们将会以文本字段作为一个基本类,当然我们也能重用文本字段的测试。除了那些重复代码之外,我们还要另外测试新的校验行为。
在messageboard/tests添加test_fields.py文件并且添加如下的基本测试。注意这些代码不是完整的(省略的区域被标以...),您能在代码库中发现这些代码。
Blah')
13 ...
14 self.assertRaises(ForbiddenTags, html.validate,
15 u'Foo
')
16 ...
17
18 def test_ForbiddenTagsHTMLValidate(self):
19 html = self._Field_Factory(forbidden_tags=('h2','pre'))
20 html.validate(u'Blah
')
21 ...
22 self.assertRaises(ForbiddenTags, html.validate,
23 u'Foo
')
24 ...
25
26 def test_suite():
27 return unittest.TestSuite((
28 unittest.makeSuite(HTMLTest),
29 ))
30
31 if __name__ == '__main__':
32 unittest.main(defaultTest='test_suite')
]]>
第2行:既然我们以文本字段作为基本类,我们就使用它的测试范例作为基本类,获得一些免费的代码。
第8行:然而,TextTest要求我们必须遵守一些规则,它要求我们必须指定_Field_Factory属性,以致于正确的字段能被测试。
第10-16行:这儿使用了allowed_ tags属性的测试校验方法。一些文本被一些空格移除,您能在完整的测试套件中看到这些代码。
第18-24行:这里我们正在尝试使用forbidden_tags 属性的校验方法。
第II步: 创建部件
部件只是一个字段的视图。因此我们把部件代码放在browser子软件包里。
我们的 HTMLSourceWidget 将会以 TextAreaWidget 作为基础并且只有转换器方法_convert(value)必须被重新实现,所以它将会把任何的不想要的标志从输入值移开( 是的,这意味值校验将贯穿于整个部件)。
(a) 实现 由于这里不需要我们创建一个新的接口,我们现在就开始我们的实现。我们开始添加widgets.py文件并插入如下内容:
]|'.join(
17 self.context.allowed_tags)
18 input = re.sub(regex, '', input)
19
20 return input
]]>
第2行:依照上面提到的,我们将以 TextAreaWidget 作为一个基本类。
第3行:这儿不需要为禁用和不被允许的标记再次重新定义规则表达式,因此,我们使用字段定义。这也将会避免部件转换器和字段校验器不同步。
第8行:我们仍然想要使用源转换,因为它会顾及终止行和一些其它常规清除。
第10-13行:如果我们发现一个被禁止的标记,只是用一些空串更换移除它。注意我们如何从部件的context(字段本身)中获得forbidden_tags属性。
第15-18行:如果我们发现一个不在被允许标志元组中的标志,然后移除它。
这个转换输入值的方法很好并且也很轻巧。
(b) 单元测试 我们通常不为高层视图代码编写单元测试,不过部件代码应该被测试,特别是转换器。在browser/tests里打开test_widgets.py文件并插入:
Blah',
16 widget._toFieldValue(u'Blah
'))
17 ...
18 self.assertEqual(u'Blah',
19 widget._toFieldValue(u'Blah
'))
20 ...
21
22 def test_ForbiddenTagsConvert(self):
23 widget = self._widget
24 widget.context.forbidden_tags=('h2','pre')
25
26 self.assertEqual(u'Blah
',
27 widget._toFieldValue(u'Blah
'))
28 ...
29 self.assertEqual(u'Blah',
30 widget._toFieldValue(u'Blah
'))
31 ...
32
33 def test_suite():
34 return unittest.TestSuite((
35 unittest.makeSuite(HTMLSourceWidgetTest),
36 ))
37
38 if __name__ == '__main__':
39 unittest.main(defaultTest='test_suite')
]]>
第2行:当然我们正在重复使用 TextAreaWidgetTest,我们重用 TextAreaWidgetTest 测试时可以省掉很多事情。
第8-9行:实现 TextAreaWidgetTest 的需求, 我们需要指定字段和我们正在使用的部件,这很有道理 ,因为widget必须有相应的field(内容)。
第12-31行:测试转换器与字段测试很相似。当前情况下我们比较输出,由于它不同于输入,因为输入基于禁止标记是否被发现。
第III步: 使用HTML 字段
现在我们有我们需要的所有东西。剩下的工作就是把他们与程序包中的其它部分进行整和。这里牵涉的如下步骤:
首先我们以一个widget为HTML字段注册了HTMLSourceWidget。下一步我们需要用HTML字段改变Imessage的接口声明。
(a) 注册Widget 我们使用zope命名空间view指令为HTML字段注册一个新的widget作为view。因此您必须在配置文件的命名空间列表中加一个zope namespace,如下:
现在加入如下指令:
]]>
第2行:由于zope:view 指令能被用于任何传输类型(例如:HTTP, WebDAV和FTP),因此注册widget给browsers (i.e. HTML)它是必需的。
第3行:部件将服务于所有字段应用IHTML。
第4行:通常表示组件就像适配器一样,有一个特定的输出接口。通常这一个接口就是 zope.interface 。 在这里我们明确的想说这个部件正是为了接受字段的输入,其它部件是显示部件(DisplayWidget)。
第5行:指定用于产生widget的factory 或class。
第6行:我们让widget公开可用,也就意味着每个用这个系统的人都可以使用这个部件。
(b) 调整 IMessage 接口
最后一步是在IMessage 接口中使用字段。 让我们决定在接口模块中哪一个属性将成为一个 HTML 字段。 当然,这个字段同样被导入了。
现在,我们想要使 IMessage 的body属性成为一个 HTML 字段。当然我们也可以让 IMessageBoard 的描述(description)字段也成为HTML 字段, 但是为了尽可能简单一些我们现在不想做这些额外的工作。因此这儿我们只需对body属性声明做一些改变(开始于第24行):
第5-9行:在这里我们把新属性增加到 IHTML 接口里。 这是我选择的有效标记, 不过您可以自由的增加或移除其中任何标记。
您已经完成所有的工作了。现在要做的就是尝试您工作的结果,重启Zope3,开始编辑一个新消息,并且看它是否接受像html或body这样的标记。您应该会发现当您保存它们的时候,这些标记已经从消息中被移除了。
练习
可以用其它的来代替我们自己HTML清除装置,我们可以利用Chris Wither的HTML Strip-o-Gram 软件包,该软件包可以在. 中找到,用这个软件包来实现HTML字段和HTMLSourceWidget 部件的新版本。
有时在消息的标题中允许HTML可能很不错,因此您需要一个HTML版本给TextLine 字段和 TextWidget,对当前转换器和校验器的实现进行改造,让消息标题和body可以使用HTML。
一些留言簿程序仅能使用HTML做为输入,因此显得很无聊和沉闷。在zwiki里,我们能利用一个系统(zope.app.renderer),系统让您选择输入的类型并且也知道如何在浏览器里渲染每种类型的输入。插入这种类型系统到留言簿程序中并且把HTML校验和转换代码与它合并。