Package: plone.tiles
plone.tilesimplements a low-level, non-Plone/Zope2-specific support for creating “tiles”.ContentsIntroductionCreating a Simple TileZCML ReferenceFurther ReadingTiles in detailA simple transient tileTile traversalTransient tile dataThe tile data managerPersistent tilesOverriding transient data with persistentTile URLsZCML directiveESI supportMarking a tile as ESI-renderedESI lookupTiles without heads or bodiesConvenience classes and placeholder renderingChangelog2.3.1 (2021-10-07)2.3.0 (2020-09-07)2.2.2 (2020-04-22)2.2.1 (2019-05-01)2.2.0 (2018-11-21)2.1 (2018-07-05)2.0.0 (2018-04-13)2.0.0b3 (2017-08-22)2.0.0b2 (2017-03-29)2.0.0b1 (2017-02-24)1.8.2 (2017-01-10)1.8.1 (2016-11-24)1.8.0 (2016-09-13)1.7.1 (2016-09-12)1.7.0 (2016-09-08)1.6.1 (2016-09-07)1.6 (2016-06-27)1.5.2 (2016-03-28)1.5.1 (2015-10-09)1.5.0 (2015-09-04)1.4.0 (2015-05-25)1.3.0 (2015-04-21)1.2 (2012-11-07)1.1 (2012-06-22)1.0 (2012-05-14)1.0a1 (2010-05-17)IntroductionFor the purposes of this package,
a tile is a browser view and an associated utility providing some metadata about that view.
The metadata includes a title and description,
an ‘add’ permission and optionally a schema interface describing configurable aspects of the tile.
The idea is that a UI (such as Mosaic) can present the user with a list of insertable tiles and optionally render a form to configure the tile upon insertion.A tile is inserted into a layout as a link:<linkrel="tile"target="placeholder"href="./@@sample.tile/tile1?option1=value1"/>The sub-path (tile1in this case) is used to set the tileidattribute.
This allows the tile to know its unique id, and, in the case of persistent tiles, look up its data.sample.tileis the name of the browser view that implements the tile.
This is made available as the__name__attribute.
Other parameters may be turned into tile data, available under thedataattribute, a dict, for regular tiles.
For persistent tiles
(those deriving from thePersistentTilebase class),
the data is fetched from annotations instead,
based on the tile id.There are three interfaces describing tiles in this package:IBasicTileis the low-level interface for tiles.
It extendsIBrowserViewto describe the semantics of the__name__andidattributes.ITiledescribes a tile that can be configured with some data.
The data is accessible via a dict calleddata.
The default implementation of this interface,plone.tiles.Tile,
will use the schema of the tile type and the query string (self.request.form) to construct that dictionary.
This interface also describes an attributeurl,
which gives the canonical tile URL,
including the id sub-path and any query string parameters.
(Note that tiles also correctly implementIAbsoluteURL.)IPersistentTiledescribes a tile that stores its configuration in object annotations,
and is needed when configuration values cannot be encoded into a query string.
The default implementation is inplone.tiles.PersistentTile.
To make it possible to have several tiles of a given type on the same layout,
the annotations are keyed by the tile__name__.Internally tiles are described byITileType.
It contains attributes for the tile name, title, description, add permission and schema (if required).A properly configured tile consists ofa utility providingITileTypewith the same name as the tile browser view.a browser view providingIBasicTileor one of its derivatives.The directive<plone:tile ... />is used to register both of these components in one go.To support creation of appropriate tile links,plone.tiles.datacontains two methods:encode()anddecode()to help turn a data dictionary into a query string and turn arequest.formdict into a data dict that complies with a tile’s schema interface.In addition to the default tile configuration sources
(transient query strings for ITile and persistent annotations for IPersistentTile),
it is possible to define custom tile configuration sources by registering dictionary like
ITileDataStorage-adapter for your context, request and tile interface.
Will be accessed dictionary like by tile data managers with tile id and its data dictionary.Creating a Simple TileThe most basic tile looks like this:fromplone.tilesimportTileclassMyTile(Tile):def__call__(self):returnu'<html><body><p>Hello world</p></body></html>'Note that the tile is expected to return a complete HTML document.
This will be interpolated into the page output according to the following rules:The contents of the tile’s<head />section is appended to the output document’s<head />section.The contents of the tile’s<body />section will replace the tile placeholder as indicated by the tile link.Note that this package doesnotprovide these interpolations.
For a Plone implementation of the interpolation algorithm, seeplone.app.blocks.If you require a persistent tile, subclassplone.tiles.PersistentTileinstead.
You may also need a schema interface if you want a configurable transient or persistent tile.To register the tile, use ZCML like this:<configurexmlns:plone="http://namespaces.plone.org/plone"><plone:tilename="sample.tile"title="A title for the tile"description="My tile's description"add_permission="my.add.Permission"schema=".interfaces.IMyTileSchema"class=".mytile.MyTile"permission="zope.Public"for="*"layer="*"/></configure>The first five attributes describe the tile by configuring an appropriateITileTypedirective.
The rest mimics the<browser:page />directive,
so you can specify atemplatefile and omit theclass, or use both atemplateandclass.If you want to register a persistent tile with a custom schema, but a template only, you can do e.g.:<plone:tilename="sample.persistenttile"title="A title for the tile"description="My tile's description"add_permission="my.add.Permission"schema=".interfaces.IMyTileSchema"class="plone.tiles.PersistentTile"template="mytile.pt"permission="zope.Public"for="*"/>If you want to override an existing tile, e.g. with a new layer or more specific context,
youmustomit the tile metadata (title, description, icon, add permission or schema).
If you include any metadata you will get a conflict error on Zope startup.
This example shows how to use a different template for our tile:<plone:tilename="sample.persistenttile"template="override.pt"permission="zope.Public"for="*"layer=".interfaces.IMyLayer"/>ZCML ReferenceTheplone:tiledirective uses the namespacexmlns:plone="http://namespaces.plone.org/plone".
In order to enable it loading of itsmeta.zcmlis needed, use:<includepackage="plone.tiles"file="meta.zcml"/>When registering a tile, in the background two registrations are done:How toaddthe tile (registered as a utility component as an instance ofplone.tiles.type.TileType).It is possible to register a tile without adding capabilities.
However, such a tile needs to be directly called, there won’t be any TTW adding possible.This registration can be done once only.This registration uses the following attributes:name(required)title(required)description(optional)icon(optional)permission(required)add_permission(required for adding capabilities)edit_permission(optional, default to add_permission)delete_permission(optional, default to add_permission)schema(optional)How torenderthe tile (as a usual page).It is possible to register different renderers for the samenamebut for different contexts (fororlayer).This registration uses the following attributes:name(required)for(optional)layer(optional)class(this ortemplateor both is required)template(this orclassor both is required)permission(required)Thedirectives attributeshave the following meaning:nameA unique, dotted name for the tile.titleA user friendly title, used when configuring the tile.descriptionA longer summary of the tile’s purpose and function.iconImage that represents tile purpose and function.permissionName of the permission required to view the tile.add_permissionName of the permission required to instantiate the tile.edit_permissionName of the permission required to modify the tile.
Defaults to theadd_permission.delete_permissionName of the permission required to remove the tile.
Defaults to theadd_permission.schemaConfiguration schema for the tile.
This is used to create standard add/edit forms.forThe interface or class this tile is available for.layerThe layer (request marker interface) the tile is available for.classClass implementing this tile. A browser view providingIBasicTileor one of its derivatives.templateThe name of a template that renders this tile.
Refers to a file containing a page template.Further ReadingSeetiles.rstanddirectives.rstfor more details.Tiles in detailTiles are a form of view component used to compose pages.
Think of a tile as a view describing one part of a page,
that can be configured with some data described by a schema and inserted into a layout via a dedicated GUI.Like a browser view, a tile can be traversed to and published on its own.
The tile should then return a full HTML page,
including a <head /> with any required resources,
and a <body /> with the visible part of the tile.
This will then be merged into the page, using a system such asplone.app.blocks.The API in this package provides support for tiles being configured according to a schema,
with data either passed on the query string (transient tiles) or retrieved from annotations (persistent tiles).Note that there is no direct UI support in this package,
so the forms that allow users to construct and edit tiles must live elsewhere.
You may be interested inplone.app.tilesandplone.app.mosaicfor that purpose.To use the package, you should first load its ZCML configuration:>>>configuration="""\... <configure
...      xmlns="http://namespaces.zope.org/zope"
...      xmlns:plone="http://namespaces.plone.org/plone"
...      i18n_domain="plone.tiles.tests">
...
...     <include package="zope.component" file="meta.zcml" />
...     <include package="zope.browserpage" file="meta.zcml" />
...
...     <include package="plone.tiles" file="meta.zcml" />
...     <include package="plone.tiles" />
...
... </configure>
... """>>>fromsiximportStringIO>>>fromzope.configurationimportxmlconfig>>>xmlconfig.xmlconfig(StringIO(configuration))A simple transient tileA basic tile is a view that implements theITileinterface.
The easiest way to do this is to subclass theTileclass:>>>fromplone.tilesimportTile>>>classSampleTile(Tile):......__name__='sample.tile'# would normally be set by a ZCML handler......def__call__(self):...return'<html><body><b>My tile</b></body></html>'The tile is a browser view:>>>fromplone.tiles.interfacesimportITile>>>ITile.implementedBy(SampleTile)True>>>fromzope.publisher.interfaces.browserimportIBrowserView>>>IBrowserView.implementedBy(SampleTile)TrueThe tile instance has a__name__attribute
(normally set at class level by the<plone:tile />ZCML directive),
as well as a propertyid.
The id may be set explicitly, either in code, or by sub-path traversal.
For example, if the tile name isexample.tile,
the id may be set totile1using an URL likehttp://example.com/foo/@@example.tile/tile1.This tile is registered as a normal browser view,
alongside a utility that provides some information about the tile itself.
Normally, this is done using the<plone:tile />directive.
Here’s how to create one manually:>>>fromplone.tiles.typeimportTileType>>>sampleTileType=TileType(...u'sample.tile',...u'Sample tile',...'dummy.Permission',...'dummy.Permission',...description=u'A tile used for testing',...schema=None)The name should match the view name and the name the utility is registered under.
The title and description may be used by the UI.
The add permission is the name of a permission that will be required to insert the tile.
The schema attribute may be used to indicate schema interface describing the tile’s configurable data - more on this below.To register a tile in ZCML, we could do:<plone:tilename="sample.tile"title="Sample tile"description="A tile used for testing"add_permission="dummy.Permission"class=".mytiles.SampleTile"for="*"permission="zope.Public"/>NoteThe tile name should be a dotted name, prefixed by a namespace you control.
It’s a good idea to use a package name for this purpose.It is also possible to specify alayerortemplatelike thebrowser:pagedirective, as well as aschema,
which we will describe below.We’ll register the sample tile directly here, for later testing.>>>fromzope.componentimportprovideAdapter,provideUtility>>>fromzope.interfaceimportInterface>>>fromplone.tiles.interfacesimportIBasicTile>>>provideUtility(sampleTileType,name=u'sample.tile')>>>provideAdapter(SampleTile,(Interface,Interface),IBasicTile,name=u'sample.tile')Tile traversalTiles are publishable as a normal browser view.
They will normally be called with a sub-path that specifies a tile id.
This allows tiles to be made aware of their instance name.
The id is unique within the page layout where the tile is used,
and may be the basis for looking up tile data.For example, a tile may be saved in a layout as a link like:<linkrel="tile"target="mytile"href="./@@sample.tile/tile1"/>(The idea here is that the tile link tells the rendering algorithm to replace
the element with idmytilewith the body of the rendered tile - seeplone.app.blocksfor details).Let’s create a sample context,
look up the view as it would be during traversal,
and verify how the tile is instantiated.>>>fromzope.componentimportgetMultiAdapter>>>fromzope.interfaceimportclassImplements>>>fromzope.interfaceimportInterface>>>fromzope.interfaceimportimplementer>>>fromzope.publisher.browserimportTestRequest>>>fromzope.annotation.interfacesimportIAnnotations>>>fromzope.annotation.interfacesimportIAttributeAnnotatable>>>classImplements(TestRequest,IAttributeAnnotatable)>>>classIContext(Interface):...pass>>>@implementer(IContext)...classContext(object):...pass>>>context=Context()>>>request=TestRequest()>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>tile=tile['tile1']# simulates sub-path traversalThe tile will now be aware of its name and id:>>>isinstance(tile,SampleTile)True>>>tile.__parent__iscontextTrue>>>tile.id'tile1'>>>tile.__name__'sample.tile'The sub-path traversal is implemented using a custom__getitem__()method.
To look up a view on a tile,
you can traverse to itafteryou’ve traversed to the id sub-path:>>>fromzope.componentimportadapts>>>fromzope.interfaceimportInterface>>>fromzope.publisher.browserimportBrowserView>>>fromzope.publisher.interfaces.browserimportIDefaultBrowserLayer>>>classTestView(BrowserView):...adapts(SampleTile,IDefaultBrowserLayer)...def__call__(self):...return'Dummy view'>>>provideAdapter(TestView,provides=Interface,name='test-view')>>>tile.idisnotNoneTrue>>>tile['test-view']()'Dummy view'If there is no view and we have an id already, we will get aKeyError:>>>tile['not-known']# doctest: +ELLIPSISTraceback(mostrecentcalllast):...KeyError:'not-known'To ensure consistency with Zope’s various tangles publication machines,
it is also possible to traverse using thepublishTraversemethod:>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>tile=tile.publishTraverse(request,'tile1')# simulates sub-path traversal>>>isinstance(tile,SampleTile)True>>>tile.__parent__iscontextTrue>>>tile.id'tile1'>>>tile.__name__'sample.tile'Transient tile dataLet us now consider how tiles may have data.
In the simplest case, tile data is passed on the query string, and described according to a schema.
A simple schema may look like:>>>importzope.schema>>>classISampleTileData(Interface):...title=zope.schema.TextLine(title=u'Tile title')...cssClass=zope.schema.ASCIILine(title=u'CSS class to apply')...count=zope.schema.Int(title=u'Number of things to show in the tile')We would normally have listed this interface when registering this tile in ZCML.
We can simply update the utility here.>>>sampleTileType.schema=ISampleTileDataTile data is represented by a simple dictionary. For example:>>>data={'title':u'My title','count':5,'cssClass':'foo'}The idea is that a tile add form is built from the schema interface, and its data saved to a dictionary.For transient tiles,
this data is then encoded into the tile query string.
To help with this,
a utility function can be used to encode a dict to a query string,
applying Zope form marshalers according to the types described in the schema:>>>fromplone.tiles.dataimportencode>>>encode(data,ISampleTileData)'title=My+title&cssClass=foo&count%3Along=5'Thecount%3Along=5bit is the encoded version ofcount:long=5.Note that not all field types may be saved.
In particular, object, interface, set or frozen set fields may not be saved, and will result in aKeyError.
Lengthy text fields or bytes fields with binary data may also be a problem.
For these types of fields,
look to use persistent tiles instead.Furthermore, the conversion may not be perfect.
For example, Zope’s form marshalers cannot distinguish between unicode and ascii fields.
Therefore, there is a correspondingdecode()method that may be used to ensure that the values match the schema:>>>marshaled={'title':u'My tile','count':5,'cssClass':u'foo'}>>>fromplone.tiles.dataimportdecode>>>sorted(decode(marshaled,ISampleTileData).items())[('count',5),('cssClass','foo'),('title','My tile')]When saved into a layout, the tile link would now look like:<linkrel="tile"target="mytile"href="./@@sample.tile/tile1?title=My+title&count%3Along=5&cssClass=foo"/>Let’s simulate traversal once more and see how the data is now available to the tile instance:>>>context=Context()>>>request=TestRequest(form={'title':u'My title','count':5,'cssClass':u'foo'})>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>tile=tile['tile1']>>>sorted(tile.data.items())[('count',5),('cssClass','foo'),('title','My title')]Notice also how the data has been properly decoded according to the schema.Transient tiles will get their data directly from the request parameters but,
if a_tiledataJSON-encoded parameter is present in the request,
this one will be used instead:>>>importjson>>>request=TestRequest(form={...'title':u'My title','count':5,'cssClass':u'foo',...'_tiledata':json.dumps({'title':u'Your title','count':6,'cssClass':u'bar'})...})>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>tile=tile['tile1']>>>sorted(tile.data.items())[('count',6),('cssClass','bar'),('title','Your title')]This way we can use transient tiles safely in contexts where the tile data can be confused with raw data coming from a form, e.g. in an edit form.The tile data managerThedataattribute is a convenience attribute to get hold of a (cached) copy of the data returned by anITileDataManager.
This interface provides three methods:get(), to return the tile’s data,set(), to update it with a new dictionary of data,
anddelete(), to delete the data.This adapter is mostly useful for writing UI around tiles.
Using our tile above, we can get the data like so:>>>fromplone.tiles.interfacesimportITileDataManager>>>dataManager=ITileDataManager(tile)>>>dataManager.get()==tile.dataTrueWe can also update the tile data:>>>dataManager.set({'count':1,'cssClass':'bar','title':u'Another title'})>>>sorted(dataManager.get().items())[('count',1),('cssClass','bar'),('title','Another title')]The data can also be deleted:>>>dataManager.delete()>>>sorted(dataManager.get().items())[('count',None),('cssClass',None),('title',None)]Note that in the case of a transient tile,
all we are doing is modifying theformdictionary of the request
(or the_tiledataparameter of this dictionary, if present).
The data needs to be encoded into the query string,
either using theencode()method or via the tile’sIAbsoluteURLadapter (see below for details).For persistent tiles, the data manager is a bit more interesting.Persistent tilesNot all types of data can be placed in a query string.
For more substantial storage requirements,
you can use persistent tiles, which store data in annotations.NoteIf you have more intricate requirements,
you can also write your ownITileDataManagerto handle data retrieval.
In this case, you probably still want to derive fromPersistentTile,
to get the appropriateIAbsoluteURLadapter, among other things.First, we need to write up annotations support.>>>fromzope.annotation.attributeimportAttributeAnnotations>>>provideAdapter(AttributeAnnotations)We also need a context that is annotatable.>>>fromzope.annotation.interfacesimportIAttributeAnnotatable>>>fromzope.interfaceimportalsoProvides>>>alsoProvides(context,IAttributeAnnotatable)Now, let’s create a persistent tile with a schema.>>>classIPersistentSampleData(Interface):...text=zope.schema.Text(title=u'Detailed text',missing_value=u'Missing!')>>>fromplone.tilesimportPersistentTile>>>classPersistentSampleTile(PersistentTile):......__name__='sample.persistenttile'# would normally be set by ZCML handler......def__call__(self):...returnu'<b>You said</b>%s'%self.data['text']>>>persistentSampleTileType=TileType(...u'sample.persistenttile',...u'Persistent sample tile',...'dummy.Permission',...'dummy.Permission',...description=u'A tile used for testing',...schema=IPersistentSampleData)>>>provideUtility(persistentSampleTileType,name=u'sample.persistenttile')>>>provideAdapter(PersistentSampleTile,(Interface,Interface),IBasicTile,name=u'sample.persistenttile')We can now traverse to the tile as before.
By default, there is no data, and the field’s missing value will be used.>>>request=TestRequest()>>>tile=getMultiAdapter((context,request),name=u'sample.persistenttile')>>>tile=tile['tile2']>>>tile.__name__'sample.persistenttile'>>>tile.id'tile2'>>>tile()'<b>You said</b> Missing!'At this point, there is nothing in the annotations for the type either:>>>list(dict(getattr(context,'__annotations__',{})).keys())[]We can write data to the context’s annotations using anITileDataManager:>>>dataManager=ITileDataManager(tile)>>>dataManager.set({'text':'Hello!'})This writes data to annotations:>>>list(dict(context.__annotations__).keys())['plone.tiles.data.tile2']>>>context.__annotations__[u'plone.tiles.data.tile2']{'text':'Hello!'}We can get this from the data manager too, of course:>>>dataManager.get(){'text':'Hello!'}Note that as with transient tiles,
thedataattribute is cached and will only be looked up once.If we now look up the tile again, we will get the new value:>>>tile=getMultiAdapter((context,request),name=u'sample.persistenttile')>>>tile=tile['tile2']>>>tile()'<b>You said</b> Hello!'>>>tile.data{'text':'Hello!'}We can also remove the annotation using the data manager:>>>dataManager.delete()>>>sorted(dict(context.__annotations__).items())# doctest: +ELLIPSIS[]Overriding transient data with persistentTo be able to re-use the same centrally managed tile based layouts for multiple context objects,
but still allow optional customization for tiles,
it’s possible to override otherwise transient tile configuration with context specific persistent configuration.This is done by either by setting a client side request header or query paramX-Tile-Persistent:>>>request=TestRequest(...form={'title':u'My title','count':5,'cssClass':u'foo',...'X-Tile-Persistent':'yes'}...)Yet, just adding the flag, doesn’t create new persistent annotations on GET requests:>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>ITileDataManager(tile)<plone.tiles.data.PersistentTileDataManagerobjectat...>>>>sorted(ITileDataManager(tile).get().items(),key=lambdax:x[0])[('count',5),('cssClass','foo'),('title','My title')]>>>list(IAnnotations(context).keys())[]That’s because the data is persistent only once it’s set:>>>data=ITileDataManager(tile).get()>>>data.update({'count':6})>>>ITileDataManager(tile).set(data)>>>list(IAnnotations(context).keys())['plone.tiles.data...']>>>sorted(list(IAnnotations(context).values())[0].items(),key=lambdax:x[0])[('count',6),('cssClass','foo'),('title','My title')]>>>sorted(ITileDataManager(tile).get().items(),key=lambdax:x[0])[('count',6),('cssClass','foo'),('title','My title')]Without the persistent flag, fixed transient data would be returned:>>>request=TestRequest(...form={'title':u'My title','count':5,'cssClass':u'foo'},...)>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>ITileDataManager(tile)<plone.tiles.data.TransientTileDataManagerobjectat...>>>>data=ITileDataManager(tile).get()>>>sorted(data.items(),key=lambdax:x[0])[('count',5),('cssClass','foo'),('title','My title')]Finally, the persistent override could also be deleted:>>>request=TestRequest(...form={'title':u'My title','count':5,'cssClass':u'foo',...'X-Tile-Persistent':'yes'}...)>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>ITileDataManager(tile)<plone.tiles.data.PersistentTileDataManagerobjectat...>>>>sorted(ITileDataManager(tile).get().items(),key=lambdax:x[0])[('count',6),('cssClass','foo'),('title','My title')]>>>ITileDataManager(tile).delete()>>>list(IAnnotations(context).keys())[]>>>sorted(ITileDataManager(tile).get().items(),key=lambdax:x[0])[('count',5),('cssClass','foo'),('title','My title')]>>>request=TestRequest(...form={'title':u'My title','count':5,'cssClass':u'foo'},...)>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>ITileDataManager(tile)<plone.tiles.data.TransientTileDataManagerobjectat...>Tile URLsAs we have seen, tiles have a canonical URL.
For transient tiles, this may also encode some tile data.If you have a tile instance and you need to know the canonical tile URL,
you can use theIAbsoluteURLAPI.For the purposes of testing,
we need to ensure that we can get an absolute URL for the context.
We’ll achieve that with a dummy adapter:>>>fromzope.interfaceimportimplementer>>>fromzope.componentimportadapter>>>fromzope.traversing.browser.interfacesimportIAbsoluteURL>>>fromzope.publisher.interfaces.httpimportIHTTPRequest>>>@implementer(IAbsoluteURL)...@adapter(IContext,IHTTPRequest)...classDummyAbsoluteURL(object):......def__init__(self,context,request):...self.context=context...self.request=request......def__unicode__(self):...returnu'http://example.com/context'...def__str__(self):...returnu'http://example.com/context'...def__call__(self):...returnself.__str__()...defbreadcrumbs(self):...return({'name':u'context','url':'http://example.com/context'},)>>>provideAdapter(DummyAbsoluteURL,name=u'absolute_url')>>>provideAdapter(DummyAbsoluteURL)>>>fromzope.traversing.browser.absoluteurlimportabsoluteURL>>>fromzope.componentimportgetMultiAdapter>>>context=Context()>>>request=TestRequest(form={'title':u'My title','count':5,'cssClass':u'foo'})>>>transientTile=getMultiAdapter((context,request),name=u'sample.tile')>>>transientTile=transientTile['tile1']>>>absoluteURL(transientTile,request)'http://example.com/context/@@sample.tile/tile1?title=My+title&cssClass=foo&count%3Along=5'>>>getMultiAdapter((transientTile,request),IAbsoluteURL).breadcrumbs()==\...({'url':'http://example.com/context','name':u'context'},...{'url':'http://example.com/context/@@sample.tile/tile1','name':'sample.tile'})TrueFor convenience, the tile URL is also available under theurlproperty:>>>transientTile.url'http://example.com/context/@@sample.tile/tile1?title=My+title&cssClass=foo&count%3Along=5'The tile absolute URL structure remains unaltered if the data is
coming from a_tiledataJSON-encoded parameter instead of from the request
parameters directly:>>>request=TestRequest(form={'_tiledata':json.dumps({'title':u'Your title','count':6,'cssClass':u'bar'})})>>>transientTile=getMultiAdapter((context,request),name=u'sample.tile')>>>transientTile=transientTile['tile1']>>>absoluteURL(transientTile,request)'http://example.com/context/@@sample.tile/tile1?title=Your+title&cssClass=bar&count%3Along=6'For persistent tiles, the are no data parameters:>>>context=Context()>>>request=TestRequest(form={'title':u'Ignored','count':0,'cssClass':u'ignored'})>>>persistentTile=getMultiAdapter((context,request),name=u'sample.persistenttile')>>>persistentTile=persistentTile['tile2']>>>absoluteURL(persistentTile,request)'http://example.com/context/@@sample.persistenttile/tile2'>>>getMultiAdapter((persistentTile,request),IAbsoluteURL).breadcrumbs()==\...({'url':'http://example.com/context','name':u'context'},...{'url':'http://example.com/context/@@sample.persistenttile/tile2','name':'sample.persistenttile'})TrueAnd again, for convenience:>>>persistentTile.url'http://example.com/context/@@sample.persistenttile/tile2'If the tile doesn’t have an id, we don’t get any sub-path:>>>request=TestRequest(form={'title':u'My title','count':5,'cssClass':u'foo'})>>>transientTile=getMultiAdapter((context,request),name=u'sample.tile')>>>absoluteURL(transientTile,request)'http://example.com/context/@@sample.tile?title=My+title&cssClass=foo&count%3Along=5'>>>request=TestRequest()>>>persistentTile=getMultiAdapter((context,request),name=u'sample.persistenttile')>>>absoluteURL(persistentTile,request)'http://example.com/context/@@sample.persistenttile'We can also disallow query parameters providing data into our tiles>>>importzope.schema>>>fromplone.tiles.directivesimportignore_querystring>>>classISampleTileData(Interface):...unfiltered=zope.schema.Text(title=u'Unfiltered data')...ignore_querystring('unfiltered')...filtered=zope.schema.Text(title=u'filtered data')>>>sampleTileType.schema=ISampleTileDataAnd create a tile with our new schema>>>fromplone.tilesimportTile>>>classSampleTile(Tile):......__name__='sample.unfilteredtile'# would normally be set by a ZCML handler......def__call__(self):...return'<html><body><div>{}{}</div></body></html>'.format(...self.data.get('unfiltered')or'',...self.data.get('filtered')or'')We’ll register the sample unfiltered tile directly here, for testing.>>>fromzope.componentimportprovideAdapter,provideUtility>>>fromzope.interfaceimportInterface>>>fromplone.tiles.interfacesimportIBasicTile>>>provideUtility(sampleTileType,name=u'sample.unfilteredtile')>>>provideAdapter(SampleTile,(Interface,Interface),IBasicTile,name=u'sample.unfilteredtile')Let’s simulate traversal to test if form data is used:>>>context=Context()>>>request=TestRequest(form={'unfiltered':'foobar','filtered':'safe'})>>>tile=getMultiAdapter((context,request),name=u'sample.unfilteredtile')>>>tile=tile['tile1']Data should not contain unfiltered field:>>>sorted(tile.data.items())[('filtered','safe')]Rendering the tile should not include ignored query string:>>>'foobar'intile()False>>>tile()'<html><body><div>safe</div></body></html>'ZCML directiveA tile is really just a browser view providingIBasicTile(or, more commonly,ITileorIPersistentTile)
coupled with a named utility providingITileType.
The names of the browser view and the tile should match.To make it easier to register these components,
this package provides a<plone:tile />directive that sets up both.
It supports several use cases:Registering a new tile from a classRegistering a new tile from a template onlyRegistering a new tile form a class and a templateRegistering a new tile for an existing tile type (e.g. for a new layer)To test this,
we have created a dummy schema and a dummy tile intesting.py,
and a dummy template intest.pt.Let’s show how these may be used by registering several tiles:>>>configuration="""\... <configure package="plone.tiles"
...      xmlns="http://namespaces.zope.org/zope"
...      xmlns:plone="http://namespaces.plone.org/plone"
...      i18n_domain="plone.tiles.tests">
...
...     <include package="zope.component" file="meta.zcml" />
...     <include package="zope.security" file="meta.zcml" />
...     <include package="zope.browserpage" file="meta.zcml" />
...
...     <include package="plone.tiles" file="meta.zcml" />
...     <include package="plone.tiles" />
...
...     <permission
...         id="plone.tiles.testing.DummyAdd"
...         title="Dummy add permission"
...         />
...     <permission
...         id="plone.tiles.testing.DummyView"
...         title="Dummy view permission"
...         />
...
...     <!-- A tile configured with all available attributes -->
...     <plone:tile
...         name="dummy1"
...         title="Dummy tile 1"
...         description="This one shows all available options"
...         add_permission="plone.tiles.testing.DummyAdd"
...         schema="plone.tiles.testing.IDummySchema"
...         class="plone.tiles.testing.DummyTileWithTemplate"
...         template="test.pt"
...         for="plone.tiles.testing.IDummyContext"
...         layer="plone.tiles.testing.IDummyLayer"
...         permission="plone.tiles.testing.DummyView"
...         />
...
...     <!-- A class-only tile -->
...     <plone:tile
...         name="dummy2"
...         title="Dummy tile 2"
...         add_permission="plone.tiles.testing.DummyAdd"
...         class="plone.tiles.testing.DummyTile"
...         for="*"
...         permission="plone.tiles.testing.DummyView"
...         />
...
...     <!-- A template-only tile -->
...     <plone:tile
...         name="dummy3"
...         title="Dummy tile 3"
...         add_permission="plone.tiles.testing.DummyAdd"
...         template="test.pt"
...         for="*"
...         permission="plone.tiles.testing.DummyView"
...         />
...
...     <!-- Use the PersistentTile class directly with a template-only tile -->
...     <plone:tile
...         name="dummy4"
...         title="Dummy tile 4"
...         add_permission="plone.tiles.testing.DummyAdd"
...         schema="plone.tiles.testing.IDummySchema"
...         class="plone.tiles.PersistentTile"
...         template="test.pt"
...         for="*"
...         permission="plone.tiles.testing.DummyView"
...         />
...
...     <!-- Override dummy3 for a new layer -->
...     <plone:tile
...         name="dummy3"
...         class="plone.tiles.testing.DummyTile"
...         for="*"
...         layer="plone.tiles.testing.IDummyLayer"
...         permission="plone.tiles.testing.DummyView"
...         />
...
... </configure>
... """>>>fromsiximportStringIO>>>fromzope.configurationimportxmlconfig>>>xmlconfig.xmlconfig(StringIO(configuration))Let’s check how the tiles were registered:>>>fromzope.componentimportgetUtility>>>fromplone.tiles.interfacesimportITileType>>>tile1_type=getUtility(ITileType,name=u'dummy1')>>>tile1_type<TileTypedummy1(Dummytile1)>>>>tile1_type.description'This one shows all available options'>>>tile1_type.add_permission'plone.tiles.testing.DummyAdd'>>>tile1_type.view_permission'plone.tiles.testing.DummyView'>>>tile1_type.schema<InterfaceClassplone.tiles.testing.IDummySchema>>>>tile2_type=getUtility(ITileType,name=u'dummy2')>>>tile2_type<TileTypedummy2(Dummytile2)>>>>tile2_type.descriptionisNoneTrue>>>tile2_type.add_permission'plone.tiles.testing.DummyAdd'>>>tile2_type.schemaisNoneTrue>>>tile3_type=getUtility(ITileType,name=u'dummy3')>>>tile3_type<TileTypedummy3(Dummytile3)>>>>tile3_type.descriptionisNoneTrue>>>tile3_type.add_permission'plone.tiles.testing.DummyAdd'>>>tile3_type.schemaisNoneTrue>>>tile4_type=getUtility(ITileType,name=u'dummy4')>>>tile4_type<TileTypedummy4(Dummytile4)>>>>tile4_type.descriptionisNoneTrue>>>tile4_type.add_permission'plone.tiles.testing.DummyAdd'>>>tile4_type.schema<InterfaceClassplone.tiles.testing.IDummySchema>Finally, let’s check that we can look up the tiles:>>>fromzope.publisher.browserimportTestRequest>>>fromzope.interfaceimportimplementer,alsoProvides>>>fromplone.tiles.testingimportIDummyContext,IDummyLayer>>>@implementer(IDummyContext)...classContext(object):...pass>>>context=Context()>>>request=TestRequest()>>>layer_request=TestRequest(skin=IDummyLayer)>>>fromzope.componentimportgetMultiAdapter>>>fromplone.tilesimportTile,PersistentTile>>>fromplone.tiles.testingimportDummyTile,DummyTileWithTemplate>>>tile1=getMultiAdapter((context,layer_request),name='dummy1')>>>isinstance(tile1,DummyTileWithTemplate)True>>>print(tile1())<b>test!</b>>>>tile1.__name__'dummy1'>>>tile2=getMultiAdapter((context,request),name='dummy2')>>>isinstance(tile2,DummyTile)True>>>print(tile2())dummy>>>tile2.__name__'dummy2'>>>tile3=getMultiAdapter((context,request),name='dummy3')>>>isinstance(tile3,Tile)True>>>print(tile3())<b>test!</b>>>>tile3.__name__'dummy3'>>>tile4=getMultiAdapter((context,request),name='dummy4')>>>isinstance(tile4,PersistentTile)True>>>print(tile4())<b>test!</b>>>>tile4.__name__'dummy4'>>>tile3_layer=getMultiAdapter((context,layer_request),name='dummy3')>>>isinstance(tile3_layer,DummyTile)True>>>print(tile3_layer())dummy>>>tile3_layer.__name__'dummy3'ESI supportSome sites may choose to render tiles in a delayed fashion using Edge Side Includes or some similar mechanism.plone.tilesincludes some support to help render ESI placeholders.
This is used inplone.app.blocksto facilitate ESI rendering.
Since ESI normally involves a “dumb” replacement operation,plone.tilesalso provides a means of accessing just the head and/or just the body of a tile.To use the package, you should first load its ZCML configuration.>>>configuration="""\... <configure
...      xmlns="http://namespaces.zope.org/zope"
...      xmlns:plone="http://namespaces.plone.org/plone"
...      i18n_domain="plone.tiles.tests">
...
...     <include package="zope.component" file="meta.zcml" />
...     <include package="zope.browserpage" file="meta.zcml" />
...
...     <include package="plone.tiles" file="meta.zcml" />
...     <include package="plone.tiles" />
...
... </configure>
... """>>>fromsiximportStringIO>>>fromzope.configurationimportxmlconfig>>>xmlconfig.xmlconfig(StringIO(configuration))Marking a tile as ESI-renderedFor ESI rendering to be available, the tile must be marked with theIESIRenderedmarker interface.
We can create a dummy tile with this interface like so:>>>fromzope.interfaceimportimplementer>>>fromplone.tiles.interfacesimportIESIRendered>>>fromplone.tilesimportTile>>>@implementer(IESIRendered)...classSampleTile(Tile):......__name__='sample.tile'# would normally be set by ZCML handler......def__call__(self):...return'<html><head><title>Title</title></head><body><b>My tile</b></body></html>'Above, we have created a simple HTML string.
This would normally be rendered using a page template.We’ll register this tile manually here.
Ordinarily, of course, it would be registered via ZCML.>>>fromplone.tiles.typeimportTileType>>>fromzope.security.permissionimportPermission>>>permission=Permission('dummy.Permission')>>>sampleTileType=TileType(...name=u'sample.tile',...title=u'Sample tile',...description=u'A tile used for testing',...add_permission='dummy.Permission',...view_permission='dummy.Permission',...schema=None)>>>fromzope.componentimportprovideAdapter,provideUtility>>>fromzope.interfaceimportInterface>>>fromplone.tiles.interfacesimportIBasicTile>>>provideUtility(permission,name=u'dummy.Permission')>>>provideUtility(sampleTileType,name=u'sample.tile')>>>provideAdapter(SampleTile,(Interface,Interface),IBasicTile,name=u'sample.tile')ESI lookupWhen a page is rendered
(for example by a system likeplone.app.blocks, but see below),
a tile placeholder may be replaced by a link such as:<esi:includesrc="/path/to/context/@@sample.tile/tile1/@@esi-body"/>When this is resolved, it will return the body part of the tile.
Equally, a tile in the head can be replaced by:<esi:includesrc="/path/to/context/@@sample.tile/tile1/@@esi-head"/>To illustrate how this works,
let’s create a sample context,
look up the view as it would be during traversal,
and instantiate the tile,
before looking up the ESI views and rendering them.>>>fromzope.interfaceimportimplementer>>>fromzope.publisher.browserimportTestRequest>>>classIContext(Interface):...pass>>>@implementer(IContext)...classContext(object):...pass>>>classIntegratedTestRequest(TestRequest):...@property...defenviron(self):...returnself._environ>>>context=Context()>>>request=IntegratedTestRequest()The following simulates traversal tocontext/@@sample.tile/tile1>>>fromzope.interfaceimportInterface>>>fromzope.componentimportgetMultiAdapter>>>tile=getMultiAdapter((context,request),name=u'sample.tile')>>>tile=tile['tile1']# simulates sub-path traversalThis tile should be ESI rendered:>>>IESIRendered.providedBy(tile)TrueAt this point, we can look up the ESI views:>>>head=getMultiAdapter((tile,request),name='esi-head')>>>head()Traceback(mostrecentcalllast):...zExceptions.unauthorized.Unauthorized:Unauthorized()But we can only render them when we have the required permissions:>>> from AccessControl.SecurityManagement import newSecurityManager
>>> from AccessControl.User import Super
>>> newSecurityManager(None, Super('manager', '', ['Manager'], []))
>>> print(head())
<title>Title</title>>>> body = getMultiAdapter((tile, request), name='esi-body')
>>> print(body())
<b>My tile</b>Tiles without heads or bodiesIn general, tiles are supposed to return full HTML documents.
Theesi-headandesi-bodyviews are tolerant of tiles that do not.
If they cannot find a<head />or<body />element, respectively, they will return the underlying tile output unaltered.For example:>>>fromplone.tiles.esiimportESITile>>>classLazyTile(ESITile):...__name__='sample.esi1'# would normally be set by ZCML handler...def__call__(self):...return'<title>Page title</title>'We won’t bother to register this for this test, instead just instantiating it directly:>>>tile=LazyTile(context,request)['tile1']>>>IESIRendered.providedBy(tile)True>>>head=getMultiAdapter((tile,request),name='esi-head')>>>print(head())<title>Pagetitle</title>Of course, the ESI body renderer would return the same thing,
since it can’t extract a specific body either:>>>body=getMultiAdapter((tile,request),name='esi-body')>>>print(body())<title>Pagetitle</title>In this case, we would likely end up with invalid HTML,
since the<title />tag is not allowed in the body.
Whether and how to resolve this is left up to the ESI interpolation implementation.Convenience classes and placeholder renderingTwo convenience base classes can be found in theplone.tiles.esimodule.
These extend the standardTileandPersistentTileclasses to provide theIESIRenderedinterface.plone.tiles.esi.ESITile, a transient, ESI-rendered tileplone.tiles.esi.ESIPersistentTile, a persistent, ESI-rendered tileThese are particularly useful if you are creating a template-only tile and want ESI rendering.
For example:<plone:tilename="sample.esitile"title="An ESI-rendered tile"add_permission="plone.tiles.tests.DummyAdd"template="esitile.pt"class="plone.tiles.esi.ESITile"for="*"permission="zope.View"/>Additionally,
these base classes implement a__call__()method that will render a tile placeholder,
if the request contains anX-ESI-Enabledheader set to the literal ‘true’.The placeholder is a simple HTML<a />tag,
which can be transformed into an<esi:include />tag using the helper functionsubstituteESILinks().
The reason for this indirection is that theesinamespace is not allowed in HTML documents,
and are liable to be stripped out by transforms using thelibxml2/lxmlHTML parser.Let us now create a simple ESI tile. To benefit from the default rendering,
we should implement therender()method instead of__call__(). Setting
a page template as theindexclass variable or using thetemplateattribute to the ZCML directive will work also.>>>fromplone.tiles.esiimportESITile>>>classSampleESITile(ESITile):...__name__='sample.esitile'# would normally be set by ZCML handler......defrender(self):...return'<html><head><title>Title</title></head><body><b>My ESI tile</b></body></html>'>>>sampleESITileType=TileType(...name=u'sample.esitile',...title=u'Sample ESI tile',...description=u'A tile used for testing ESI',...add_permission='dummy.Permission',...view_permission='dummy.Permission',...schema=None)>>>provideUtility(sampleESITileType,name=u'sample.esitile')>>>provideAdapter(SampleESITile,(Interface,Interface),IBasicTile,name=u'sample.esitile')The following simulates traversal tocontext/@@sample.esitile/tile1>>>tile=getMultiAdapter((context,request),name=u'sample.esitile')>>>tile=tile['tile1']# simulates sub-path traversalBy default, the tile renders as normal:>>>print(tile())<html><head><title>Title</title></head><body><b>MyESItile</b></body></html>However, if we opt into ESI rendering via a request header, we get a different view:>>>fromplone.tiles.interfacesimportESI_HEADER_KEY>>>request.environ[ESI_HEADER_KEY]='true'>>>print(tile())# doctest: +NORMALIZE_WHITESPACE<!DOCTYPEhtmlPUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><htmlxmlns="http://www.w3.org/1999/xhtml"><body><aclass="_esi_placeholder"rel="esi"href="http://127.0.0.1/@@esi-body?"></a></body></html>This can be transformed into a proper ESI tag withsubstituteESILinks():>>>fromplone.tiles.esiimportsubstituteESILinks>>>print(substituteESILinks(tile()))# doctest: +NORMALIZE_WHITESPACE<!DOCTYPEhtmlPUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><htmlxmlns:esi="http://www.edge-delivery.org/esi/1.0"xmlns="http://www.w3.org/1999/xhtml"><body><esi:includesrc="http://127.0.0.1/@@esi-body?"/></body></html>It is also possible to render the ESI tile for the head.
This is done with a class variable ‘head’
(which would of course normally be set within the class):>>>SampleESITile.head=True>>>print(tile())# doctest: +NORMALIZE_WHITESPACE<!DOCTYPEhtmlPUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><htmlxmlns="http://www.w3.org/1999/xhtml"><body><aclass="_esi_placeholder"rel="esi"href="http://127.0.0.1/@@esi-head?"></a></body></html>Changelog2.3.1 (2021-10-07)Bug fixes:Fix incorrectly set condition for plone.protect.
[thet] (#33)2.3.0 (2020-09-07)New features:Drop Plone 4.3 support.
[maurits] (#3130)Bug fixes:Fixed deprecation warning for ComponentLookupError.
[maurits] (#3130)2.2.2 (2020-04-22)Bug fixes:Minor packaging updates. (#1)Fixed tests on Travis.
[maurits] (#33)2.2.1 (2019-05-01)Bug fixes:Fixed issue where creating a Mosaic page with shared content layout with filled rich text fields ended up having empty rich text fields, because the rich text field is marked primary (as it should be), and primary fields were never parsed from query string by default. (#30)2.2.0 (2018-11-21)New features:Add support for Python 3. [pbauer] (#29)Bug fixes:Initialize towncrier. [gforcada] (#2548)2.1 (2018-07-05)Dependency on zope.app.publisher removed,
needed parts were moved to zope.browserpage long time ago.
This probably breaks Plone 4.2 support (removed).
[jensens]2.0.0 (2018-04-13)Fix license classifier.2.0.0b3 (2017-08-22)Breaking changes:Fix querystring en-/decoder to always skip primary fields
[datakurre]2.0.0b2 (2017-03-29)Bug fixes:Do not swallowAttributeErrorinsideindex()on template rendering.
[hvelarde]Fix code analysis errors.
[gforcada]2.0.0b1 (2017-02-24)Breaking changes:Tiles no longer add relativeX-Tile-Url-header in__call__.
Tiles still add absoluteX-Tile-Url-header during traversal, but
it gets removed after rendering when request is not CSRF-authorized.
[datakurre]Generic ESI helper check now taht the request is authorized to render
the tile according to the registered view permission fo the tile.
[datakurre]Transactions of requests to ESI helper views are automatically aborted,
because ESI requests should always be immutable GET requests
[datakurre]plone.app.theming (transform) is now disabled with X-Theme-Disabled-header
for requests rendering tiles
[datakurre]plone.protect’s ProtectTransform is skipped for tile requests with correct
CSRF token prevent its side-effects on tile editors rendering tiles
individually
[datakurre]New features:Added X-Frame-Options -header for ESI-tile views with matching behavior
with plone.protect
[datakurre]Bug fixes:Fix issue where ESI-tile helper views didn’t get correct
Cache-Control-headers, because ESI-helpers views were not acquisition
wrapped
[datakurre]1.8.2 (2017-01-10)Bug fixes:Fix issue where transient tile was unable to encode data
with list of unicode strings
[datakurre]Remove unused unittest2 requirement
[tomgross]1.8.1 (2016-11-24)Bugfix:Fix encode error in nested unicodes (like in plone.app.querystring)
[tomgross]Restructure testing
[tomgross]1.8.0 (2016-09-13)New features:Provide ignore_querystring form directive to mark particular tiles fields
that are not allowed to default data from query string data
[vangheem]1.7.1 (2016-09-12)Fix issue where collective.cover was broken, because internal changes in
default data managers
[datakurre]1.7.0 (2016-09-08)New features:Option to customize storage layer with ITileDataStorage adapter
[datakurre]1.6.1 (2016-09-07)Bug fixes:Reformat docs.
[gforcada]Add coveralls shield.
[gforcada]1.6 (2016-06-27)Let TileType instances (tile registration utility) know about the view
permission too.
[jensens]1.5.2 (2016-03-28)Fix issue where ESI href was not properly replaced.
[jensens]Add section “ZCML Reference” to README.rst.
[jensens]PEP8, code-analysis, documentation and packaging fixes.
[jensens, mauritsvanrees]1.5.1 (2015-10-09)Fix decoding List type of Choice value types
[vangheem]1.5.0 (2015-09-04)Add support for overriding transient data manager with persistent data
manager by adding X-Tile-Persistent=1 into tile URL
[datakurre]Fix persistent data manager to read its default from query string
[vangheem]1.4.0 (2015-05-25)Add support for encoding dictionary fields into tile URL
[datakurre]Fix issue where saving or deleting transient tile data mutated the current request
[datakurre]Fix issue where non-ascii characters in tile data raised UnicodeEncode/DecodeErrors
[datakurre]1.3.0 (2015-04-21)Fix edit_permission and delete_permission to default
to add_permission only in TileType constructor
[datakurre]Fix argument order in TileType constructor call
[datakurre]Fix absolute_url-adapter to fallback to relative URL
[datakurre]Add response to include absolute X-Tile-Url header
[bloodbare]1.2 (2012-11-07)Adding icon property for tiletype
[garbas]Url that we pass via X-Tile-Url should be relative to current context
[garbas]Adding support for more robust permissions for edit and delete on tiles
[cewing calvinhp]1.1 (2012-06-22)X-Tile-Uid header is passed on tile view containing tile’s id.
[garbas]PEP 8/Pyflakes (ignoring E121, E123, E126, E127 and E501).
[hvelarde]1.0 (2012-05-14)Refactor ESI support. To use theESITileandESIPersistentTilebase classes, you should either use a template assigned via ZCML or
override therender()method. Seeesi.rstfor full details.
[optilude]Internationalized title and description of the tile directive.
[vincentfretin]Use a  json-encoded parameter in transient tiles as first option.
[dukebody]Use adapters for the Zope Publisher type casting
[dukebody]Conditionaly support z3c.relationfield’s RelationChoice fields
[dukebody]Ignore type casting for fields without fixed type, like zope.schema.Choice
[dukebody]1.0a1 (2010-05-17)Initial release.