plone.tiles 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plone/tiles/esi.rst ADDED
@@ -0,0 +1,325 @@
1
+ ESI support
2
+ ===========
3
+
4
+ Some sites may choose to render tiles in a delayed fashion using Edge Side Includes or some similar mechanism.
5
+ ``plone.tiles`` includes some support to help render ESI placeholders.
6
+ This is used in ``plone.app.blocks`` to facilitate ESI rendering.
7
+ Since ESI normally involves a "dumb" replacement operation,
8
+ ``plone.tiles`` also provides a means of accessing just the head and/or just the body of a tile.
9
+
10
+ To use the package, you should first load its ZCML configuration.
11
+
12
+ .. code-block:: python
13
+
14
+ >>> configuration = """\
15
+ ... <configure
16
+ ... xmlns="http://namespaces.zope.org/zope"
17
+ ... xmlns:plone="http://namespaces.plone.org/plone"
18
+ ... i18n_domain="plone.tiles.tests">
19
+ ...
20
+ ... <include package="zope.component" file="meta.zcml" />
21
+ ... <include package="zope.browserpage" file="meta.zcml" />
22
+ ...
23
+ ... <include package="plone.tiles" file="meta.zcml" />
24
+ ... <include package="plone.tiles" />
25
+ ...
26
+ ... </configure>
27
+ ... """
28
+
29
+ >>> from io import StringIO
30
+ >>> from zope.configuration import xmlconfig
31
+ >>> xmlconfig.xmlconfig(StringIO(configuration))
32
+
33
+ Marking a tile as ESI-rendered
34
+ ------------------------------
35
+
36
+ For ESI rendering to be available, the tile must be marked with the ``IESIRendered`` marker interface.
37
+ We can create a dummy tile with this interface like so:
38
+
39
+ .. code-block:: python
40
+
41
+ >>> from zope.interface import implementer
42
+ >>> from plone.tiles.interfaces import IESIRendered
43
+ >>> from plone.tiles import Tile
44
+
45
+ >>> @implementer(IESIRendered)
46
+ ... class SampleTile(Tile):
47
+ ...
48
+ ... __name__ = 'sample.tile' # would normally be set by ZCML handler
49
+ ...
50
+ ... def __call__(self):
51
+ ... return '<html><head><title>Title</title></head><body><b>My tile</b></body></html>'
52
+
53
+ Above, we have created a simple HTML string.
54
+ This would normally be rendered using a page template.
55
+
56
+ We'll register this tile manually here.
57
+ Ordinarily, of course, it would be registered via ZCML.
58
+
59
+ .. code-block:: python
60
+
61
+ >>> from plone.tiles.type import TileType
62
+ >>> from zope.security.permission import Permission
63
+ >>> permission = Permission('dummy.Permission')
64
+ >>> sampleTileType = TileType(
65
+ ... name=u'sample.tile',
66
+ ... title=u'Sample tile',
67
+ ... description=u'A tile used for testing',
68
+ ... add_permission='dummy.Permission',
69
+ ... view_permission='dummy.Permission',
70
+ ... schema=None)
71
+
72
+ >>> from zope.component import provideAdapter, provideUtility
73
+ >>> from zope.interface import Interface
74
+ >>> from plone.tiles.interfaces import IBasicTile
75
+
76
+ >>> provideUtility(permission, name=u'dummy.Permission')
77
+ >>> provideUtility(sampleTileType, name=u'sample.tile')
78
+ >>> provideAdapter(SampleTile, (Interface, Interface), IBasicTile, name=u'sample.tile')
79
+
80
+ ESI lookup
81
+ ----------
82
+
83
+ When a page is rendered
84
+ (for example by a system like ``plone.app.blocks``, but see below),
85
+ a tile placeholder may be replaced by a link such as:
86
+
87
+ .. code-block:: xml
88
+
89
+ <esi:include src="/path/to/context/@@sample.tile/tile1/@@esi-body" />
90
+
91
+ When this is resolved, it will return the body part of the tile.
92
+ Equally, a tile in the head can be replaced by:
93
+
94
+ .. code-block:: xml
95
+
96
+ <esi:include src="/path/to/context/@@sample.tile/tile1/@@esi-head" />
97
+
98
+ To illustrate how this works,
99
+ let's create a sample context,
100
+ look up the view as it would be during traversal,
101
+ and instantiate the tile,
102
+ before looking up the ESI views and rendering them.
103
+
104
+ .. code-block:: python
105
+
106
+ >>> from zope.interface import implementer
107
+ >>> from zope.publisher.browser import TestRequest
108
+
109
+ >>> class IContext(Interface):
110
+ ... pass
111
+
112
+ >>> @implementer(IContext)
113
+ ... class Context(object):
114
+ ... pass
115
+
116
+ >>> class IntegratedTestRequest(TestRequest):
117
+ ... @property
118
+ ... def environ(self):
119
+ ... return self._environ
120
+
121
+ >>> context = Context()
122
+ >>> request = IntegratedTestRequest()
123
+
124
+ The following simulates traversal to ``context/@@sample.tile/tile1``
125
+
126
+ .. code-block:: python
127
+
128
+ >>> from zope.interface import Interface
129
+ >>> from zope.component import getMultiAdapter
130
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
131
+ >>> tile = tile['tile1'] # simulates sub-path traversal
132
+
133
+ This tile should be ESI rendered:
134
+
135
+ .. code-block:: python
136
+
137
+ >>> IESIRendered.providedBy(tile)
138
+ True
139
+
140
+ At this point, we can look up the ESI views:
141
+
142
+ .. code-block:: python
143
+
144
+ >>> head = getMultiAdapter((tile, request), name='esi-head')
145
+ >>> head()
146
+ Traceback (most recent call last):
147
+ ...
148
+ zExceptions.unauthorized.Unauthorized: Unauthorized()
149
+
150
+ But we can only render them when we have the required permissions:
151
+
152
+ >>> from AccessControl.SecurityManagement import newSecurityManager
153
+ >>> from AccessControl.User import Super
154
+ >>> newSecurityManager(None, Super('manager', '', ['Manager'], []))
155
+ >>> print(head())
156
+ <title>Title</title>
157
+
158
+ >>> body = getMultiAdapter((tile, request), name='esi-body')
159
+ >>> print(body())
160
+ <b>My tile</b>
161
+
162
+ Tiles without heads or bodies
163
+ -----------------------------
164
+
165
+ In general, tiles are supposed to return full HTML documents.
166
+ The ``esi-head`` and ``esi-body`` views are tolerant of tiles that do not.
167
+ If they cannot find a ``<head />`` or ``<body />`` element, respectively, they will return the underlying tile output unaltered.
168
+
169
+ For example:
170
+
171
+ .. code-block:: python
172
+
173
+ >>> from plone.tiles.esi import ESITile
174
+ >>> class LazyTile(ESITile):
175
+ ... __name__ = 'sample.esi1' # would normally be set by ZCML handler
176
+ ... def __call__(self):
177
+ ... return '<title>Page title</title>'
178
+
179
+ We won't bother to register this for this test, instead just instantiating it directly:
180
+
181
+ .. code-block:: python
182
+
183
+ >>> tile = LazyTile(context, request)['tile1']
184
+
185
+ >>> IESIRendered.providedBy(tile)
186
+ True
187
+
188
+ >>> head = getMultiAdapter((tile, request), name='esi-head')
189
+ >>> print(head())
190
+ <title>Page title</title>
191
+
192
+ Of course, the ESI body renderer would return the same thing,
193
+ since it can't extract a specific body either:
194
+
195
+ .. code-block:: python
196
+
197
+ >>> body = getMultiAdapter((tile, request), name='esi-body')
198
+ >>> print(body())
199
+ <title>Page title</title>
200
+
201
+ In this case, we would likely end up with invalid HTML,
202
+ since the ``<title />`` tag is not allowed in the body.
203
+ Whether and how to resolve this is left up to the ESI interpolation implementation.
204
+
205
+ Convenience classes and placeholder rendering
206
+ ---------------------------------------------
207
+
208
+ Two convenience base classes can be found in the ``plone.tiles.esi`` module.
209
+ These extend the standard ``Tile`` and ``PersistentTile`` classes to provide the ``IESIRendered`` interface.
210
+
211
+ * ``plone.tiles.esi.ESITile``, a transient, ESI-rendered tile
212
+ * ``plone.tiles.esi.ESIPersistentTile``, a persistent, ESI-rendered tile
213
+
214
+ These are particularly useful if you are creating a template-only tile and want ESI rendering.
215
+ For example:
216
+
217
+ .. code-block:: xml
218
+
219
+ <plone:tile
220
+ name="sample.esitile"
221
+ title="An ESI-rendered tile"
222
+ add_permission="plone.tiles.tests.DummyAdd"
223
+ template="esitile.pt"
224
+ class="plone.tiles.esi.ESITile"
225
+ for="*"
226
+ permission="zope.View"
227
+ />
228
+
229
+ Additionally,
230
+ these base classes implement a ``__call__()`` method that will render a tile placeholder,
231
+ if the request contains an ``X-ESI-Enabled`` header set to the literal 'true'.
232
+
233
+ The placeholder is a simple HTML ``<a />`` tag,
234
+ which can be transformed into an ``<esi:include />`` tag using the helper function ``substituteESILinks()``.
235
+ The reason for this indirection is that the ``esi`` namespace is not allowed in HTML documents,
236
+ and are liable to be stripped out by transforms using the ``libxml2`` / ``lxml`` HTML parser.
237
+
238
+ Let us now create a simple ESI tile. To benefit from the default rendering,
239
+ we should implement the ``render()`` method instead of ``__call__()``. Setting
240
+ a page template as the ``index`` class variable or using the ``template``
241
+ attribute to the ZCML directive will work also.
242
+
243
+ .. code-block:: python
244
+
245
+ >>> from plone.tiles.esi import ESITile
246
+
247
+ >>> class SampleESITile(ESITile):
248
+ ... __name__ = 'sample.esitile' # would normally be set by ZCML handler
249
+ ...
250
+ ... def render(self):
251
+ ... return '<html><head><title>Title</title></head><body><b>My ESI tile</b></body></html>'
252
+
253
+ >>> sampleESITileType = TileType(
254
+ ... name=u'sample.esitile',
255
+ ... title=u'Sample ESI tile',
256
+ ... description=u'A tile used for testing ESI',
257
+ ... add_permission='dummy.Permission',
258
+ ... view_permission='dummy.Permission',
259
+ ... schema=None)
260
+
261
+ >>> provideUtility(sampleESITileType, name=u'sample.esitile')
262
+ >>> provideAdapter(SampleESITile, (Interface, Interface), IBasicTile, name=u'sample.esitile')
263
+
264
+ The following simulates traversal to ``context/@@sample.esitile/tile1``
265
+
266
+ .. code-block:: python
267
+
268
+ >>> tile = getMultiAdapter((context, request), name=u'sample.esitile')
269
+ >>> tile = tile['tile1'] # simulates sub-path traversal
270
+
271
+ By default, the tile renders as normal:
272
+
273
+ .. code-block:: python
274
+
275
+ >>> print(tile())
276
+ <html><head><title>Title</title></head><body><b>My ESI tile</b></body></html>
277
+
278
+ However, if we opt into ESI rendering via a request header, we get a different view:
279
+
280
+ .. code-block:: python
281
+
282
+ >>> from plone.tiles.interfaces import ESI_HEADER_KEY
283
+ >>> request.environ[ESI_HEADER_KEY] = 'true'
284
+ >>> print(tile()) # doctest: +NORMALIZE_WHITESPACE
285
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
286
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
287
+ <html xmlns="http://www.w3.org/1999/xhtml">
288
+ <body>
289
+ <a class="_esi_placeholder"
290
+ rel="esi"
291
+ href="http://127.0.0.1/@@esi-body?"></a>
292
+ </body>
293
+ </html>
294
+
295
+ This can be transformed into a proper ESI tag with ``substituteESILinks()``:
296
+
297
+ .. code-block:: python
298
+
299
+ >>> from plone.tiles.esi import substituteESILinks
300
+ >>> print(substituteESILinks(tile())) # doctest: +NORMALIZE_WHITESPACE
301
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
302
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
303
+ <html xmlns:esi="http://www.edge-delivery.org/esi/1.0" xmlns="http://www.w3.org/1999/xhtml">
304
+ <body>
305
+ <esi:include src="http://127.0.0.1/@@esi-body?" />
306
+ </body>
307
+ </html>
308
+
309
+ It is also possible to render the ESI tile for the head.
310
+ This is done with a class variable 'head'
311
+ (which would of course normally be set within the class):
312
+
313
+ .. code-block:: python
314
+
315
+ >>> SampleESITile.head = True
316
+ >>> print(tile()) # doctest: +NORMALIZE_WHITESPACE
317
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
318
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
319
+ <html xmlns="http://www.w3.org/1999/xhtml">
320
+ <body>
321
+ <a class="_esi_placeholder"
322
+ rel="esi"
323
+ href="http://127.0.0.1/@@esi-head?"></a>
324
+ </body>
325
+ </html>
@@ -0,0 +1,39 @@
1
+ from plone.tiles.interfaces import IFieldTypeConverter
2
+ from zope.interface import implementer
3
+
4
+
5
+ @implementer(IFieldTypeConverter)
6
+ class NoConverter:
7
+
8
+ def __init__(self, field):
9
+ self.field = field
10
+
11
+ token = None
12
+
13
+
14
+ class TextConverter(NoConverter):
15
+ token = "text"
16
+
17
+
18
+ class LongConverter(NoConverter):
19
+ token = "long"
20
+
21
+
22
+ class FloatConverter(NoConverter):
23
+ token = "float"
24
+
25
+
26
+ class BoolConverter(NoConverter):
27
+ token = "boolean"
28
+
29
+
30
+ class TupleConverter(NoConverter):
31
+ token = "tuple"
32
+
33
+
34
+ class ListConverter(NoConverter):
35
+ token = "list"
36
+
37
+
38
+ class DictConverter(NoConverter):
39
+ token = "record"
@@ -0,0 +1,172 @@
1
+ from zope.interface import Interface
2
+ from zope.interface.common.mapping import IMapping
3
+ from zope.interface.interfaces import IInterface
4
+ from zope.publisher.interfaces.browser import IBrowserView
5
+
6
+ import zope.schema
7
+
8
+
9
+ ESI_HEADER = "X-ESI-Enabled"
10
+ ESI_HEADER_KEY = "HTTP_" + ESI_HEADER.replace("-", "_").upper()
11
+
12
+
13
+ class ITileType(Interface):
14
+ """A utility that describes a type of tile"""
15
+
16
+ __name__ = zope.schema.DottedName(title="Tile name (same as utility name)")
17
+
18
+ title = zope.schema.TextLine(title="Title")
19
+
20
+ description = zope.schema.Text(title="Description", required=False)
21
+
22
+ icon = zope.schema.Text(title="Icon", required=False)
23
+
24
+ add_permission = zope.schema.Id(title="Zope 3 IPermission utility name")
25
+
26
+ schema = zope.schema.Object(
27
+ title="Tile schema",
28
+ description="Describes configurable data for this tile and allows a "
29
+ "form to be rendered to edit it. Set to None if the tile "
30
+ "has no configurable schema",
31
+ schema=IInterface,
32
+ required=False,
33
+ )
34
+
35
+
36
+ class IBasicTile(IBrowserView):
37
+ """A tile is a publishable resource that can be inserted into a site or
38
+ page layout.
39
+
40
+ The tile should be a named multi adapter on (<context>, <layer>)
41
+ providing IBasicTile.
42
+
43
+ It will normally be traversed to like this::
44
+
45
+ http://localhost:8080/plone-site/object/@@my.tile/tile1
46
+
47
+ In this case:
48
+
49
+ * The tile context is the content object at /plone-site/object.
50
+ * The ``__name__`` of the tile instance is 'my.tile'
51
+ * The ``id`` of the tile instance is 'tile1'
52
+ * The ``url`` of the tile instance is the URL as above
53
+ """
54
+
55
+ __name__ = zope.schema.DottedName(
56
+ title="The name of the type of this tile",
57
+ description="This should be a dotted name prefixed with the "
58
+ "package that defined the tile",
59
+ )
60
+
61
+ id = zope.schema.DottedName(
62
+ title="Tile instance id",
63
+ description="The id is normally set using sub-path traversal"
64
+ "A given tile type may be used multiple times on "
65
+ "the same page, each with a unique id. The id must "
66
+ "be unique even across multiple layouts for the "
67
+ "same context.",
68
+ )
69
+
70
+
71
+ class ITile(IBasicTile):
72
+ """A tile with some data (probably from a query string)."""
73
+
74
+ data = zope.schema.Dict(
75
+ title="The tile's configuration data",
76
+ description="This attribute cannot be set, but the dictionary may "
77
+ "be updated",
78
+ key_type=zope.schema.Id(title="The data element name"),
79
+ value_type=zope.schema.Field(title="The value"),
80
+ required=True,
81
+ readonly=True,
82
+ default={},
83
+ )
84
+
85
+ url = zope.schema.URI(
86
+ title="Tile URL",
87
+ description="This is the canonical URL for the tile. In the "
88
+ "case of transient tiles with data, this may "
89
+ "include a query string with parameters. Provided "
90
+ "that the `id` attribute is set, it will also "
91
+ "include a sub-path with this in it.",
92
+ )
93
+
94
+
95
+ class IPersistentTile(ITile):
96
+ """A tile with full-blown persistent data (stored in annotations)."""
97
+
98
+
99
+ class IESIRendered(Interface):
100
+ """Marker interface for tiles which are to be rendered via ESI.
101
+
102
+ Two corresponding views, @@esi-body and @@esi-head, will be made available
103
+ on the tile itself. This will return the children of the <head /> or
104
+ <body /> of the tile, respectively.
105
+
106
+ Thus, a tile marked with this interface may be replaced with an ESI
107
+ instruction like::
108
+
109
+ <esi:include src="./@@my.tile/tile-id/@@esi-body" />
110
+
111
+ When fetched, this placeholder will be replaced by the body of the tile.
112
+ """
113
+
114
+
115
+ class ITileDataManager(Interface):
116
+ """Support for getting and setting tile data dicts.
117
+
118
+ This is an adapter on a tile. The tile's id must be set.
119
+ """
120
+
121
+ def get():
122
+ """Get a dictionary with tile data for this tile. The dictionary is
123
+ disconnected from the underlying storage.
124
+ """
125
+
126
+ def set(data):
127
+ """Persist the given data dict."""
128
+
129
+ def delete():
130
+ """Delete the data record for this tile."""
131
+
132
+
133
+ class ITileDataContext(Interface):
134
+ """Indirection to help determine where persistent tiles store their data.
135
+
136
+ This is a multi-adapter on ``(context, request, tile)``. The context and
137
+ request are the same as ``tile.context`` and ``tile.request``, but these
138
+ discriminators allow the data context to be customised depending on
139
+ the context or request.
140
+
141
+ The default implementation simply returns ``tile.context``. That must
142
+ be annotatable for the default tile data storage adapter and
143
+ persistent tile ``ITileDataManager`` to work.
144
+ """
145
+
146
+
147
+ class ITileDataStorage(IMapping):
148
+ """Indirection to help determine how persistent tiles store their data.
149
+
150
+ This is a multi-adapter on ``(context, request, tile)``. The context and
151
+ request are the same as ``tile.context`` and ``tile.request``, but these
152
+ discriminators allow the data context to be customised depending on
153
+ the context or request.
154
+
155
+ The default implementation simply returns the configured zope.annotation
156
+ storage for the given context.
157
+
158
+ The adapter is expected to provide IMapping interface and be accessed
159
+ by tile data managers similarly to zope.annotation storage.
160
+ """
161
+
162
+
163
+ class IFieldTypeConverter(Interface):
164
+ """Field type converter for querystring parameters for Zope."""
165
+
166
+ token = zope.schema.TextLine(
167
+ title="Token",
168
+ description="""
169
+ String parameter appended to the field id
170
+ for the Zope Publisher to cast it.
171
+ """,
172
+ )
plone/tiles/meta.py ADDED
@@ -0,0 +1,162 @@
1
+ from plone.tiles.interfaces import ITileType
2
+ from plone.tiles.tile import Tile
3
+ from plone.tiles.type import TileType
4
+ from Products.Five.browser.metaconfigure import page
5
+ from zope import schema
6
+ from zope.component.zcml import utility
7
+ from zope.configuration.exceptions import ConfigurationError
8
+ from zope.configuration.fields import GlobalInterface
9
+ from zope.configuration.fields import GlobalObject
10
+ from zope.configuration.fields import MessageID
11
+ from zope.configuration.fields import Path
12
+ from zope.interface import Interface
13
+ from zope.publisher.interfaces.browser import IDefaultBrowserLayer
14
+ from zope.security.zcml import Permission
15
+
16
+
17
+ class ITileDirective(Interface):
18
+ """Directive which registers a new type of tile"""
19
+
20
+ name = schema.DottedName(
21
+ title="Name",
22
+ description="A unique, dotted name for the tile",
23
+ required=True,
24
+ )
25
+
26
+ title = MessageID(
27
+ title="Title",
28
+ description="A user friendly title, used when configuring the tile",
29
+ required=False,
30
+ )
31
+
32
+ description = MessageID(
33
+ title="Description",
34
+ description="A longer summary of the tile's purpose and function",
35
+ required=False,
36
+ )
37
+
38
+ icon = MessageID(
39
+ title="Icon",
40
+ description="Image that represents tile purpose and function",
41
+ required=False,
42
+ )
43
+
44
+ add_permission = Permission(
45
+ title="Add permission",
46
+ description="Name of the permission required to instantiate " "this tile",
47
+ required=False,
48
+ )
49
+
50
+ edit_permission = Permission(
51
+ title="Edit permission",
52
+ description="Name of the permission required to edit this tile",
53
+ required=False,
54
+ )
55
+
56
+ delete_permission = Permission(
57
+ title="Delete permission",
58
+ description="Name of the permission required to delete this tile",
59
+ required=False,
60
+ )
61
+
62
+ schema = GlobalInterface(
63
+ title="Configuration schema for the tile",
64
+ description="This is used to create standard add/edit forms",
65
+ required=False,
66
+ )
67
+
68
+ for_ = GlobalObject(
69
+ title="The interface or class this tile is available for",
70
+ required=False,
71
+ )
72
+
73
+ layer = GlobalInterface(title="The layer the tile is available for", required=False)
74
+
75
+ class_ = GlobalObject(
76
+ title="Class", description="Class implementing this tile", required=False
77
+ )
78
+
79
+ template = Path(
80
+ title="The name of a template that renders this tile",
81
+ description="Refers to a file containing a page template",
82
+ required=False,
83
+ )
84
+
85
+ permission = Permission(
86
+ title="View permission",
87
+ description="Name of the permission required to view this item",
88
+ required=True,
89
+ )
90
+
91
+
92
+ def tile(
93
+ _context,
94
+ name,
95
+ title=None,
96
+ description=None,
97
+ icon=None,
98
+ add_permission=None,
99
+ edit_permission=None,
100
+ delete_permission=None,
101
+ schema=None,
102
+ for_=None,
103
+ layer=None,
104
+ class_=None,
105
+ template=None,
106
+ permission=None,
107
+ ):
108
+ """Implements the <plone:tile /> directive"""
109
+ if (
110
+ title is not None
111
+ or description is not None
112
+ or icon is not None
113
+ or add_permission is not None
114
+ or schema is not None
115
+ ):
116
+ if title is None or add_permission is None:
117
+ raise ConfigurationError(
118
+ "When configuring a new type of tile, 'title' and "
119
+ "'add_permission' are required"
120
+ )
121
+ type_ = TileType(
122
+ name,
123
+ title,
124
+ add_permission,
125
+ permission,
126
+ edit_permission=edit_permission,
127
+ delete_permission=delete_permission,
128
+ description=description,
129
+ icon=icon,
130
+ schema=schema,
131
+ )
132
+
133
+ utility(_context, provides=ITileType, component=type_, name=name)
134
+
135
+ if (
136
+ for_ is not None
137
+ or layer is not None
138
+ or class_ is not None
139
+ or template is not None
140
+ ):
141
+ if class_ is None and template is None:
142
+ raise ConfigurationError(
143
+ "'class' or 'template' must be given when configuring a tile."
144
+ )
145
+
146
+ if for_ is None:
147
+ for_ = Interface
148
+ if layer is None:
149
+ layer = IDefaultBrowserLayer
150
+
151
+ if class_ is None:
152
+ class_ = Tile
153
+
154
+ page(
155
+ _context,
156
+ name=name,
157
+ permission=permission,
158
+ for_=for_,
159
+ layer=layer,
160
+ template=template,
161
+ class_=class_,
162
+ )