dexterity 行为 参考
dexterity 行为 参考
http://www.315ok.org/blogfolder/567
http://www.315ok.org/logo.png
dexterity 行为 参考
dexterity 行为 参考
[align=left]怎样为Dexterity 类型创建可重用的行为?[/align] 1. 简介
[align=left]关于本手册
[/align] 行为是为每一个内容类型提供一种方式:逐类型地启用或禁用一打可重用的功能。包括如下功能:
Behaviors 并不只绑定到Dexterity, 但 Dexterity 通过它的 types的behaviors FTI property提供behavior支持。为了深入了解behavior是如何实现的,可以查看 plone.behavior 包。本手册将教会你写behavior的每件事情,但不谈论behavior怎样集成到其他框架。
2. Behavior 基本
[align=left]behavior背后的基本概念[/align] 在深入实际例子前,我们需要解释下关于behavior的一些概义。从最通常的角度来看,behavior就像是一个条件adapter。 a Dexterity content type, the condition is, "is this behavior listed in the behaviors property in the FTI?" When a behavior is enabled for a particular object, it will be possible to adapt that object to the behavior's interface. If the behavior is disabled, adaptation will fail.
A behavior consist at the very least of an interface and some metadata, namely a title and a description. In most cases, there is also a factory, akin to an adapter factory, which will be invoked to get an appropriate adapter when requested. This is usually just a class that looks like any other adapter factory, although it will tend to be applicable to Interface, IContentish or a similarly broad context.
In some cases, behaviors specify a marker interface, which will be directly provided by instances for which the behavior is enabled. This is useful if you want to conditionally enable event handlers or view components, which are registered for this marker interface. Some behaviors have no factory. In this case, the behavior interface and the marker interface must be one and the same.
Behaviors are registered globally, using the <plone.behavior /> ZCML directive. This results in, among other things, a named utility providing plone.behavior.interfaces.IBehavior being registered. This utility contains various information about the behavior, such as its name, title, interface and (optional) marker interface. The utility name is the full dotted name to the behavior interface.
3. 创建并注册behaviors [align=left]怎样创建一个行为来扩展form fields[/align] 下面的例子基于 collective.gtags 产品,它提供一个行为来添加一个 tags field 到 "Categorization" fieldset,保存实际的tags 在 Dublin Core Subject 字段中。
collective.gtags是一个标准包,附带一个 configure.zcml, 一个 GenericSetup profile,和一些其他模块。我们在这里,仅仅对它的行为模块感兴趣。
First, there are a few dependencies in setup.py:
Next, we have behaviors.zcml, which is included from configure.zcml and contains all necessary configuration to set up the behaviors. It looks like this:
The next three lines include plone.directives.form and its meta.zcml file, and then invoke the grok action on the behaviors module. This is not directly related to the behavior, but rather to the configuration of a schema interface that provides form fields and display hints to plone.autoform (and thus Dexterity's standard add and edit forms). If your behavior is not a form field provider, you can omit these lines. Similarly, if you have grokked the entire package elsewhere with <grok:grok package="." />, you can omit the <grok:grok package=".behaviors" /> line. Otherwise, adjust it to reflect the module or package where your behaviors are kept.
The behavior itself is registered with the <plone:behavior /> directive. We set a title and a description, and then speicfy the behavior interface with the provides attribute. This attribute is required, and is used to construct the unique name for the behavior. In this case, the behavior name is collective.gtags.behaviors.ITags, the full dotted name to the behavior interface. When the behavior is enabled for a type, it will be possible to adapt instances of that type to ITags. That adaptation will invoke the factory specified by the factory attribute.
The behaviors.py module looks like this:
Since we want this behavior to provide form fields, we derive the behavior interface from form.Schema and set form hints using plone.directives.form (remember that these will only take effect if the package is grokked). We also mark the ITags interface with IFormFieldProvider to signal that it should be processed for form fields by the standard forms. See the Dexterity Developer Manual for more information about setting form hints in schema interfaces.
If your behavior does not provide form fields, you can just derive from zope.interface.Interface and omit the alsoProvides() line.
Next, we write the class that implements the behavior adapter and acts the adapter factory. Notice how it implements the behavior interface (ITags), and adapts a broad interface (IDublinCore). The behavior cannot be enabled on types not supporting this interface. In many cases, you will omit the adapts() line, provided your behavior is generic enough to work on any context.
The adapter is otherwise identical to any other adapter. It implements the interface, here by storing values in the Subject field. The use of getproperty and setproperty from the rwproperty package is for convenience only.
4. 提供标记接口 Providing marker interfaces [align=left]怎样为给定的内容类型的实例采用behaviors 来设置标记接口?[/align] 有时,为对象提供一个行为的同时也提供一个标记接口 是十分有意义的。 例如,你能针对特定的标记接口注册一个 viewlet,然后通过一个behavior为特定的内容类型的所有实例启用这个标记接口 。这个Viewlet将在启用该behavior的对象上被显示。 同样的道理,这套机制也可以被应用到 event handlers, views and other components。
[align=left]通常情况下,由于一个标准的behavior已经是一个条件适配器,当要启用一个定制的适配器时,无须采用标记接口。然而,在某些情况下,你可能想提供一个或更多的适配器到一个非behavior的接口。 例如由其他部件提供的一个特别的扩展。在这种情况下,可以方便地设置一个标记接口,并且为该标记接口提供一个适配器。
[/align]plone.behavior'的标记能用于下面两种情况:
Supplementary marker behaviors第二种情况,我们除了想象通常情况下一样通过一个behavior adapter factory提供一个行为接口(e.g. with some form fields and a custom storage or a few methods implemented in an adapter);而且我们也需要一个定制的标记接口。这里,在<plone:behavior />语句中,我们既用 provides属性 又用 marker 属性,来引用两个接口以及一个factory。
举一个更有意思的例子,来自一个项目的behavior能让内容作者有更特别的权限控制 (iz.EditOfficialReviewers and iz.EditUnofficialReviewers),为给定的内容类型 指定这个 "official" 和任意 "unofficial" 审核者。该 behavior提供必须的表单字段,而且也设置了一个标记接口 ,该标记接口能自动启用一个ILocalRoleProvider adapter ,以便给选定的审核者授予本地角色;另外,该标记接口也自动绑定一个适配器来完成定制indexer,以便通过catalog方便地列出 reviewers。
The ZCML registration looks like this:
This whole package is grokked, so in configure.zcml we have:
<grok:grok package="." />The reviewers.py 详细代码如下:
这是一个相当复杂的behavior,但是有希望看明白发生了什么:
5. 仅仅提供Schema扩展的行为,存储采用annotations 还是 attributes [align=left]写提供 schema fields的行为[/align] 通常,我们开发一个完成简单集合字段的行为能够被复用。系统集成人员然后可以组合不同的schemata。写 behavior schema和写其他 schema 接口没有什么不同。问题在于如何保存schema对应的字段值,以及保存在哪里?缺省情况下, plone.behavior 提供两种可选方式。
Using annotationsAnnotations由 zope.annotation 包提供,它是一种存储 key/value 对在对象上的一个标准。 In the default implementation (so-called attribute annotation), the values are stored in a BTree on the object called __annotations__. 原始的 annotations API 主要是适配一个对象到 IAnnotations i接口,其行为有点象字典,保存values 在唯一的keys下面。 plone.behavior包提供一个特别类型的工厂,可以简单地适配一个对象到一个行为接口,并且获得一个适配器实现该接口,在这个适配器,你可以读取或设置值,这些值被保存在 annotations.
我们之前已经看到了这种工厂的一个样例:
Storing attributesThis approach is convenient,这种实现方式是方便的,但是有一种更方便的实现方式 but there is another approach that is even more convenient, and, contrary to what you may think, may be more efficient: simply store the attributes of the schema interface directly on the content object.
As an example, here's the standard IRelatedItems behavior from plone.app.dexerity:
This approach has a few advantages:
[/align]
6. Testing behaviors [align=left]How to write unit tests for behaviors[/align] Behaviors, like any other code, should be tested. If you are writing a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface. However, any actual code, such as a behavior adapter factory, ought to be tested.
Writing a behavior integration test is not very difficult if you are happy to depend on Dexterity in your test. You can create a dummy type by instantiating a Dexterty FTI in portal_types and enable your behavior by adding its interface name to the behaviors property.
In many cases, however, it is better not to depend on Dexterity at all. It is not too difficult to mock what Dexterity does to enable behaviors on its types. The following example is taken from collective.gtags and tests the ITags behavior we saw on the first page of this manual.
Behaviors=========This package provides a behavior called `collective.gtags.behaviors.ITags`.This adds a `Tags` field called `tags` to the "Categorization" fieldset, witha behavior adapter that stores the chosen tags in the Subject metadata field.To learn more about the `Tags` field and how it works, see `tagging.txt`.Test setup———-Before we can run these tests, we need to load the collective.gtagsconfiguration. This will configure the behavior.
This test tries to prove that the behavior is correctly installed and works as intended on a suitable content class. It is not a true unit test, of course. For that, we would simply test the Tags adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples.
First, we configure the package. To keep the test small, we limit ourselves to the behaviors.zcml file, which in this case will suffice. We still need to include a minimal set of ZCML from Five.
Next, we implement an IBehaviorAssignable adapter. This is a low-level component used by plone.behavior to determine if a behavior is enabled on a particular object. Dexterity provides an implementation that checks the type's FTI. Our test version is much simpler - it hardcodes the supported behaviors.
With this in place, we first check that the IBehavior utility has been correctly registered. This is essentially a test to show that we've used the <plone:behavior /> directive as intended. We also verify that our schema interface is an IFormFieldsProvider. For a non-form behavior, we'd obviously omit this.
Finally, we test the behavior. We've chosen to use CMFDefault's Document type for our test, as the behavior adapter requires an object providing IDublinCore. If we were less lazy, we'd write our own class and implement IDublinCore directly. However, in many cases, the types from CMFDefault are going to provide convenient test fodder.
Obviously, if our behavior was more complex, we'd add more intricate tests. By the last section of the doctest, we have enough context to test the adapter factory.
To run the test, we need a test suite. In tests.py, we have:
[align=left]关于本手册
[/align] 行为是为每一个内容类型提供一种方式:逐类型地启用或禁用一打可重用的功能。包括如下功能:
- 一个表单字段集合(基于标准的 add and edit forms)
- 启用特定的事件handler
- 基于内容类型启用一个或更多views, viewlets 或者其他 UI components
- 能够通适配器或标记接口,在py代码中表述的任何其他东西
- 想在多个内容类型间共享字段和功能模块。 Behaviors 允许你写一次字段schema 或相关的配合部件(例如, adapters, event handlers, views, viwelets) ,然后在多个地方方便地重用它们。
- 一个有经验的开发人员通过行为开发功能,然后提供给集成人员。 例如,行为可以被打包并且作为插件发布。集成人员,安装该插件,并通过代码或WEB配置,应用特定行为到内容类型。
Behaviors 并不只绑定到Dexterity, 但 Dexterity 通过它的 types的behaviors FTI property提供behavior支持。为了深入了解behavior是如何实现的,可以查看 plone.behavior 包。本手册将教会你写behavior的每件事情,但不谈论behavior怎样集成到其他框架。
2. Behavior 基本
[align=left]behavior背后的基本概念[/align] 在深入实际例子前,我们需要解释下关于behavior的一些概义。从最通常的角度来看,behavior就像是一个条件adapter。 a Dexterity content type, the condition is, "is this behavior listed in the behaviors property in the FTI?" When a behavior is enabled for a particular object, it will be possible to adapt that object to the behavior's interface. If the behavior is disabled, adaptation will fail.
A behavior consist at the very least of an interface and some metadata, namely a title and a description. In most cases, there is also a factory, akin to an adapter factory, which will be invoked to get an appropriate adapter when requested. This is usually just a class that looks like any other adapter factory, although it will tend to be applicable to Interface, IContentish or a similarly broad context.
In some cases, behaviors specify a marker interface, which will be directly provided by instances for which the behavior is enabled. This is useful if you want to conditionally enable event handlers or view components, which are registered for this marker interface. Some behaviors have no factory. In this case, the behavior interface and the marker interface must be one and the same.
Behaviors are registered globally, using the <plone.behavior /> ZCML directive. This results in, among other things, a named utility providing plone.behavior.interfaces.IBehavior being registered. This utility contains various information about the behavior, such as its name, title, interface and (optional) marker interface. The utility name is the full dotted name to the behavior interface.
3. 创建并注册behaviors [align=left]怎样创建一个行为来扩展form fields[/align] 下面的例子基于 collective.gtags 产品,它提供一个行为来添加一个 tags field 到 "Categorization" fieldset,保存实际的tags 在 Dublin Core Subject 字段中。
collective.gtags是一个标准包,附带一个 configure.zcml, 一个 GenericSetup profile,和一些其他模块。我们在这里,仅仅对它的行为模块感兴趣。
First, there are a few dependencies in setup.py:
install_requires=[
...,
'plone.behavior',
'plone.directives.form',
'zope.schema',
'zope.interface',
'zope.component',
'rwproperty', ],The dependency on plone.directives.form is there to support form fields. If your behavior does not require form fields, you can skip this dependency. The rwproperty dependency provides some convenience decorators that are used in the behavior adapter factory class.Next, we have behaviors.zcml, which is included from configure.zcml and contains all necessary configuration to set up the behaviors. It looks like this:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:plone="http://namespaces.plone.org/plone" xmlns:grok="http://namespaces.zope.org/grok" i18n_domain="collective.gtags"> <include package="plone.behavior" file="meta.zcml" /> <include package="plone.directives.form" file="meta.zcml" /> <include package="plone.directives.form" /> <grok:grok package=".behaviors" /> <plone:behavior title="GTags" description="Use the Dublin Core Subject (keywords) field for Google Code like tags." provides=".behaviors.ITags" factory=".behaviors.Tags" /> </configure>We first include the plone.behavior meta.zcml file, so that we get access to the <plone:behavior /> ZCML directive.
The next three lines include plone.directives.form and its meta.zcml file, and then invoke the grok action on the behaviors module. This is not directly related to the behavior, but rather to the configuration of a schema interface that provides form fields and display hints to plone.autoform (and thus Dexterity's standard add and edit forms). If your behavior is not a form field provider, you can omit these lines. Similarly, if you have grokked the entire package elsewhere with <grok:grok package="." />, you can omit the <grok:grok package=".behaviors" /> line. Otherwise, adjust it to reflect the module or package where your behaviors are kept.
The behavior itself is registered with the <plone:behavior /> directive. We set a title and a description, and then speicfy the behavior interface with the provides attribute. This attribute is required, and is used to construct the unique name for the behavior. In this case, the behavior name is collective.gtags.behaviors.ITags, the full dotted name to the behavior interface. When the behavior is enabled for a type, it will be possible to adapt instances of that type to ITags. That adaptation will invoke the factory specified by the factory attribute.
The behaviors.py module looks like this:
"""Behaviours to assign tags (to ideas).Includes a form field and a behaviour adapter that stores the data in thestandard Subject field."""
from rwproperty import getproperty, setproperty
from zope.interface import implements, alsoProvides
from zope.component import adapts
from plone.directives import form
from collective.gtags.field import Tags
from Products.CMFCore.interfaces import IDublinCore
from collective.gtags import MessageFactory as _
class ITags(form.Schema):
"""Add tags to content """
form.fieldset(
'categorization',
label=_(u'Categorization'),
fields=('tags',), )
tags = Tags(
title=_(u"Tags"),
description=_(u"Applicable tags"),
required=False,
allow_uncommon=True, )
alsoProvides(ITags, form.IFormFieldProvider)
class Tags(object):
"""Store tags in the Dublin Core metadata Subject field. This makes tags easy to search for. """ implements(ITags)
adapts(IDublinCore)
def __init__(self, context):
self.context = context
@getproperty
def tags(self):
return set(self.context.Subject())
@setproperty
def tags(self, value):
if value is None:
value = ()
self.context.setSubject(tuple(value))We first define the ITags interface, which is also the behavior interface. Here, we define a single attribute, tags, but we could also have added methods and additional fields if required. Naturally, these need to be implemented by the behavior adapter.Since we want this behavior to provide form fields, we derive the behavior interface from form.Schema and set form hints using plone.directives.form (remember that these will only take effect if the package is grokked). We also mark the ITags interface with IFormFieldProvider to signal that it should be processed for form fields by the standard forms. See the Dexterity Developer Manual for more information about setting form hints in schema interfaces.
If your behavior does not provide form fields, you can just derive from zope.interface.Interface and omit the alsoProvides() line.
Next, we write the class that implements the behavior adapter and acts the adapter factory. Notice how it implements the behavior interface (ITags), and adapts a broad interface (IDublinCore). The behavior cannot be enabled on types not supporting this interface. In many cases, you will omit the adapts() line, provided your behavior is generic enough to work on any context.
The adapter is otherwise identical to any other adapter. It implements the interface, here by storing values in the Subject field. The use of getproperty and setproperty from the rwproperty package is for convenience only.
4. 提供标记接口 Providing marker interfaces [align=left]怎样为给定的内容类型的实例采用behaviors 来设置标记接口?[/align] 有时,为对象提供一个行为的同时也提供一个标记接口 是十分有意义的。 例如,你能针对特定的标记接口注册一个 viewlet,然后通过一个behavior为特定的内容类型的所有实例启用这个标记接口 。这个Viewlet将在启用该behavior的对象上被显示。 同样的道理,这套机制也可以被应用到 event handlers, views and other components。
[align=left]通常情况下,由于一个标准的behavior已经是一个条件适配器,当要启用一个定制的适配器时,无须采用标记接口。然而,在某些情况下,你可能想提供一个或更多的适配器到一个非behavior的接口。 例如由其他部件提供的一个特别的扩展。在这种情况下,可以方便地设置一个标记接口,并且为该标记接口提供一个适配器。
[/align]plone.behavior'的标记能用于下面两种情况:
- 作为这个 behavior接口本身。这种情况,没有适配器 factory。这个 behavior 接口和标记接口只有一个,并且是同一个。
- 作为一个提供者提供给一个标准 behavior adapter。这种情况下,一个 factory 被提供,并且这个 behavior interface (即这个 behavior adapter factory 实现的接口) 不同于这个标记接口。
<plone:behavior
title="Pony viewlet"
description="Shows a pony next to the content"
provides=".behaviors.IWantAPony" />One could imagine a viewlet based on plone.pony registered for the IWantAPony marker interface. If the behavior is enabled for a particular object, IWantAPony.providedBy(object) would be true.Supplementary marker behaviors第二种情况,我们除了想象通常情况下一样通过一个behavior adapter factory提供一个行为接口(e.g. with some form fields and a custom storage or a few methods implemented in an adapter);而且我们也需要一个定制的标记接口。这里,在<plone:behavior />语句中,我们既用 provides属性 又用 marker 属性,来引用两个接口以及一个factory。
举一个更有意思的例子,来自一个项目的behavior能让内容作者有更特别的权限控制 (iz.EditOfficialReviewers and iz.EditUnofficialReviewers),为给定的内容类型 指定这个 "official" 和任意 "unofficial" 审核者。该 behavior提供必须的表单字段,而且也设置了一个标记接口 ,该标记接口能自动启用一个ILocalRoleProvider adapter ,以便给选定的审核者授予本地角色;另外,该标记接口也自动绑定一个适配器来完成定制indexer,以便通过catalog方便地列出 reviewers。
The ZCML registration looks like this:
<plone:behavior
title="Reviewers"
description="The ability to assign a list of official and/or unofficial reviewers to an item, granting those users special powers."
provides=".reviewers.IReviewers"
factory="plone.behavior.AnnotationStorage"
marker=".reviewers.IReviewersMarker" />注意这个 AnnotationStorage factory的使用。这是一个可以重用的 factory ,能用来为通过schema interfaces接口创建的行为,保存行为的值在 annotations中。稍后,我们将详细解释这个。 We could just as easily have provided our own factory in this example.This whole package is grokked, so in configure.zcml we have:
<grok:grok package="." />The reviewers.py 详细代码如下:
"""Behavior to enable certain users to nominate reviewers
Includes form fields, an indexer to make it easy to find the items with
specific reviewers, and a local role provider to grant the Reviewer and
OfficialReviewer roles appropriately.
"""
from five import grok
from zope.interface import alsoProvides, Interface
from plone.directives import form
from zope import schema
from plone.formwidget.autocomplete.widget import AutocompleteMultiFieldWidget
from borg.localrole.interfaces import ILocalRoleProvider
from plone.indexer.interfaces import IIndexer
from Products.ZCatalog.interfaces import IZCatalog
from iz.behaviors import MessageFactory as _
class IReviewers(form.Schema):
"""Support for specifying official and unofficial reviewers
"""
form.fieldset(
'ownership',
label=_(u'Ownership'),
fields=('official_reviewers', 'unofficial_reviewers'),
)
form.widget(official_reviewers=AutocompleteMultiFieldWidget)
form.write_permission(official_reviewers='iz.EditOfficialReviewers')
official_reviewers = schema.Tuple(
title=_(u'Official reviewers'),
description=_(u'People or groups who may review this item in an official capacity.'),
value_type=schema.Choice(title=_(u"Principal"), source="plone.principalsource.Principals"),
required=False,
missing_value=(), # important!
)
form.widget(unofficial_reviewers=AutocompleteMultiFieldWidget)
form.write_permission(unofficial_reviewers='iz.EditUnofficialReviewers')
unofficial_reviewers = schema.Tuple(
title=_(u'Unofficial reviewers'),
description=_(u'People or groups who may review this item in a supplementary capacity'),
value_type=schema.Choice(title=_(u"Principal"), source="plone.principalsource.Principals"),
required=False,
missing_value=(), # important!
)
alsoProvides(IReviewers, form.IFormFieldProvider)
class IReviewersMarker(Interface):
"""Marker interface that will be provided by instances using the
IReviewers behavior. The ILocalRoleProvider adapter is registered for
this marker.
"""
class ReviewerLocalRoles(grok.Adapter):
"""Grant local roles to reviewers when the behavior is used.
"""
grok.implements(ILocalRoleProvider)
grok.context(IReviewersMarker)
grok.name('iz.behaviors.reviewers')
def getRoles(self, principal_id):
"""If the user is in the list of reviewers for this item, grant
the Reader, Editor and Contributor local roles.
"""
c = IReviewers(self.context, None)
if c is None or (not c.official_reviewers and not c.unofficial_reviewers):
return ()
if principal_id in c.official_reviewers:
return ('Reviewer', 'OfficialReviewer',)
elif principal_id in c.unofficial_reviewers:
return ('Reviewer',)
return ()
def getAllRoles(self):
"""Return a list of tuples (principal_id, roles), where roles is a
list of roles for the given user id.
"""
c = IReviewers(self.context, None)
if c is None or (not c.official_reviewers and not c.unofficial_reviewers):
return
seen = set ()
for principal_id in c.official_reviewers:
seen.add(principal_id)
yield (principal_id, ('Reviewer', 'OfficialReviewer'),)
for principal_id in c.unofficial_reviewers:
if principal_id not in seen:
yield (principal_id, ('Reviewer',),)
class ReviewersIndexer(grok.MultiAdapter):
"""Catalog indexer for the 'reviewers' index.
"""
grok.implements(IIndexer)
grok.adapts(IReviewersMarker, IZCatalog)
grok.name('reviewers')
def __init__(self, context, catalog):
self.reviewers = IReviewers(context)
def __call__(self):
official = self.reviewers.official_reviewers or ()
unofficial = self.reviewers.unofficial_reviewers or ()
return tuple(set(official + unofficial))Note that the iz.EditOfficialReviewers and iz.EditUnofficialReviewers permissions are defined and granted elsewhere.这是一个相当复杂的behavior,但是有希望看明白发生了什么:
- 这儿有一个标准的schema interface, 它用提供form hints,并且被标记为一个 IFormFieldProvider。它采用plone.formwidget.autocomplete widget和 plone.principalsource实现该字段。
- 我们定义了一个 marker interface (IReviewersMarker) 并且在 <plone:behavior /> 语句中用 marker 属性进行注册。
- 我们也定义了一个适配器适配该 marker 到 ILocalRoles接口,ILocalRoles接口来自borg.localrole包,这里我们采用groker语法注册这个适配器,当然,我们也能同等的zcml配置来实现。
- 同样道理,我们也定义了一个 multi-adapter 适配到 IIndexer,IIndexer由 plone.indexer包提供。同样,用groker注册该适配器。
5. 仅仅提供Schema扩展的行为,存储采用annotations 还是 attributes [align=left]写提供 schema fields的行为[/align] 通常,我们开发一个完成简单集合字段的行为能够被复用。系统集成人员然后可以组合不同的schemata。写 behavior schema和写其他 schema 接口没有什么不同。问题在于如何保存schema对应的字段值,以及保存在哪里?缺省情况下, plone.behavior 提供两种可选方式。
Using annotationsAnnotations由 zope.annotation 包提供,它是一种存储 key/value 对在对象上的一个标准。 In the default implementation (so-called attribute annotation), the values are stored in a BTree on the object called __annotations__. 原始的 annotations API 主要是适配一个对象到 IAnnotations i接口,其行为有点象字典,保存values 在唯一的keys下面。 plone.behavior包提供一个特别类型的工厂,可以简单地适配一个对象到一个行为接口,并且获得一个适配器实现该接口,在这个适配器,你可以读取或设置值,这些值被保存在 annotations.
我们之前已经看到了这种工厂的一个样例:
<plone:behavior
title="Reviewers"
description="The ability to assign a list of official and/or unofficial reviewers to an item, granting those users special powers."
provides=".reviewers.IReviewers"
factory="plone.behavior.AnnotationStorage"
marker=".reviewers.IReviewersMarkere" />Here, plone.behavior.AnnotationStorage is a behavior factory that can be used by any behavior with an interface that consists entirely of zope.schema fields. It simply stores those items in object annotations, saving you the trouble of writing your own annotation storage adapter. If you adapt an object for which the behavior is enabled to the behavior interface, you will be able to read and write values off the resultant adapter as normal.Storing attributesThis approach is convenient,这种实现方式是方便的,但是有一种更方便的实现方式 but there is another approach that is even more convenient, and, contrary to what you may think, may be more efficient: simply store the attributes of the schema interface directly on the content object.
As an example, here's the standard IRelatedItems behavior from plone.app.dexerity:
<plone:behavior
title="Related items"
description="Adds the ability to assign related items"
provides=".related.IRelatedItems"
for="plone.dexterity.interfaces.IDexterityContent" />The IRelatedItems schema looks like this:from zope.interface import alsoProvides
from z3c.relationfield.schema import RelationChoice, RelationList
from plone.formwidget.contenttree import ObjPathSourceBinder
from plone.directives import form
class IRelatedItems(form.Schema):
"""Behavior interface to make a type support related items. """
form.fieldset('categorization', label=u"Categorization",
fields=['relatedItems'])
relatedItems = RelationList(
title=u"Related Items",
default=[],
value_type=RelationChoice(title=u"Related",
source=ObjPathSourceBinder()),
required=False, )
alsoProvides(IRelatedItems, form.IFormFieldProvider)
This is a standard schema using plone.directives.form (the package is also grokked). However, notice the lack of a behavior factory. This is a directly provided "marker" interface, except that it has attributes, and so it is not actually a marker interface. The result is that the relatedItems attribute will be stored directly onto a content object when first set (usually in the add form).This approach has a few advantages:
- There is no need to write or use a separate factory, so it is a little easier to use.
- The attribute is available on the content object directly, so you can write context/relatedItems in a TAL expression, for example. This does require that it has been set at least once, though! If the schema is used in the type's add form, that will normally suffice, but old instances of the same type may not have the attribute and could raise an AttributeError.
- If the value is going to be used frequently, and especially if it is read when viewing the content object, storing it in an attribute is more efficient than storing it in an annotation. (This is because the __annotations__ BTree is a separate persistent object which has to be loaded into memory, and may push something else out of the ZODB cache.)
- The attribute name may collide with another attribute on the object, either from its class, its base schema, or another behavior. Whether this is a problem in practice depends largely on whether the name is likely to be unique. In most cases, it will probably be sufficiently unique.
- If the attribute stores a large value, it will increase memory usage, as it will be loaded into memory each time the object is fetched from the ZODB. However, you should use BLOBs or BTrees to store large values anyway. Loading an object with a BLOB or BTree does not mean loading the entire BLOB or Btree, so the memory overhead does not occur unless the whole BLOB or BTree is actually used.
[/align]
6. Testing behaviors [align=left]How to write unit tests for behaviors[/align] Behaviors, like any other code, should be tested. If you are writing a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface. However, any actual code, such as a behavior adapter factory, ought to be tested.
Writing a behavior integration test is not very difficult if you are happy to depend on Dexterity in your test. You can create a dummy type by instantiating a Dexterty FTI in portal_types and enable your behavior by adding its interface name to the behaviors property.
In many cases, however, it is better not to depend on Dexterity at all. It is not too difficult to mock what Dexterity does to enable behaviors on its types. The following example is taken from collective.gtags and tests the ITags behavior we saw on the first page of this manual.
Behaviors=========This package provides a behavior called `collective.gtags.behaviors.ITags`.This adds a `Tags` field called `tags` to the "Categorization" fieldset, witha behavior adapter that stores the chosen tags in the Subject metadata field.To learn more about the `Tags` field and how it works, see `tagging.txt`.Test setup———-Before we can run these tests, we need to load the collective.gtagsconfiguration. This will configure the behavior.
>>> configuration = """\ ... <configure ... xmlns="http://namespaces.zope.org/zope" ... i18n_domain="collective.gtags"> ... ... <include package="Products.Five" file="meta.zcml" /> ... <include package="collective.gtags" file="behaviors.zcml" /> ... ... </configure> ... """ >>> from StringIO import StringIO >>> from zope.configuration import xmlconfig >>> xmlconfig.xmlconfig(StringIO(configuration))This behavior can be enabled for any `IDublinCore`. For the purposes oftesting, we will use the CMFDefault Document type and a customIBehaviorAssignable adapter to mark the behavior as enabled. >>> from Products.CMFDefault.Document import Document >>> from plone.behavior.interfaces import IBehaviorAssignable >>> from collective.gtags.behaviors import ITags >>> from zope.component import adapts >>> from zope.interface import implements >>> class TestingAssignable(object): ... implements(IBehaviorAssignable) ... adapts(Document) ... ... enabled = [ITags] ... ... def __init__(self, context): ... self.context = context ... ... def supports(self, behavior_interface): ... return behavior_interface in self.enabled ... ... def enumerate_behaviors(self): ... for e in self.enabled: ... yield queryUtility(IBehavior, name=e.__identifier__) >>> from zope.component import provideAdapter >>> provideAdapter(TestingAssignable)Behavior installation———————We can now test that the behavior is installed when the ZCML for this packageis loaded.
>>> from zope.component import getUtility
>>> from plone.behavior.interfaces import IBehavior
>>> tags_behavior = getUtility(IBehavior, name='collective.gtags.behaviors.ITags')
>>> tags_behavior.interface
<InterfaceClass collective.gtags.behaviors.ITags>We also expect this behavior to be a form field provider. Let's verify that. >>> from plone.directives.form import IFormFieldProvider
>>> IFormFieldProvider.providedBy(tags_behavior.interface)
TrueUsing the behavior——————Let's create a content object that has this behavior enabled and check thatit works. >>> doc = Document('doc')
>>> tags_adapter = ITags(doc, None)
>>> tags_adapter is not None
TrueWe'll check that the `tags` set is built from the `Subject()` field: >>> doc.setSubject(['One', 'Two'])
>>> doc.Subject()
('One', 'Two')
>>> tags_adapter.tags == set(['One', 'Two'])
True
>>> tags_adapter.tags = set(['Two', 'Three'])
>>> doc.Subject() == ('Two', 'Three')
TrueThis test tries to prove that the behavior is correctly installed and works as intended on a suitable content class. It is not a true unit test, of course. For that, we would simply test the Tags adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples.
First, we configure the package. To keep the test small, we limit ourselves to the behaviors.zcml file, which in this case will suffice. We still need to include a minimal set of ZCML from Five.
Next, we implement an IBehaviorAssignable adapter. This is a low-level component used by plone.behavior to determine if a behavior is enabled on a particular object. Dexterity provides an implementation that checks the type's FTI. Our test version is much simpler - it hardcodes the supported behaviors.
With this in place, we first check that the IBehavior utility has been correctly registered. This is essentially a test to show that we've used the <plone:behavior /> directive as intended. We also verify that our schema interface is an IFormFieldsProvider. For a non-form behavior, we'd obviously omit this.
Finally, we test the behavior. We've chosen to use CMFDefault's Document type for our test, as the behavior adapter requires an object providing IDublinCore. If we were less lazy, we'd write our own class and implement IDublinCore directly. However, in many cases, the types from CMFDefault are going to provide convenient test fodder.
Obviously, if our behavior was more complex, we'd add more intricate tests. By the last section of the doctest, we have enough context to test the adapter factory.
To run the test, we need a test suite. In tests.py, we have:
import doctest
import unittest
from zope.testing import doctestunit
from zope.app.testing import setup
def setUp(test):
pass
def tearDown(test):
setup.placefulTearDown()
def test_suite():
return unittest.TestSuite((
doctestunit.DocFileSuite(
'behaviors.txt',
setUp=setUp, tearDown=tearDown,
optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS),
))This runs the behaviors.txt doctest from the same directory as the tests.py file. To run the test, we can use the usual test runner:$ ./bin/instance test -s collective.gtags关于标记接口的提示 注意标记接口的实现需要由Dexterty 框架提供的代码支持,不能方便地应用在重复的测试中。如果需要在测试中使用标记接口,你应该在测试代码中手动用 zope.interface.alsoProvides设置 ,或者和 Dexterity content一起写个集成测试。