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.
@@ -0,0 +1,1990 @@
1
+ Metadata-Version: 2.2
2
+ Name: plone.tiles
3
+ Version: 3.0.0
4
+ Summary: APIs for managing tiles
5
+ Home-page: https://github.com/plone/plone.tiles
6
+ Author: Martin Aspeli
7
+ Author-email: optilude@gmail.com
8
+ License: GPL version 2
9
+ Keywords: plone tiles
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: Plone
13
+ Classifier: Framework :: Plone :: 6.0
14
+ Classifier: Framework :: Plone :: 6.1
15
+ Classifier: Framework :: Plone :: 6.2
16
+ Classifier: Framework :: Plone :: Core
17
+ Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Requires-Python: >=3.8
29
+ License-File: LICENSE.GPL
30
+ License-File: LICENSE.txt
31
+ Requires-Dist: plone.subrequest
32
+ Requires-Dist: setuptools
33
+ Requires-Dist: zope.annotation
34
+ Requires-Dist: zope.component
35
+ Requires-Dist: zope.configuration
36
+ Requires-Dist: zope.interface
37
+ Requires-Dist: zope.publisher
38
+ Requires-Dist: zope.schema
39
+ Requires-Dist: zope.security
40
+ Requires-Dist: zope.traversing
41
+ Requires-Dist: plone.protect
42
+ Requires-Dist: plone.supermodel
43
+ Requires-Dist: Zope
44
+ Provides-Extra: test
45
+ Requires-Dist: plone.testing[z2,zca]; extra == "test"
46
+ Requires-Dist: plone.rfc822; extra == "test"
47
+ Dynamic: author
48
+ Dynamic: author-email
49
+ Dynamic: classifier
50
+ Dynamic: description
51
+ Dynamic: home-page
52
+ Dynamic: keywords
53
+ Dynamic: license
54
+ Dynamic: provides-extra
55
+ Dynamic: requires-dist
56
+ Dynamic: requires-python
57
+ Dynamic: summary
58
+
59
+ ===========
60
+ plone.tiles
61
+ ===========
62
+
63
+ ``plone.tiles`` implements a low-level, non-Plone/Zope2-specific support for creating "tiles".
64
+
65
+ .. contents::
66
+
67
+
68
+ Introduction
69
+ ============
70
+
71
+ For the purposes of this package,
72
+ a tile is a browser view and an associated utility providing some metadata about that view.
73
+ The metadata includes a title and description,
74
+ an 'add' permission and optionally a schema interface describing configurable aspects of the tile.
75
+ 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.
76
+
77
+ A tile is inserted into a layout as a link:
78
+
79
+ .. code-block:: xml
80
+
81
+ <link rel="tile" target="placeholder" href="./@@sample.tile/tile1?option1=value1" />
82
+
83
+ The sub-path (``tile1`` in this case) is used to set the tile ``id`` attribute.
84
+ This allows the tile to know its unique id, and, in the case of persistent tiles, look up its data.
85
+ ``sample.tile`` is the name of the browser view that implements the tile.
86
+ This is made available as the ``__name__`` attribute.
87
+ Other parameters may be turned into tile data, available under the ``data`` attribute, a dict, for regular tiles.
88
+ For persistent tiles
89
+ (those deriving from the ``PersistentTile`` base class),
90
+ the data is fetched from annotations instead,
91
+ based on the tile id.
92
+
93
+ There are three interfaces describing tiles in this package:
94
+
95
+ ``IBasicTile``
96
+ is the low-level interface for tiles.
97
+ It extends ``IBrowserView`` to describe the semantics of the ``__name__`` and ``id`` attributes.
98
+ ``ITile``
99
+ describes a tile that can be configured with some data.
100
+ The data is accessible via a dict called ``data``.
101
+ The default implementation of this interface, ``plone.tiles.Tile``,
102
+ will use the schema of the tile type and the query string (``self.request.form``) to construct that dictionary.
103
+ This interface also describes an attribute ``url``,
104
+ which gives the canonical tile URL,
105
+ including the id sub-path and any query string parameters.
106
+ (Note that tiles also correctly implement ``IAbsoluteURL``.)
107
+ ``IPersistentTile``
108
+ describes a tile that stores its configuration in object annotations,
109
+ and is needed when configuration values cannot be encoded into a query string.
110
+ The default implementation is in ``plone.tiles.PersistentTile``.
111
+ To make it possible to have several tiles of a given type on the same layout,
112
+ the annotations are keyed by the tile ``__name__``.
113
+
114
+ Internally tiles are described by ``ITileType``.
115
+ It contains attributes for the tile name, title, description, add permission and schema (if required).
116
+
117
+ A properly configured tile consists of
118
+
119
+ - a utility providing ``ITileType`` with the same name as the tile browser view.
120
+ - a browser view providing ``IBasicTile`` or one of its derivatives.
121
+
122
+ The directive ``<plone:tile ... />`` is used to register both of these components in one go.
123
+
124
+ To support creation of appropriate tile links, ``plone.tiles.data`` contains two methods:
125
+
126
+ 1) ``encode()`` and
127
+ 2) ``decode()``
128
+
129
+ to help turn a data dictionary into a query string and turn a `request.form` dict into a data dict that complies with a tile's schema interface.
130
+
131
+ In addition to the default tile configuration sources
132
+ (transient query strings for ITile and persistent annotations for IPersistentTile),
133
+ it is possible to define custom tile configuration sources by registering dictionary like
134
+ ITileDataStorage-adapter for your context, request and tile interface.
135
+ Will be accessed dictionary like by tile data managers with tile id and its data dictionary.
136
+
137
+ Creating a Simple Tile
138
+ ----------------------
139
+
140
+ The most basic tile looks like this:
141
+
142
+ .. code-block:: python
143
+
144
+ from plone.tiles import Tile
145
+
146
+ class MyTile(Tile):
147
+
148
+ def __call__(self):
149
+ return u'<html><body><p>Hello world</p></body></html>'
150
+
151
+ Note that the tile is expected to return a complete HTML document.
152
+ This will be interpolated into the page output according to the following rules:
153
+
154
+ * The contents of the tile's ``<head />`` section is appended to the output document's ``<head />`` section.
155
+ * The contents of the tile's ``<body />`` section will replace the tile placeholder as indicated by the tile link.
156
+
157
+ Note that this package does *not* provide these interpolations.
158
+ For a Plone implementation of the interpolation algorithm, see `plone.app.blocks`_.
159
+
160
+ If you require a persistent tile, subclass ``plone.tiles.PersistentTile`` instead.
161
+ You may also need a schema interface if you want a configurable transient or persistent tile.
162
+
163
+ To register the tile, use ZCML like this:
164
+
165
+ .. code-block:: xml
166
+
167
+ <configure xmlns:plone="http://namespaces.plone.org/plone">
168
+
169
+ <plone:tile
170
+ name="sample.tile"
171
+
172
+ title="A title for the tile"
173
+ description="My tile's description"
174
+ add_permission="my.add.Permission"
175
+ schema=".interfaces.IMyTileSchema"
176
+
177
+ class=".mytile.MyTile"
178
+ permission="zope.Public"
179
+ for="*"
180
+ layer="*"
181
+ />
182
+
183
+ </configure>
184
+
185
+ The first five attributes describe the tile by configuring an appropriate ``ITileType`` directive.
186
+ The rest mimics the ``<browser:page />`` directive,
187
+ so you can specify a ``template`` file and omit the ``class``, or use both a ``template`` and ``class``.
188
+
189
+ If you want to register a persistent tile with a custom schema, but a template only, you can do e.g.:
190
+
191
+ .. code-block:: xml
192
+
193
+ <plone:tile
194
+ name="sample.persistenttile"
195
+ title="A title for the tile"
196
+ description="My tile's description"
197
+ add_permission="my.add.Permission"
198
+ schema=".interfaces.IMyTileSchema"
199
+ class="plone.tiles.PersistentTile"
200
+ template="mytile.pt"
201
+ permission="zope.Public"
202
+ for="*"
203
+ />
204
+
205
+ If you want to override an existing tile, e.g. with a new layer or more specific context,
206
+ you *must* omit the tile metadata (title, description, icon, add permission or schema).
207
+ If you include any metadata you will get a conflict error on Zope startup.
208
+ This example shows how to use a different template for our tile:
209
+
210
+ .. code-block:: xml
211
+
212
+ <plone:tile
213
+ name="sample.persistenttile"
214
+ template="override.pt"
215
+ permission="zope.Public"
216
+ for="*"
217
+ layer=".interfaces.IMyLayer"
218
+ />
219
+
220
+ ZCML Reference
221
+ --------------
222
+
223
+ The ``plone:tile`` directive uses the namespace ``xmlns:plone="http://namespaces.plone.org/plone"``.
224
+ In order to enable it loading of its ``meta.zcml`` is needed, use:
225
+
226
+ .. code-block:: xml
227
+
228
+ <include package="plone.tiles" file="meta.zcml" />
229
+
230
+ When registering a tile, in the background two registrations are done:
231
+
232
+ 1) How to **add** the tile (registered as a utility component as an instance of ``plone.tiles.type.TileType``).
233
+
234
+ It is possible to register a tile without adding capabilities.
235
+ However, such a tile needs to be directly called, there won't be any TTW adding possible.
236
+
237
+ This registration can be done once only.
238
+
239
+ This registration uses the following attributes:
240
+
241
+ - ``name`` (required)
242
+ - ``title`` (required)
243
+ - ``description`` (optional)
244
+ - ``icon`` (optional)
245
+ - ``permission`` (required)
246
+ - ``add_permission`` (required for adding capabilities)
247
+ - ``edit_permission`` (optional, default to add_permission)
248
+ - ``delete_permission`` (optional, default to add_permission)
249
+ - ``schema`` (optional)
250
+
251
+ 2) How to **render** the tile (as a usual page).
252
+
253
+ It is possible to register different renderers for the same ``name`` but for different contexts (``for`` or ``layer``).
254
+
255
+ This registration uses the following attributes:
256
+
257
+ - ``name`` (required)
258
+ - ``for`` (optional)
259
+ - ``layer`` (optional)
260
+ - ``class`` (this or ``template`` or both is required)
261
+ - ``template`` (this or ``class`` or both is required)
262
+ - ``permission`` (required)
263
+
264
+ The **directives attributes** have the following meaning:
265
+
266
+ ``name``
267
+ A unique, dotted name for the tile.
268
+
269
+ ``title``
270
+ A user friendly title, used when configuring the tile.
271
+
272
+ ``description``
273
+ A longer summary of the tile's purpose and function.
274
+
275
+ ``icon``
276
+ Image that represents tile purpose and function.
277
+
278
+ ``permission``
279
+ Name of the permission required to view the tile.
280
+
281
+ ``add_permission``
282
+ Name of the permission required to instantiate the tile.
283
+
284
+ ``edit_permission``
285
+ Name of the permission required to modify the tile.
286
+ Defaults to the ``add_permission``.
287
+
288
+ ``delete_permission``
289
+ Name of the permission required to remove the tile.
290
+ Defaults to the ``add_permission``.
291
+
292
+ ``schema``
293
+ Configuration schema for the tile.
294
+ This is used to create standard add/edit forms.
295
+
296
+ ``for``
297
+ The interface or class this tile is available for.
298
+
299
+ ``layer``
300
+ The layer (request marker interface) the tile is available for.
301
+
302
+ ``class``
303
+ Class implementing this tile. A browser view providing ``IBasicTile`` or one of its derivatives.
304
+
305
+ ``template``
306
+ The name of a template that renders this tile.
307
+ Refers to a file containing a page template.
308
+
309
+
310
+ Further Reading
311
+ ---------------
312
+
313
+ See `tiles.rst` and `directives.rst` for more details.
314
+
315
+ .. _plone.app.blocks: http://pypi.python.org/pypi/plone.app.blocks
316
+
317
+
318
+ Tiles in detail
319
+ ===============
320
+
321
+ Tiles are a form of view component used to compose pages.
322
+ Think of a tile as a view describing one part of a page,
323
+ that can be configured with some data described by a schema and inserted into a layout via a dedicated GUI.
324
+
325
+ Like a browser view, a tile can be traversed to and published on its own.
326
+ The tile should then return a full HTML page,
327
+ including a <head /> with any required resources,
328
+ and a <body /> with the visible part of the tile.
329
+ This will then be merged into the page, using a system such as ``plone.app.blocks``.
330
+
331
+ The API in this package provides support for tiles being configured according to a schema,
332
+ with data either passed on the query string (transient tiles) or retrieved from annotations (persistent tiles).
333
+
334
+ Note that there is no direct UI support in this package,
335
+ so the forms that allow users to construct and edit tiles must live elsewhere.
336
+ You may be interested in ``plone.app.tiles`` and ``plone.app.mosaic`` for that purpose.
337
+
338
+ To use the package, you should first load its ZCML configuration:
339
+
340
+ .. code-block:: python
341
+
342
+ >>> configuration = """\
343
+ ... <configure
344
+ ... xmlns="http://namespaces.zope.org/zope"
345
+ ... xmlns:plone="http://namespaces.plone.org/plone"
346
+ ... i18n_domain="plone.tiles.tests">
347
+ ...
348
+ ... <include package="zope.component" file="meta.zcml" />
349
+ ... <include package="zope.browserpage" file="meta.zcml" />
350
+ ...
351
+ ... <include package="plone.tiles" file="meta.zcml" />
352
+ ... <include package="plone.tiles" />
353
+ ...
354
+ ... </configure>
355
+ ... """
356
+
357
+ >>> from io import StringIO
358
+ >>> from zope.configuration import xmlconfig
359
+ >>> xmlconfig.xmlconfig(StringIO(configuration))
360
+
361
+ A simple transient tile
362
+ -----------------------
363
+
364
+ A basic tile is a view that implements the ``ITile`` interface.
365
+ The easiest way to do this is to subclass the ``Tile`` class:
366
+
367
+ .. code-block:: python
368
+
369
+ >>> from plone.tiles import Tile
370
+ >>> class SampleTile(Tile):
371
+ ...
372
+ ... __name__ = 'sample.tile' # would normally be set by a ZCML handler
373
+ ...
374
+ ... def __call__(self):
375
+ ... return '<html><body><b>My tile</b></body></html>'
376
+
377
+ The tile is a browser view:
378
+
379
+ .. code-block:: python
380
+
381
+ >>> from plone.tiles.interfaces import ITile
382
+ >>> ITile.implementedBy(SampleTile)
383
+ True
384
+
385
+ >>> from zope.publisher.interfaces.browser import IBrowserView
386
+ >>> IBrowserView.implementedBy(SampleTile)
387
+ True
388
+
389
+ The tile instance has a ``__name__`` attribute
390
+ (normally set at class level by the ``<plone:tile />`` ZCML directive),
391
+ as well as a property ``id``.
392
+ The id may be set explicitly, either in code, or by sub-path traversal.
393
+ For example, if the tile name is ``example.tile``,
394
+ the id may be set to ``tile1`` using an URL like ``http://example.com/foo/@@example.tile/tile1``.
395
+
396
+ This tile is registered as a normal browser view,
397
+ alongside a utility that provides some information about the tile itself.
398
+ Normally, this is done using the ``<plone:tile />`` directive.
399
+ Here's how to create one manually:
400
+
401
+ .. code-block:: python
402
+
403
+ >>> from plone.tiles.type import TileType
404
+ >>> sampleTileType = TileType(
405
+ ... u'sample.tile',
406
+ ... u'Sample tile',
407
+ ... 'dummy.Permission',
408
+ ... 'dummy.Permission',
409
+ ... description=u'A tile used for testing',
410
+ ... schema=None)
411
+
412
+ The name should match the view name and the name the utility is registered under.
413
+ The title and description may be used by the UI.
414
+ The add permission is the name of a permission that will be required to insert the tile.
415
+ The schema attribute may be used to indicate schema interface describing the tile's configurable data - more on this below.
416
+
417
+ To register a tile in ZCML, we could do:
418
+
419
+ .. code-block:: xml
420
+
421
+ <plone:tile
422
+ name="sample.tile"
423
+ title="Sample tile"
424
+ description="A tile used for testing"
425
+ add_permission="dummy.Permission"
426
+ class=".mytiles.SampleTile"
427
+ for="*"
428
+ permission="zope.Public"
429
+ />
430
+
431
+ .. note:: The tile name should be a dotted name, prefixed by a namespace you control.
432
+ It's a good idea to use a package name for this purpose.
433
+
434
+ It is also possible to specify a ``layer`` or ``template`` like the ``browser:page`` directive, as well as a ``schema``,
435
+ which we will describe below.
436
+
437
+ We'll register the sample tile directly here, for later testing.
438
+
439
+ .. code-block:: python
440
+
441
+ >>> from zope.component import provideAdapter, provideUtility
442
+ >>> from zope.interface import Interface
443
+ >>> from plone.tiles.interfaces import IBasicTile
444
+
445
+ >>> provideUtility(sampleTileType, name=u'sample.tile')
446
+ >>> provideAdapter(SampleTile, (Interface, Interface), IBasicTile, name=u'sample.tile')
447
+
448
+ Tile traversal
449
+ --------------
450
+
451
+ Tiles are publishable as a normal browser view.
452
+ They will normally be called with a sub-path that specifies a tile id.
453
+ This allows tiles to be made aware of their instance name.
454
+ The id is unique within the page layout where the tile is used,
455
+ and may be the basis for looking up tile data.
456
+
457
+ For example, a tile may be saved in a layout as a link like:
458
+
459
+ .. code-block:: html
460
+
461
+ <link rel="tile" target="mytile" href="./@@sample.tile/tile1" />
462
+
463
+ (The idea here is that the tile link tells the rendering algorithm to replace
464
+ the element with id ``mytile`` with the body of the rendered tile - see
465
+ ``plone.app.blocks`` for details).
466
+
467
+ Let's create a sample context,
468
+ look up the view as it would be during traversal,
469
+ and verify how the tile is instantiated.
470
+
471
+ .. code-block:: python
472
+
473
+ >>> from zope.component import getMultiAdapter
474
+ >>> from zope.interface import classImplements
475
+ >>> from zope.interface import Interface
476
+ >>> from zope.interface import implementer
477
+ >>> from zope.publisher.browser import TestRequest
478
+ >>> from zope.annotation.interfaces import IAnnotations
479
+ >>> from zope.annotation.interfaces import IAttributeAnnotatable
480
+ >>> classImplements(TestRequest, IAttributeAnnotatable)
481
+
482
+ >>> class IContext(Interface):
483
+ ... pass
484
+
485
+ >>> @implementer(IContext)
486
+ ... class Context(object):
487
+ ... pass
488
+
489
+ >>> context = Context()
490
+ >>> request = TestRequest()
491
+
492
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
493
+ >>> tile = tile['tile1'] # simulates sub-path traversal
494
+
495
+ The tile will now be aware of its name and id:
496
+
497
+ .. code-block:: python
498
+
499
+ >>> isinstance(tile, SampleTile)
500
+ True
501
+ >>> tile.__parent__ is context
502
+ True
503
+ >>> tile.id
504
+ 'tile1'
505
+ >>> tile.__name__
506
+ 'sample.tile'
507
+
508
+ The sub-path traversal is implemented using a custom ``__getitem__()`` method.
509
+ To look up a view on a tile,
510
+ you can traverse to it *after* you've traversed to the id sub-path:
511
+
512
+ .. code-block:: python
513
+
514
+ >>> from zope.component import adapts
515
+ >>> from zope.interface import Interface
516
+ >>> from zope.publisher.browser import BrowserView
517
+ >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
518
+
519
+ >>> class TestView(BrowserView):
520
+ ... adapts(SampleTile, IDefaultBrowserLayer)
521
+ ... def __call__(self):
522
+ ... return 'Dummy view'
523
+
524
+ >>> provideAdapter(TestView, provides=Interface, name='test-view')
525
+
526
+ >>> tile.id is not None
527
+ True
528
+ >>> tile['test-view']()
529
+ 'Dummy view'
530
+
531
+ If there is no view and we have an id already, we will get a ``KeyError``:
532
+
533
+ .. code-block:: python
534
+
535
+ >>> tile['not-known'] # doctest: +ELLIPSIS
536
+ Traceback (most recent call last):
537
+ ...
538
+ KeyError: 'not-known'
539
+
540
+ To ensure consistency with Zope's various tangles publication machines,
541
+ it is also possible to traverse using the ``publishTraverse`` method:
542
+
543
+ .. code-block:: python
544
+
545
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
546
+ >>> tile = tile.publishTraverse(request, 'tile1') # simulates sub-path traversal
547
+
548
+ >>> isinstance(tile, SampleTile)
549
+ True
550
+ >>> tile.__parent__ is context
551
+ True
552
+ >>> tile.id
553
+ 'tile1'
554
+ >>> tile.__name__
555
+ 'sample.tile'
556
+
557
+ Transient tile data
558
+ -------------------
559
+
560
+ Let us now consider how tiles may have data.
561
+ In the simplest case, tile data is passed on the query string, and described according to a schema.
562
+ A simple schema may look like:
563
+
564
+ .. code-block:: python
565
+
566
+ >>> import zope.schema
567
+
568
+ >>> class ISampleTileData(Interface):
569
+ ... title = zope.schema.TextLine(title=u'Tile title')
570
+ ... cssClass = zope.schema.ASCIILine(title=u'CSS class to apply')
571
+ ... count = zope.schema.Int(title=u'Number of things to show in the tile')
572
+
573
+ We would normally have listed this interface when registering this tile in ZCML.
574
+ We can simply update the utility here.
575
+
576
+ .. code-block:: python
577
+
578
+ >>> sampleTileType.schema = ISampleTileData
579
+
580
+ Tile data is represented by a simple dictionary. For example:
581
+
582
+ .. code-block:: python
583
+
584
+ >>> data = {'title': u'My title', 'count': 5, 'cssClass': 'foo'}
585
+
586
+ The idea is that a tile add form is built from the schema interface, and its data saved to a dictionary.
587
+
588
+ For transient tiles,
589
+ this data is then encoded into the tile query string.
590
+ To help with this,
591
+ a utility function can be used to encode a dict to a query string,
592
+ applying Zope form marshalers according to the types described in the schema:
593
+
594
+ .. code-block:: python
595
+
596
+ >>> from plone.tiles.data import encode
597
+ >>> encode(data, ISampleTileData)
598
+ 'title=My+title&cssClass=foo&count%3Along=5'
599
+
600
+ The ``count%3Along=5`` bit is the encoded version of ``count:long=5``.
601
+
602
+ Note that not all field types may be saved.
603
+ In particular, object, interface, set or frozen set fields may not be saved, and will result in a ``KeyError``.
604
+ Lengthy text fields or bytes fields with binary data may also be a problem.
605
+ For these types of fields,
606
+ look to use persistent tiles instead.
607
+
608
+ Furthermore, the conversion may not be perfect.
609
+ For example, Zope's form marshalers cannot distinguish between unicode and ascii fields.
610
+ Therefore, there is a corresponding ``decode()`` method that may be used to ensure that the values match the schema:
611
+
612
+ .. code-block:: python
613
+
614
+ >>> marshaled = {'title': u'My tile', 'count': 5, 'cssClass': u'foo'}
615
+
616
+ >>> from plone.tiles.data import decode
617
+ >>> sorted(decode(marshaled, ISampleTileData).items())
618
+ [('count', 5), ('cssClass', 'foo'), ('title', 'My tile')]
619
+
620
+ When saved into a layout, the tile link would now look like:
621
+
622
+ .. code-block:: html
623
+
624
+ <link rel="tile" target="mytile"
625
+ href="./@@sample.tile/tile1?title=My+title&count%3Along=5&cssClass=foo" />
626
+
627
+ Let's simulate traversal once more and see how the data is now available to the tile instance:
628
+
629
+ .. code-block:: python
630
+
631
+ >>> context = Context()
632
+ >>> request = TestRequest(form={'title': u'My title', 'count': 5, 'cssClass': u'foo'})
633
+
634
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
635
+ >>> tile = tile['tile1']
636
+
637
+ >>> sorted(tile.data.items())
638
+ [('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
639
+
640
+ Notice also how the data has been properly decoded according to the schema.
641
+
642
+ Transient tiles will get their data directly from the request parameters but,
643
+ if a `_tiledata` JSON-encoded parameter is present in the request,
644
+ this one will be used instead:
645
+
646
+ .. code-block:: python
647
+
648
+ >>> import json
649
+
650
+ >>> request = TestRequest(form={
651
+ ... 'title': u'My title', 'count': 5, 'cssClass': u'foo',
652
+ ... '_tiledata': json.dumps({'title': u'Your title', 'count': 6, 'cssClass': u'bar'})
653
+ ... })
654
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
655
+ >>> tile = tile['tile1']
656
+
657
+ >>> sorted(tile.data.items())
658
+ [('count', 6), ('cssClass', 'bar'), ('title', 'Your title')]
659
+
660
+ 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.
661
+
662
+ The tile data manager
663
+ ---------------------
664
+
665
+ The ``data`` attribute is a convenience attribute to get hold of a (cached) copy of the data returned by an ``ITileDataManager``.
666
+ This interface provides three methods:
667
+ ``get()``, to return the tile's data,
668
+ ``set()``, to update it with a new dictionary of data,
669
+ and ``delete()``, to delete the data.
670
+
671
+ This adapter is mostly useful for writing UI around tiles.
672
+ Using our tile above, we can get the data like so:
673
+
674
+ .. code-block:: python
675
+
676
+ >>> from plone.tiles.interfaces import ITileDataManager
677
+ >>> dataManager = ITileDataManager(tile)
678
+ >>> dataManager.get() == tile.data
679
+ True
680
+
681
+ We can also update the tile data:
682
+
683
+ .. code-block:: python
684
+
685
+ >>> dataManager.set({'count': 1, 'cssClass': 'bar', 'title': u'Another title'})
686
+ >>> sorted(dataManager.get().items())
687
+ [('count', 1), ('cssClass', 'bar'), ('title', 'Another title')]
688
+
689
+ The data can also be deleted:
690
+
691
+ .. code-block:: python
692
+
693
+ >>> dataManager.delete()
694
+ >>> sorted(dataManager.get().items())
695
+ [('count', None), ('cssClass', None), ('title', None)]
696
+
697
+ Note that in the case of a transient tile,
698
+ all we are doing is modifying the ``form`` dictionary of the request
699
+ (or the `_tiledata` parameter of this dictionary, if present).
700
+ The data needs to be encoded into the query string,
701
+ either using the ``encode()`` method or via the tile's ``IAbsoluteURL`` adapter (see below for details).
702
+
703
+ For persistent tiles, the data manager is a bit more interesting.
704
+
705
+ Persistent tiles
706
+ ----------------
707
+
708
+ Not all types of data can be placed in a query string.
709
+ For more substantial storage requirements,
710
+ you can use persistent tiles, which store data in annotations.
711
+
712
+ .. note:: If you have more intricate requirements,
713
+ you can also write your own ``ITileDataManager`` to handle data retrieval.
714
+ In this case, you probably still want to derive from ``PersistentTile``,
715
+ to get the appropriate ``IAbsoluteURL`` adapter, among other things.
716
+
717
+ First, we need to write up annotations support.
718
+
719
+ .. code-block:: python
720
+
721
+ >>> from zope.annotation.attribute import AttributeAnnotations
722
+ >>> provideAdapter(AttributeAnnotations)
723
+
724
+ We also need a context that is annotatable.
725
+
726
+ .. code-block:: python
727
+
728
+ >>> from zope.annotation.interfaces import IAttributeAnnotatable
729
+ >>> from zope.interface import alsoProvides
730
+ >>> alsoProvides(context, IAttributeAnnotatable)
731
+
732
+ Now, let's create a persistent tile with a schema.
733
+
734
+ .. code-block:: python
735
+
736
+ >>> class IPersistentSampleData(Interface):
737
+ ... text = zope.schema.Text(title=u'Detailed text', missing_value=u'Missing!')
738
+
739
+ >>> from plone.tiles import PersistentTile
740
+ >>> class PersistentSampleTile(PersistentTile):
741
+ ...
742
+ ... __name__ = 'sample.persistenttile' # would normally be set by ZCML handler
743
+ ...
744
+ ... def __call__(self):
745
+ ... return u'<b>You said</b> %s' % self.data['text']
746
+
747
+ >>> persistentSampleTileType = TileType(
748
+ ... u'sample.persistenttile',
749
+ ... u'Persistent sample tile',
750
+ ... 'dummy.Permission',
751
+ ... 'dummy.Permission',
752
+ ... description=u'A tile used for testing',
753
+ ... schema=IPersistentSampleData)
754
+
755
+ >>> provideUtility(persistentSampleTileType, name=u'sample.persistenttile')
756
+ >>> provideAdapter(PersistentSampleTile, (Interface, Interface), IBasicTile, name=u'sample.persistenttile')
757
+
758
+ We can now traverse to the tile as before.
759
+ By default, there is no data, and the field's missing value will be used.
760
+
761
+ .. code-block:: python
762
+
763
+ >>> request = TestRequest()
764
+
765
+ >>> tile = getMultiAdapter((context, request), name=u'sample.persistenttile')
766
+ >>> tile = tile['tile2']
767
+ >>> tile.__name__
768
+ 'sample.persistenttile'
769
+ >>> tile.id
770
+ 'tile2'
771
+
772
+ >>> tile()
773
+ '<b>You said</b> Missing!'
774
+
775
+ At this point, there is nothing in the annotations for the type either:
776
+
777
+ .. code-block:: python
778
+
779
+ >>> list(dict(getattr(context, '__annotations__', {})).keys())
780
+ []
781
+
782
+ We can write data to the context's annotations using an ``ITileDataManager``:
783
+
784
+ .. code-block:: python
785
+
786
+ >>> dataManager = ITileDataManager(tile)
787
+ >>> dataManager.set({'text': 'Hello!'})
788
+
789
+ This writes data to annotations:
790
+
791
+ .. code-block:: python
792
+
793
+ >>> list(dict(context.__annotations__).keys())
794
+ ['plone.tiles.data.tile2']
795
+ >>> context.__annotations__[u'plone.tiles.data.tile2']
796
+ {'text': 'Hello!'}
797
+
798
+ We can get this from the data manager too, of course:
799
+
800
+ .. code-block:: python
801
+
802
+ >>> dataManager.get()
803
+ {'text': 'Hello!'}
804
+
805
+ Note that as with transient tiles,
806
+ the ``data`` attribute is cached and will only be looked up once.
807
+
808
+ If we now look up the tile again, we will get the new value:
809
+
810
+ .. code-block:: python
811
+
812
+ >>> tile = getMultiAdapter((context, request), name=u'sample.persistenttile')
813
+ >>> tile = tile['tile2']
814
+ >>> tile()
815
+ '<b>You said</b> Hello!'
816
+
817
+ >>> tile.data
818
+ {'text': 'Hello!'}
819
+
820
+ We can also remove the annotation using the data manager:
821
+
822
+ .. code-block:: python
823
+
824
+ >>> dataManager.delete()
825
+ >>> sorted(dict(context.__annotations__).items()) # doctest: +ELLIPSIS
826
+ []
827
+
828
+ Overriding transient data with persistent
829
+ -----------------------------------------
830
+
831
+ To be able to reuse the same centrally managed tile based layouts for multiple context objects,
832
+ but still allow optional customization for tiles,
833
+ it's possible to override otherwise transient tile configuration with context specific persistent configuration.
834
+
835
+ This is done by either by setting a client side request header or query param ``X-Tile-Persistent``:
836
+
837
+ .. code-block:: python
838
+
839
+ >>> request = TestRequest(
840
+ ... form={'title': u'My title', 'count': 5, 'cssClass': u'foo',
841
+ ... 'X-Tile-Persistent': 'yes'}
842
+ ... )
843
+
844
+ Yet, just adding the flag, doesn't create new persistent annotations on GET requests:
845
+
846
+ .. code-block:: python
847
+
848
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
849
+ >>> ITileDataManager(tile)
850
+ <plone.tiles.data.PersistentTileDataManager object at ...>
851
+
852
+ >>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
853
+ [('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
854
+
855
+ >>> list(IAnnotations(context).keys())
856
+ []
857
+
858
+ That's because the data is persistent only once it's set:
859
+
860
+ .. code-block:: python
861
+
862
+ >>> data = ITileDataManager(tile).get()
863
+ >>> data.update({'count': 6})
864
+ >>> ITileDataManager(tile).set(data)
865
+ >>> list(IAnnotations(context).keys())
866
+ ['plone.tiles.data...']
867
+
868
+ >>> sorted(list(IAnnotations(context).values())[0].items(), key=lambda x: x[0])
869
+ [('count', 6), ('cssClass', 'foo'), ('title', 'My title')]
870
+
871
+ >>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
872
+ [('count', 6), ('cssClass', 'foo'), ('title', 'My title')]
873
+
874
+ Without the persistent flag, fixed transient data would be returned:
875
+
876
+ .. code-block:: python
877
+
878
+ >>> request = TestRequest(
879
+ ... form={'title': u'My title', 'count': 5, 'cssClass': u'foo'},
880
+ ... )
881
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
882
+ >>> ITileDataManager(tile)
883
+ <plone.tiles.data.TransientTileDataManager object at ...>
884
+
885
+ >>> data = ITileDataManager(tile).get()
886
+ >>> sorted(data.items(), key=lambda x: x[0])
887
+ [('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
888
+
889
+ Finally, the persistent override could also be deleted:
890
+
891
+ .. code-block:: python
892
+
893
+ >>> request = TestRequest(
894
+ ... form={'title': u'My title', 'count': 5, 'cssClass': u'foo',
895
+ ... 'X-Tile-Persistent': 'yes'}
896
+ ... )
897
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
898
+ >>> ITileDataManager(tile)
899
+ <plone.tiles.data.PersistentTileDataManager object at ...>
900
+
901
+ >>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
902
+ [('count', 6), ('cssClass', 'foo'), ('title', 'My title')]
903
+
904
+ >>> ITileDataManager(tile).delete()
905
+ >>> list(IAnnotations(context).keys())
906
+ []
907
+
908
+ >>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
909
+ [('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
910
+
911
+ >>> request = TestRequest(
912
+ ... form={'title': u'My title', 'count': 5, 'cssClass': u'foo'},
913
+ ... )
914
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
915
+ >>> ITileDataManager(tile)
916
+ <plone.tiles.data.TransientTileDataManager object at ...>
917
+
918
+
919
+ Tile URLs
920
+ ---------
921
+
922
+ As we have seen, tiles have a canonical URL.
923
+ For transient tiles, this may also encode some tile data.
924
+
925
+ If you have a tile instance and you need to know the canonical tile URL,
926
+ you can use the ``IAbsoluteURL`` API.
927
+
928
+ For the purposes of testing,
929
+ we need to ensure that we can get an absolute URL for the context.
930
+ We'll achieve that with a dummy adapter:
931
+
932
+ .. code-block:: python
933
+
934
+ >>> from zope.interface import implementer
935
+ >>> from zope.component import adapter
936
+
937
+ >>> from zope.traversing.browser.interfaces import IAbsoluteURL
938
+ >>> from zope.publisher.interfaces.http import IHTTPRequest
939
+
940
+ >>> @implementer(IAbsoluteURL)
941
+ ... @adapter(IContext, IHTTPRequest)
942
+ ... class DummyAbsoluteURL(object):
943
+ ...
944
+ ... def __init__(self, context, request):
945
+ ... self.context = context
946
+ ... self.request = request
947
+ ...
948
+ ... def __unicode__(self):
949
+ ... return u'http://example.com/context'
950
+ ... def __str__(self):
951
+ ... return u'http://example.com/context'
952
+ ... def __call__(self):
953
+ ... return self.__str__()
954
+ ... def breadcrumbs(self):
955
+ ... return ({'name': u'context', 'url': 'http://example.com/context'},)
956
+ >>> provideAdapter(DummyAbsoluteURL, name=u'absolute_url')
957
+ >>> provideAdapter(DummyAbsoluteURL)
958
+
959
+ >>> from zope.traversing.browser.absoluteurl import absoluteURL
960
+ >>> from zope.component import getMultiAdapter
961
+
962
+ >>> context = Context()
963
+ >>> request = TestRequest(form={'title': u'My title', 'count': 5, 'cssClass': u'foo'})
964
+ >>> transientTile = getMultiAdapter((context, request), name=u'sample.tile')
965
+ >>> transientTile = transientTile['tile1']
966
+
967
+ >>> absoluteURL(transientTile, request)
968
+ 'http://example.com/context/@@sample.tile/tile1?title=My+title&cssClass=foo&count%3Along=5'
969
+
970
+ >>> getMultiAdapter((transientTile, request), IAbsoluteURL).breadcrumbs() == \
971
+ ... ({'url': 'http://example.com/context', 'name': u'context'},
972
+ ... {'url': 'http://example.com/context/@@sample.tile/tile1', 'name': 'sample.tile'})
973
+ True
974
+
975
+ For convenience, the tile URL is also available under the ``url`` property:
976
+
977
+ .. code-block:: python
978
+
979
+ >>> transientTile.url
980
+ 'http://example.com/context/@@sample.tile/tile1?title=My+title&cssClass=foo&count%3Along=5'
981
+
982
+ The tile absolute URL structure remains unaltered if the data is
983
+ coming from a `_tiledata` JSON-encoded parameter instead of from the request
984
+ parameters directly:
985
+
986
+ .. code-block:: python
987
+
988
+ >>> request = TestRequest(form={'_tiledata': json.dumps({'title': u'Your title', 'count': 6, 'cssClass': u'bar'})})
989
+ >>> transientTile = getMultiAdapter((context, request), name=u'sample.tile')
990
+ >>> transientTile = transientTile['tile1']
991
+
992
+ >>> absoluteURL(transientTile, request)
993
+ 'http://example.com/context/@@sample.tile/tile1?title=Your+title&cssClass=bar&count%3Along=6'
994
+
995
+ For persistent tiles, the are no data parameters:
996
+
997
+ .. code-block:: python
998
+
999
+ >>> context = Context()
1000
+ >>> request = TestRequest(form={'title': u'Ignored', 'count': 0, 'cssClass': u'ignored'})
1001
+ >>> persistentTile = getMultiAdapter((context, request), name=u'sample.persistenttile')
1002
+ >>> persistentTile = persistentTile['tile2']
1003
+
1004
+ >>> absoluteURL(persistentTile, request)
1005
+ 'http://example.com/context/@@sample.persistenttile/tile2'
1006
+
1007
+ >>> getMultiAdapter((persistentTile, request), IAbsoluteURL).breadcrumbs() == \
1008
+ ... ({'url': 'http://example.com/context', 'name': u'context'},
1009
+ ... {'url': 'http://example.com/context/@@sample.persistenttile/tile2', 'name': 'sample.persistenttile'})
1010
+ True
1011
+
1012
+ And again, for convenience:
1013
+
1014
+ .. code-block:: python
1015
+
1016
+ >>> persistentTile.url
1017
+ 'http://example.com/context/@@sample.persistenttile/tile2'
1018
+
1019
+ If the tile doesn't have an id, we don't get any sub-path:
1020
+
1021
+ .. code-block:: python
1022
+
1023
+ >>> request = TestRequest(form={'title': u'My title', 'count': 5, 'cssClass': u'foo'})
1024
+ >>> transientTile = getMultiAdapter((context, request), name=u'sample.tile')
1025
+ >>> absoluteURL(transientTile, request)
1026
+ 'http://example.com/context/@@sample.tile?title=My+title&cssClass=foo&count%3Along=5'
1027
+
1028
+ >>> request = TestRequest()
1029
+ >>> persistentTile = getMultiAdapter((context, request), name=u'sample.persistenttile')
1030
+ >>> absoluteURL(persistentTile, request)
1031
+ 'http://example.com/context/@@sample.persistenttile'
1032
+
1033
+
1034
+ We can also disallow query parameters providing data into our tiles
1035
+
1036
+ .. code-block:: python
1037
+
1038
+ >>> import zope.schema
1039
+ >>> from plone.tiles.directives import ignore_querystring
1040
+
1041
+ >>> class ISampleTileData(Interface):
1042
+ ... unfiltered = zope.schema.Text(title=u'Unfiltered data')
1043
+ ... ignore_querystring('unfiltered')
1044
+ ... filtered = zope.schema.Text(title=u'filtered data')
1045
+
1046
+ >>> sampleTileType.schema = ISampleTileData
1047
+
1048
+ And create a tile with our new schema
1049
+
1050
+ .. code-block:: python
1051
+
1052
+ >>> from plone.tiles import Tile
1053
+ >>> class SampleTile(Tile):
1054
+ ...
1055
+ ... __name__ = 'sample.unfilteredtile' # would normally be set by a ZCML handler
1056
+ ...
1057
+ ... def __call__(self):
1058
+ ... return '<html><body><div>{}{}</div></body></html>'.format(
1059
+ ... self.data.get('unfiltered') or '',
1060
+ ... self.data.get('filtered') or '')
1061
+
1062
+ We'll register the sample unfiltered tile directly here, for testing.
1063
+
1064
+ .. code-block:: python
1065
+
1066
+ >>> from zope.component import provideAdapter, provideUtility
1067
+ >>> from zope.interface import Interface
1068
+ >>> from plone.tiles.interfaces import IBasicTile
1069
+
1070
+ >>> provideUtility(sampleTileType, name=u'sample.unfilteredtile')
1071
+ >>> provideAdapter(SampleTile, (Interface, Interface), IBasicTile, name=u'sample.unfilteredtile')
1072
+
1073
+
1074
+ Let's simulate traversal to test if form data is used:
1075
+
1076
+ .. code-block:: python
1077
+
1078
+ >>> context = Context()
1079
+ >>> request = TestRequest(form={'unfiltered': 'foobar', 'filtered': 'safe'})
1080
+
1081
+ >>> tile = getMultiAdapter((context, request), name=u'sample.unfilteredtile')
1082
+ >>> tile = tile['tile1']
1083
+
1084
+ Data should not contain unfiltered field:
1085
+
1086
+ .. code-block:: python
1087
+
1088
+ >>> sorted(tile.data.items())
1089
+ [('filtered', 'safe')]
1090
+
1091
+
1092
+ Rendering the tile should not include ignored query string:
1093
+
1094
+ .. code-block:: python
1095
+
1096
+ >>> 'foobar' in tile()
1097
+ False
1098
+
1099
+ >>> tile()
1100
+ '<html><body><div>safe</div></body></html>'
1101
+
1102
+ ZCML directive
1103
+ ==============
1104
+
1105
+ A tile is really just a browser view providing ``IBasicTile``
1106
+ (or, more commonly, ``ITile`` or ``IPersistentTile``)
1107
+ coupled with a named utility providing ``ITileType``.
1108
+ The names of the browser view and the tile should match.
1109
+
1110
+ To make it easier to register these components,
1111
+ this package provides a ``<plone:tile />`` directive that sets up both.
1112
+ It supports several use cases:
1113
+
1114
+ * Registering a new tile from a class
1115
+ * Registering a new tile from a template only
1116
+ * Registering a new tile form a class and a template
1117
+ * Registering a new tile for an existing tile type (e.g. for a new layer)
1118
+
1119
+ To test this,
1120
+ we have created a dummy schema and a dummy tile in ``testing.py``,
1121
+ and a dummy template in ``test.pt``.
1122
+
1123
+ Let's show how these may be used by registering several tiles:
1124
+
1125
+ .. code-block:: python
1126
+
1127
+ >>> configuration = """\
1128
+ ... <configure package="plone.tiles"
1129
+ ... xmlns="http://namespaces.zope.org/zope"
1130
+ ... xmlns:plone="http://namespaces.plone.org/plone"
1131
+ ... i18n_domain="plone.tiles.tests">
1132
+ ...
1133
+ ... <include package="zope.component" file="meta.zcml" />
1134
+ ... <include package="zope.security" file="meta.zcml" />
1135
+ ... <include package="zope.browserpage" file="meta.zcml" />
1136
+ ...
1137
+ ... <include package="plone.tiles" file="meta.zcml" />
1138
+ ... <include package="plone.tiles" />
1139
+ ...
1140
+ ... <permission
1141
+ ... id="plone.tiles.testing.DummyAdd"
1142
+ ... title="Dummy add permission"
1143
+ ... />
1144
+ ... <permission
1145
+ ... id="plone.tiles.testing.DummyView"
1146
+ ... title="Dummy view permission"
1147
+ ... />
1148
+ ...
1149
+ ... <!-- A tile configured with all available attributes -->
1150
+ ... <plone:tile
1151
+ ... name="dummy1"
1152
+ ... title="Dummy tile 1"
1153
+ ... description="This one shows all available options"
1154
+ ... add_permission="plone.tiles.testing.DummyAdd"
1155
+ ... schema="plone.tiles.testing.IDummySchema"
1156
+ ... class="plone.tiles.testing.DummyTileWithTemplate"
1157
+ ... template="test.pt"
1158
+ ... for="plone.tiles.testing.IDummyContext"
1159
+ ... layer="plone.tiles.testing.IDummyLayer"
1160
+ ... permission="plone.tiles.testing.DummyView"
1161
+ ... />
1162
+ ...
1163
+ ... <!-- A class-only tile -->
1164
+ ... <plone:tile
1165
+ ... name="dummy2"
1166
+ ... title="Dummy tile 2"
1167
+ ... add_permission="plone.tiles.testing.DummyAdd"
1168
+ ... class="plone.tiles.testing.DummyTile"
1169
+ ... for="*"
1170
+ ... permission="plone.tiles.testing.DummyView"
1171
+ ... />
1172
+ ...
1173
+ ... <!-- A template-only tile -->
1174
+ ... <plone:tile
1175
+ ... name="dummy3"
1176
+ ... title="Dummy tile 3"
1177
+ ... add_permission="plone.tiles.testing.DummyAdd"
1178
+ ... template="test.pt"
1179
+ ... for="*"
1180
+ ... permission="plone.tiles.testing.DummyView"
1181
+ ... />
1182
+ ...
1183
+ ... <!-- Use the PersistentTile class directly with a template-only tile -->
1184
+ ... <plone:tile
1185
+ ... name="dummy4"
1186
+ ... title="Dummy tile 4"
1187
+ ... add_permission="plone.tiles.testing.DummyAdd"
1188
+ ... schema="plone.tiles.testing.IDummySchema"
1189
+ ... class="plone.tiles.PersistentTile"
1190
+ ... template="test.pt"
1191
+ ... for="*"
1192
+ ... permission="plone.tiles.testing.DummyView"
1193
+ ... />
1194
+ ...
1195
+ ... <!-- Override dummy3 for a new layer -->
1196
+ ... <plone:tile
1197
+ ... name="dummy3"
1198
+ ... class="plone.tiles.testing.DummyTile"
1199
+ ... for="*"
1200
+ ... layer="plone.tiles.testing.IDummyLayer"
1201
+ ... permission="plone.tiles.testing.DummyView"
1202
+ ... />
1203
+ ...
1204
+ ... </configure>
1205
+ ... """
1206
+
1207
+ >>> from io import StringIO
1208
+ >>> from zope.configuration import xmlconfig
1209
+ >>> xmlconfig.xmlconfig(StringIO(configuration))
1210
+
1211
+ Let's check how the tiles were registered:
1212
+
1213
+ .. code-block:: python
1214
+
1215
+ >>> from zope.component import getUtility
1216
+ >>> from plone.tiles.interfaces import ITileType
1217
+
1218
+ >>> tile1_type = getUtility(ITileType, name=u'dummy1')
1219
+ >>> tile1_type
1220
+ <TileType dummy1 (Dummy tile 1)>
1221
+ >>> tile1_type.description
1222
+ 'This one shows all available options'
1223
+
1224
+ >>> tile1_type.add_permission
1225
+ 'plone.tiles.testing.DummyAdd'
1226
+
1227
+ >>> tile1_type.view_permission
1228
+ 'plone.tiles.testing.DummyView'
1229
+
1230
+ >>> tile1_type.schema
1231
+ <InterfaceClass plone.tiles.testing.IDummySchema>
1232
+
1233
+ >>> tile2_type = getUtility(ITileType, name=u'dummy2')
1234
+ >>> tile2_type
1235
+ <TileType dummy2 (Dummy tile 2)>
1236
+ >>> tile2_type.description is None
1237
+ True
1238
+ >>> tile2_type.add_permission
1239
+ 'plone.tiles.testing.DummyAdd'
1240
+ >>> tile2_type.schema is None
1241
+ True
1242
+
1243
+ >>> tile3_type = getUtility(ITileType, name=u'dummy3')
1244
+ >>> tile3_type
1245
+ <TileType dummy3 (Dummy tile 3)>
1246
+ >>> tile3_type.description is None
1247
+ True
1248
+ >>> tile3_type.add_permission
1249
+ 'plone.tiles.testing.DummyAdd'
1250
+ >>> tile3_type.schema is None
1251
+ True
1252
+
1253
+ >>> tile4_type = getUtility(ITileType, name=u'dummy4')
1254
+ >>> tile4_type
1255
+ <TileType dummy4 (Dummy tile 4)>
1256
+ >>> tile4_type.description is None
1257
+ True
1258
+ >>> tile4_type.add_permission
1259
+ 'plone.tiles.testing.DummyAdd'
1260
+ >>> tile4_type.schema
1261
+ <InterfaceClass plone.tiles.testing.IDummySchema>
1262
+
1263
+ Finally, let's check that we can look up the tiles:
1264
+
1265
+ .. code-block:: python
1266
+
1267
+ >>> from zope.publisher.browser import TestRequest
1268
+ >>> from zope.interface import implementer, alsoProvides
1269
+
1270
+ >>> from plone.tiles.testing import IDummyContext, IDummyLayer
1271
+
1272
+ >>> @implementer(IDummyContext)
1273
+ ... class Context(object):
1274
+ ... pass
1275
+
1276
+ >>> context = Context()
1277
+ >>> request = TestRequest()
1278
+ >>> layer_request = TestRequest(skin=IDummyLayer)
1279
+
1280
+ >>> from zope.component import getMultiAdapter
1281
+ >>> from plone.tiles import Tile, PersistentTile
1282
+ >>> from plone.tiles.testing import DummyTile, DummyTileWithTemplate
1283
+
1284
+ >>> tile1 = getMultiAdapter((context, layer_request), name='dummy1')
1285
+ >>> isinstance(tile1, DummyTileWithTemplate)
1286
+ True
1287
+ >>> print(tile1().strip())
1288
+ <b>test!</b>
1289
+ >>> tile1.__name__
1290
+ 'dummy1'
1291
+
1292
+ >>> tile2 = getMultiAdapter((context, request), name='dummy2')
1293
+ >>> isinstance(tile2, DummyTile)
1294
+ True
1295
+ >>> print(tile2())
1296
+ dummy
1297
+ >>> tile2.__name__
1298
+ 'dummy2'
1299
+
1300
+ >>> tile3 = getMultiAdapter((context, request), name='dummy3')
1301
+ >>> isinstance(tile3, Tile)
1302
+ True
1303
+ >>> print(tile3().strip())
1304
+ <b>test!</b>
1305
+ >>> tile3.__name__
1306
+ 'dummy3'
1307
+
1308
+ >>> tile4 = getMultiAdapter((context, request), name='dummy4')
1309
+ >>> isinstance(tile4, PersistentTile)
1310
+ True
1311
+ >>> print(tile4().strip())
1312
+ <b>test!</b>
1313
+ >>> tile4.__name__
1314
+ 'dummy4'
1315
+
1316
+ >>> tile3_layer = getMultiAdapter((context, layer_request), name='dummy3')
1317
+ >>> isinstance(tile3_layer, DummyTile)
1318
+ True
1319
+ >>> print(tile3_layer())
1320
+ dummy
1321
+ >>> tile3_layer.__name__
1322
+ 'dummy3'
1323
+
1324
+ ESI support
1325
+ ===========
1326
+
1327
+ Some sites may choose to render tiles in a delayed fashion using Edge Side Includes or some similar mechanism.
1328
+ ``plone.tiles`` includes some support to help render ESI placeholders.
1329
+ This is used in ``plone.app.blocks`` to facilitate ESI rendering.
1330
+ Since ESI normally involves a "dumb" replacement operation,
1331
+ ``plone.tiles`` also provides a means of accessing just the head and/or just the body of a tile.
1332
+
1333
+ To use the package, you should first load its ZCML configuration.
1334
+
1335
+ .. code-block:: python
1336
+
1337
+ >>> configuration = """\
1338
+ ... <configure
1339
+ ... xmlns="http://namespaces.zope.org/zope"
1340
+ ... xmlns:plone="http://namespaces.plone.org/plone"
1341
+ ... i18n_domain="plone.tiles.tests">
1342
+ ...
1343
+ ... <include package="zope.component" file="meta.zcml" />
1344
+ ... <include package="zope.browserpage" file="meta.zcml" />
1345
+ ...
1346
+ ... <include package="plone.tiles" file="meta.zcml" />
1347
+ ... <include package="plone.tiles" />
1348
+ ...
1349
+ ... </configure>
1350
+ ... """
1351
+
1352
+ >>> from io import StringIO
1353
+ >>> from zope.configuration import xmlconfig
1354
+ >>> xmlconfig.xmlconfig(StringIO(configuration))
1355
+
1356
+ Marking a tile as ESI-rendered
1357
+ ------------------------------
1358
+
1359
+ For ESI rendering to be available, the tile must be marked with the ``IESIRendered`` marker interface.
1360
+ We can create a dummy tile with this interface like so:
1361
+
1362
+ .. code-block:: python
1363
+
1364
+ >>> from zope.interface import implementer
1365
+ >>> from plone.tiles.interfaces import IESIRendered
1366
+ >>> from plone.tiles import Tile
1367
+
1368
+ >>> @implementer(IESIRendered)
1369
+ ... class SampleTile(Tile):
1370
+ ...
1371
+ ... __name__ = 'sample.tile' # would normally be set by ZCML handler
1372
+ ...
1373
+ ... def __call__(self):
1374
+ ... return '<html><head><title>Title</title></head><body><b>My tile</b></body></html>'
1375
+
1376
+ Above, we have created a simple HTML string.
1377
+ This would normally be rendered using a page template.
1378
+
1379
+ We'll register this tile manually here.
1380
+ Ordinarily, of course, it would be registered via ZCML.
1381
+
1382
+ .. code-block:: python
1383
+
1384
+ >>> from plone.tiles.type import TileType
1385
+ >>> from zope.security.permission import Permission
1386
+ >>> permission = Permission('dummy.Permission')
1387
+ >>> sampleTileType = TileType(
1388
+ ... name=u'sample.tile',
1389
+ ... title=u'Sample tile',
1390
+ ... description=u'A tile used for testing',
1391
+ ... add_permission='dummy.Permission',
1392
+ ... view_permission='dummy.Permission',
1393
+ ... schema=None)
1394
+
1395
+ >>> from zope.component import provideAdapter, provideUtility
1396
+ >>> from zope.interface import Interface
1397
+ >>> from plone.tiles.interfaces import IBasicTile
1398
+
1399
+ >>> provideUtility(permission, name=u'dummy.Permission')
1400
+ >>> provideUtility(sampleTileType, name=u'sample.tile')
1401
+ >>> provideAdapter(SampleTile, (Interface, Interface), IBasicTile, name=u'sample.tile')
1402
+
1403
+ ESI lookup
1404
+ ----------
1405
+
1406
+ When a page is rendered
1407
+ (for example by a system like ``plone.app.blocks``, but see below),
1408
+ a tile placeholder may be replaced by a link such as:
1409
+
1410
+ .. code-block:: xml
1411
+
1412
+ <esi:include src="/path/to/context/@@sample.tile/tile1/@@esi-body" />
1413
+
1414
+ When this is resolved, it will return the body part of the tile.
1415
+ Equally, a tile in the head can be replaced by:
1416
+
1417
+ .. code-block:: xml
1418
+
1419
+ <esi:include src="/path/to/context/@@sample.tile/tile1/@@esi-head" />
1420
+
1421
+ To illustrate how this works,
1422
+ let's create a sample context,
1423
+ look up the view as it would be during traversal,
1424
+ and instantiate the tile,
1425
+ before looking up the ESI views and rendering them.
1426
+
1427
+ .. code-block:: python
1428
+
1429
+ >>> from zope.interface import implementer
1430
+ >>> from zope.publisher.browser import TestRequest
1431
+
1432
+ >>> class IContext(Interface):
1433
+ ... pass
1434
+
1435
+ >>> @implementer(IContext)
1436
+ ... class Context(object):
1437
+ ... pass
1438
+
1439
+ >>> class IntegratedTestRequest(TestRequest):
1440
+ ... @property
1441
+ ... def environ(self):
1442
+ ... return self._environ
1443
+
1444
+ >>> context = Context()
1445
+ >>> request = IntegratedTestRequest()
1446
+
1447
+ The following simulates traversal to ``context/@@sample.tile/tile1``
1448
+
1449
+ .. code-block:: python
1450
+
1451
+ >>> from zope.interface import Interface
1452
+ >>> from zope.component import getMultiAdapter
1453
+ >>> tile = getMultiAdapter((context, request), name=u'sample.tile')
1454
+ >>> tile = tile['tile1'] # simulates sub-path traversal
1455
+
1456
+ This tile should be ESI rendered:
1457
+
1458
+ .. code-block:: python
1459
+
1460
+ >>> IESIRendered.providedBy(tile)
1461
+ True
1462
+
1463
+ At this point, we can look up the ESI views:
1464
+
1465
+ .. code-block:: python
1466
+
1467
+ >>> head = getMultiAdapter((tile, request), name='esi-head')
1468
+ >>> head()
1469
+ Traceback (most recent call last):
1470
+ ...
1471
+ zExceptions.unauthorized.Unauthorized: Unauthorized()
1472
+
1473
+ But we can only render them when we have the required permissions:
1474
+
1475
+ >>> from AccessControl.SecurityManagement import newSecurityManager
1476
+ >>> from AccessControl.User import Super
1477
+ >>> newSecurityManager(None, Super('manager', '', ['Manager'], []))
1478
+ >>> print(head())
1479
+ <title>Title</title>
1480
+
1481
+ >>> body = getMultiAdapter((tile, request), name='esi-body')
1482
+ >>> print(body())
1483
+ <b>My tile</b>
1484
+
1485
+ Tiles without heads or bodies
1486
+ -----------------------------
1487
+
1488
+ In general, tiles are supposed to return full HTML documents.
1489
+ The ``esi-head`` and ``esi-body`` views are tolerant of tiles that do not.
1490
+ If they cannot find a ``<head />`` or ``<body />`` element, respectively, they will return the underlying tile output unaltered.
1491
+
1492
+ For example:
1493
+
1494
+ .. code-block:: python
1495
+
1496
+ >>> from plone.tiles.esi import ESITile
1497
+ >>> class LazyTile(ESITile):
1498
+ ... __name__ = 'sample.esi1' # would normally be set by ZCML handler
1499
+ ... def __call__(self):
1500
+ ... return '<title>Page title</title>'
1501
+
1502
+ We won't bother to register this for this test, instead just instantiating it directly:
1503
+
1504
+ .. code-block:: python
1505
+
1506
+ >>> tile = LazyTile(context, request)['tile1']
1507
+
1508
+ >>> IESIRendered.providedBy(tile)
1509
+ True
1510
+
1511
+ >>> head = getMultiAdapter((tile, request), name='esi-head')
1512
+ >>> print(head())
1513
+ <title>Page title</title>
1514
+
1515
+ Of course, the ESI body renderer would return the same thing,
1516
+ since it can't extract a specific body either:
1517
+
1518
+ .. code-block:: python
1519
+
1520
+ >>> body = getMultiAdapter((tile, request), name='esi-body')
1521
+ >>> print(body())
1522
+ <title>Page title</title>
1523
+
1524
+ In this case, we would likely end up with invalid HTML,
1525
+ since the ``<title />`` tag is not allowed in the body.
1526
+ Whether and how to resolve this is left up to the ESI interpolation implementation.
1527
+
1528
+ Convenience classes and placeholder rendering
1529
+ ---------------------------------------------
1530
+
1531
+ Two convenience base classes can be found in the ``plone.tiles.esi`` module.
1532
+ These extend the standard ``Tile`` and ``PersistentTile`` classes to provide the ``IESIRendered`` interface.
1533
+
1534
+ * ``plone.tiles.esi.ESITile``, a transient, ESI-rendered tile
1535
+ * ``plone.tiles.esi.ESIPersistentTile``, a persistent, ESI-rendered tile
1536
+
1537
+ These are particularly useful if you are creating a template-only tile and want ESI rendering.
1538
+ For example:
1539
+
1540
+ .. code-block:: xml
1541
+
1542
+ <plone:tile
1543
+ name="sample.esitile"
1544
+ title="An ESI-rendered tile"
1545
+ add_permission="plone.tiles.tests.DummyAdd"
1546
+ template="esitile.pt"
1547
+ class="plone.tiles.esi.ESITile"
1548
+ for="*"
1549
+ permission="zope.View"
1550
+ />
1551
+
1552
+ Additionally,
1553
+ these base classes implement a ``__call__()`` method that will render a tile placeholder,
1554
+ if the request contains an ``X-ESI-Enabled`` header set to the literal 'true'.
1555
+
1556
+ The placeholder is a simple HTML ``<a />`` tag,
1557
+ which can be transformed into an ``<esi:include />`` tag using the helper function ``substituteESILinks()``.
1558
+ The reason for this indirection is that the ``esi`` namespace is not allowed in HTML documents,
1559
+ and are liable to be stripped out by transforms using the ``libxml2`` / ``lxml`` HTML parser.
1560
+
1561
+ Let us now create a simple ESI tile. To benefit from the default rendering,
1562
+ we should implement the ``render()`` method instead of ``__call__()``. Setting
1563
+ a page template as the ``index`` class variable or using the ``template``
1564
+ attribute to the ZCML directive will work also.
1565
+
1566
+ .. code-block:: python
1567
+
1568
+ >>> from plone.tiles.esi import ESITile
1569
+
1570
+ >>> class SampleESITile(ESITile):
1571
+ ... __name__ = 'sample.esitile' # would normally be set by ZCML handler
1572
+ ...
1573
+ ... def render(self):
1574
+ ... return '<html><head><title>Title</title></head><body><b>My ESI tile</b></body></html>'
1575
+
1576
+ >>> sampleESITileType = TileType(
1577
+ ... name=u'sample.esitile',
1578
+ ... title=u'Sample ESI tile',
1579
+ ... description=u'A tile used for testing ESI',
1580
+ ... add_permission='dummy.Permission',
1581
+ ... view_permission='dummy.Permission',
1582
+ ... schema=None)
1583
+
1584
+ >>> provideUtility(sampleESITileType, name=u'sample.esitile')
1585
+ >>> provideAdapter(SampleESITile, (Interface, Interface), IBasicTile, name=u'sample.esitile')
1586
+
1587
+ The following simulates traversal to ``context/@@sample.esitile/tile1``
1588
+
1589
+ .. code-block:: python
1590
+
1591
+ >>> tile = getMultiAdapter((context, request), name=u'sample.esitile')
1592
+ >>> tile = tile['tile1'] # simulates sub-path traversal
1593
+
1594
+ By default, the tile renders as normal:
1595
+
1596
+ .. code-block:: python
1597
+
1598
+ >>> print(tile())
1599
+ <html><head><title>Title</title></head><body><b>My ESI tile</b></body></html>
1600
+
1601
+ However, if we opt into ESI rendering via a request header, we get a different view:
1602
+
1603
+ .. code-block:: python
1604
+
1605
+ >>> from plone.tiles.interfaces import ESI_HEADER_KEY
1606
+ >>> request.environ[ESI_HEADER_KEY] = 'true'
1607
+ >>> print(tile()) # doctest: +NORMALIZE_WHITESPACE
1608
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1609
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1610
+ <html xmlns="http://www.w3.org/1999/xhtml">
1611
+ <body>
1612
+ <a class="_esi_placeholder"
1613
+ rel="esi"
1614
+ href="http://127.0.0.1/@@esi-body?"></a>
1615
+ </body>
1616
+ </html>
1617
+
1618
+ This can be transformed into a proper ESI tag with ``substituteESILinks()``:
1619
+
1620
+ .. code-block:: python
1621
+
1622
+ >>> from plone.tiles.esi import substituteESILinks
1623
+ >>> print(substituteESILinks(tile())) # doctest: +NORMALIZE_WHITESPACE
1624
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1625
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1626
+ <html xmlns:esi="http://www.edge-delivery.org/esi/1.0" xmlns="http://www.w3.org/1999/xhtml">
1627
+ <body>
1628
+ <esi:include src="http://127.0.0.1/@@esi-body?" />
1629
+ </body>
1630
+ </html>
1631
+
1632
+ It is also possible to render the ESI tile for the head.
1633
+ This is done with a class variable 'head'
1634
+ (which would of course normally be set within the class):
1635
+
1636
+ .. code-block:: python
1637
+
1638
+ >>> SampleESITile.head = True
1639
+ >>> print(tile()) # doctest: +NORMALIZE_WHITESPACE
1640
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1641
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1642
+ <html xmlns="http://www.w3.org/1999/xhtml">
1643
+ <body>
1644
+ <a class="_esi_placeholder"
1645
+ rel="esi"
1646
+ href="http://127.0.0.1/@@esi-head?"></a>
1647
+ </body>
1648
+ </html>
1649
+
1650
+ Changelog
1651
+ =========
1652
+
1653
+ .. You should *NOT* be adding new change log entries to this file.
1654
+ You should create a file in the news directory instead.
1655
+ For helpful instructions, please see:
1656
+ https://github.com/plone/plone.releaser/blob/master/ADD-A-NEWS-ITEM.rst
1657
+
1658
+ .. towncrier release notes start
1659
+
1660
+ 3.0.0 (2025-06-05)
1661
+ ------------------
1662
+
1663
+ Breaking changes:
1664
+
1665
+
1666
+ - Remove Python 2 and Plone 5.2 support. @petschki
1667
+
1668
+
1669
+ Internal:
1670
+
1671
+
1672
+ - Update configuration files.
1673
+ [plone devs]
1674
+
1675
+
1676
+ 2.3.1 (2021-10-07)
1677
+ ------------------
1678
+
1679
+ Bug fixes:
1680
+
1681
+
1682
+ - Fix incorrectly set condition for plone.protect.
1683
+ [thet] (#33)
1684
+
1685
+
1686
+ 2.3.0 (2020-09-07)
1687
+ ------------------
1688
+
1689
+ New features:
1690
+
1691
+
1692
+ - Drop Plone 4.3 support.
1693
+ [maurits] (#3130)
1694
+
1695
+
1696
+ Bug fixes:
1697
+
1698
+
1699
+ - Fixed deprecation warning for ComponentLookupError.
1700
+ [maurits] (#3130)
1701
+
1702
+
1703
+ 2.2.2 (2020-04-22)
1704
+ ------------------
1705
+
1706
+ Bug fixes:
1707
+
1708
+
1709
+ - Minor packaging updates. (#1)
1710
+ - Fixed tests on Travis.
1711
+ [maurits] (#33)
1712
+
1713
+
1714
+ 2.2.1 (2019-05-01)
1715
+ ------------------
1716
+
1717
+ Bug fixes:
1718
+
1719
+
1720
+ - 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)
1721
+
1722
+
1723
+ 2.2.0 (2018-11-21)
1724
+ ------------------
1725
+
1726
+ New features:
1727
+
1728
+
1729
+ - Add support for Python 3. [pbauer] (#29)
1730
+
1731
+
1732
+ Bug fixes:
1733
+
1734
+
1735
+ - Initialize towncrier. [gforcada] (#2548)
1736
+
1737
+
1738
+ 2.1 (2018-07-05)
1739
+ ----------------
1740
+
1741
+ - Dependency on zope.app.publisher removed,
1742
+ needed parts were moved to zope.browserpage long time ago.
1743
+ This probably breaks Plone 4.2 support (removed).
1744
+ [jensens]
1745
+
1746
+
1747
+ 2.0.0 (2018-04-13)
1748
+ ------------------
1749
+
1750
+ - Fix license classifier.
1751
+
1752
+
1753
+ 2.0.0b3 (2017-08-22)
1754
+ --------------------
1755
+
1756
+ Breaking changes:
1757
+
1758
+ - Fix querystring en-/decoder to always skip primary fields
1759
+ [datakurre]
1760
+
1761
+
1762
+ 2.0.0b2 (2017-03-29)
1763
+ --------------------
1764
+
1765
+ Bug fixes:
1766
+
1767
+ - Do not swallow ``AttributeError`` inside ``index()`` on template rendering.
1768
+ [hvelarde]
1769
+
1770
+ - Fix code analysis errors.
1771
+ [gforcada]
1772
+
1773
+
1774
+ 2.0.0b1 (2017-02-24)
1775
+ --------------------
1776
+
1777
+ Breaking changes:
1778
+
1779
+ - Tiles no longer add relative ``X-Tile-Url``-header in ``__call__``.
1780
+ Tiles still add absolute ``X-Tile-Url``-header during traversal, but
1781
+ it gets removed after rendering when request is not CSRF-authorized.
1782
+ [datakurre]
1783
+
1784
+ - Generic ESI helper check now that the request is authorized to render
1785
+ the tile according to the registered view permission to the tile.
1786
+ [datakurre]
1787
+
1788
+ - Transactions of requests to ESI helper views are automatically aborted,
1789
+ because ESI requests should always be immutable GET requests
1790
+ [datakurre]
1791
+
1792
+ - plone.app.theming (transform) is now disabled with X-Theme-Disabled-header
1793
+ for requests rendering tiles
1794
+ [datakurre]
1795
+
1796
+ - plone.protect's ProtectTransform is skipped for tile requests with correct
1797
+ CSRF token prevent its side-effects on tile editors rendering tiles
1798
+ individually
1799
+ [datakurre]
1800
+
1801
+ New features:
1802
+
1803
+ - Added X-Frame-Options -header for ESI-tile views with matching behavior
1804
+ with plone.protect
1805
+ [datakurre]
1806
+
1807
+ Bug fixes:
1808
+
1809
+ - Fix issue where ESI-tile helper views didn't get correct
1810
+ Cache-Control-headers, because ESI-helpers views were not acquisition
1811
+ wrapped
1812
+ [datakurre]
1813
+
1814
+
1815
+ 1.8.2 (2017-01-10)
1816
+ ------------------
1817
+
1818
+ Bug fixes:
1819
+
1820
+ - Fix issue where transient tile was unable to encode data
1821
+ with list of unicode strings
1822
+ [datakurre]
1823
+
1824
+ - Remove unused unittest2 requirement
1825
+ [tomgross]
1826
+
1827
+
1828
+ 1.8.1 (2016-11-24)
1829
+ ------------------
1830
+
1831
+ Bugfix:
1832
+
1833
+ - Fix encode error in nested unicodes (like in plone.app.querystring)
1834
+ [tomgross]
1835
+
1836
+ - Restructure testing
1837
+ [tomgross]
1838
+
1839
+
1840
+ 1.8.0 (2016-09-13)
1841
+ ------------------
1842
+
1843
+ New features:
1844
+
1845
+ - Provide ignore_querystring form directive to mark particular tiles fields
1846
+ that are not allowed to default data from query string data
1847
+ [vangheem]
1848
+
1849
+
1850
+ 1.7.1 (2016-09-12)
1851
+ ------------------
1852
+
1853
+ - Fix issue where collective.cover was broken, because internal changes in
1854
+ default data managers
1855
+ [datakurre]
1856
+
1857
+ 1.7.0 (2016-09-08)
1858
+ ------------------
1859
+
1860
+ New features:
1861
+
1862
+ - Option to customize storage layer with ITileDataStorage adapter
1863
+ [datakurre]
1864
+
1865
+
1866
+ 1.6.1 (2016-09-07)
1867
+ ------------------
1868
+
1869
+ Bug fixes:
1870
+
1871
+ - Reformat docs.
1872
+ [gforcada]
1873
+
1874
+ - Add coveralls shield.
1875
+ [gforcada]
1876
+
1877
+
1878
+ 1.6 (2016-06-27)
1879
+ ----------------
1880
+
1881
+ - Let TileType instances (tile registration utility) know about the view
1882
+ permission too.
1883
+ [jensens]
1884
+
1885
+
1886
+ 1.5.2 (2016-03-28)
1887
+ ------------------
1888
+
1889
+ - Fix issue where ESI href was not properly replaced.
1890
+ [jensens]
1891
+
1892
+ - Add section "ZCML Reference" to README.rst.
1893
+ [jensens]
1894
+
1895
+ - PEP8, code-analysis, documentation and packaging fixes.
1896
+ [jensens, mauritsvanrees]
1897
+
1898
+
1899
+ 1.5.1 (2015-10-09)
1900
+ ------------------
1901
+
1902
+ - Fix decoding List type of Choice value types
1903
+ [vangheem]
1904
+
1905
+
1906
+ 1.5.0 (2015-09-04)
1907
+ ------------------
1908
+
1909
+ - Add support for overriding transient data manager with persistent data
1910
+ manager by adding X-Tile-Persistent=1 into tile URL
1911
+ [datakurre]
1912
+
1913
+ - Fix persistent data manager to read its default from query string
1914
+ [vangheem]
1915
+
1916
+ 1.4.0 (2015-05-25)
1917
+ ------------------
1918
+
1919
+ - Add support for encoding dictionary fields into tile URL
1920
+ [datakurre]
1921
+ - Fix issue where saving or deleting transient tile data mutated the current request
1922
+ [datakurre]
1923
+ - Fix issue where non-ascii characters in tile data raised UnicodeEncode/DecodeErrors
1924
+ [datakurre]
1925
+
1926
+ 1.3.0 (2015-04-21)
1927
+ ------------------
1928
+
1929
+ - Fix edit_permission and delete_permission to default
1930
+ to add_permission only in TileType constructor
1931
+ [datakurre]
1932
+
1933
+ - Fix argument order in TileType constructor call
1934
+ [datakurre]
1935
+
1936
+ - Fix absolute_url-adapter to fallback to relative URL
1937
+ [datakurre]
1938
+
1939
+ - Add response to include absolute X-Tile-Url header
1940
+ [bloodbare]
1941
+
1942
+ 1.2 (2012-11-07)
1943
+ ----------------
1944
+
1945
+ - Adding icon property for tiletype
1946
+ [garbas]
1947
+
1948
+ - Url that we pass via X-Tile-Url should be relative to current context
1949
+ [garbas]
1950
+
1951
+ - Adding support for more robust permissions for edit and delete on tiles
1952
+ [cewing calvinhp]
1953
+
1954
+ 1.1 (2012-06-22)
1955
+ ----------------
1956
+
1957
+ - X-Tile-Uid header is passed on tile view containing tile's id.
1958
+ [garbas]
1959
+
1960
+ - PEP 8/Pyflakes (ignoring E121, E123, E126, E127 and E501).
1961
+ [hvelarde]
1962
+
1963
+ 1.0 (2012-05-14)
1964
+ ----------------
1965
+
1966
+ - Refactor ESI support. To use the ``ESITile`` and ``ESIPersistentTile``
1967
+ base classes, you should either use a template assigned via ZCML or
1968
+ override the ``render()`` method. See ``esi.rst`` for full details.
1969
+ [optilude]
1970
+
1971
+ - Internationalized title and description of the tile directive.
1972
+ [vincentfretin]
1973
+
1974
+ - Use a json-encoded parameter in transient tiles as first option.
1975
+ [dukebody]
1976
+
1977
+ - Use adapters for the Zope Publisher type casting
1978
+ [dukebody]
1979
+
1980
+ - Conditionally support z3c.relationfield's RelationChoice fields
1981
+ [dukebody]
1982
+
1983
+ - Ignore type casting for fields without fixed type, like zope.schema.Choice
1984
+ [dukebody]
1985
+
1986
+ 1.0a1 (2010-05-17)
1987
+ ------------------
1988
+
1989
+ - Initial release.
1990
+