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/__init__.py +7 -0
- plone/tiles/absoluteurl.py +101 -0
- plone/tiles/configure.zcml +184 -0
- plone/tiles/data.py +422 -0
- plone/tiles/data.rst +179 -0
- plone/tiles/directives.py +13 -0
- plone/tiles/directives.rst +221 -0
- plone/tiles/esi.py +214 -0
- plone/tiles/esi.rst +325 -0
- plone/tiles/fieldtypeconverters.py +39 -0
- plone/tiles/interfaces.py +172 -0
- plone/tiles/meta.py +162 -0
- plone/tiles/meta.zcml +16 -0
- plone/tiles/test.pt +1 -0
- plone/tiles/testing.py +59 -0
- plone/tiles/tests/__init__.py +0 -0
- plone/tiles/tests/test_data.py +83 -0
- plone/tiles/tests/test_doctests.py +30 -0
- plone/tiles/tile.py +186 -0
- plone/tiles/tiles.rst +783 -0
- plone/tiles/type.py +39 -0
- plone.tiles-3.0.0-py3.13-nspkg.pth +1 -0
- plone_tiles-3.0.0.dist-info/LICENSE.GPL +339 -0
- plone_tiles-3.0.0.dist-info/LICENSE.txt +16 -0
- plone_tiles-3.0.0.dist-info/METADATA +1990 -0
- plone_tiles-3.0.0.dist-info/RECORD +29 -0
- plone_tiles-3.0.0.dist-info/WHEEL +5 -0
- plone_tiles-3.0.0.dist-info/namespace_packages.txt +1 -0
- plone_tiles-3.0.0.dist-info/top_level.txt +1 -0
plone/tiles/tiles.rst
ADDED
@@ -0,0 +1,783 @@
|
|
1
|
+
Tiles in detail
|
2
|
+
===============
|
3
|
+
|
4
|
+
Tiles are a form of view component used to compose pages.
|
5
|
+
Think of a tile as a view describing one part of a page,
|
6
|
+
that can be configured with some data described by a schema and inserted into a layout via a dedicated GUI.
|
7
|
+
|
8
|
+
Like a browser view, a tile can be traversed to and published on its own.
|
9
|
+
The tile should then return a full HTML page,
|
10
|
+
including a <head /> with any required resources,
|
11
|
+
and a <body /> with the visible part of the tile.
|
12
|
+
This will then be merged into the page, using a system such as ``plone.app.blocks``.
|
13
|
+
|
14
|
+
The API in this package provides support for tiles being configured according to a schema,
|
15
|
+
with data either passed on the query string (transient tiles) or retrieved from annotations (persistent tiles).
|
16
|
+
|
17
|
+
Note that there is no direct UI support in this package,
|
18
|
+
so the forms that allow users to construct and edit tiles must live elsewhere.
|
19
|
+
You may be interested in ``plone.app.tiles`` and ``plone.app.mosaic`` for that purpose.
|
20
|
+
|
21
|
+
To use the package, you should first load its ZCML configuration:
|
22
|
+
|
23
|
+
.. code-block:: python
|
24
|
+
|
25
|
+
>>> configuration = """\
|
26
|
+
... <configure
|
27
|
+
... xmlns="http://namespaces.zope.org/zope"
|
28
|
+
... xmlns:plone="http://namespaces.plone.org/plone"
|
29
|
+
... i18n_domain="plone.tiles.tests">
|
30
|
+
...
|
31
|
+
... <include package="zope.component" file="meta.zcml" />
|
32
|
+
... <include package="zope.browserpage" file="meta.zcml" />
|
33
|
+
...
|
34
|
+
... <include package="plone.tiles" file="meta.zcml" />
|
35
|
+
... <include package="plone.tiles" />
|
36
|
+
...
|
37
|
+
... </configure>
|
38
|
+
... """
|
39
|
+
|
40
|
+
>>> from io import StringIO
|
41
|
+
>>> from zope.configuration import xmlconfig
|
42
|
+
>>> xmlconfig.xmlconfig(StringIO(configuration))
|
43
|
+
|
44
|
+
A simple transient tile
|
45
|
+
-----------------------
|
46
|
+
|
47
|
+
A basic tile is a view that implements the ``ITile`` interface.
|
48
|
+
The easiest way to do this is to subclass the ``Tile`` class:
|
49
|
+
|
50
|
+
.. code-block:: python
|
51
|
+
|
52
|
+
>>> from plone.tiles import Tile
|
53
|
+
>>> class SampleTile(Tile):
|
54
|
+
...
|
55
|
+
... __name__ = 'sample.tile' # would normally be set by a ZCML handler
|
56
|
+
...
|
57
|
+
... def __call__(self):
|
58
|
+
... return '<html><body><b>My tile</b></body></html>'
|
59
|
+
|
60
|
+
The tile is a browser view:
|
61
|
+
|
62
|
+
.. code-block:: python
|
63
|
+
|
64
|
+
>>> from plone.tiles.interfaces import ITile
|
65
|
+
>>> ITile.implementedBy(SampleTile)
|
66
|
+
True
|
67
|
+
|
68
|
+
>>> from zope.publisher.interfaces.browser import IBrowserView
|
69
|
+
>>> IBrowserView.implementedBy(SampleTile)
|
70
|
+
True
|
71
|
+
|
72
|
+
The tile instance has a ``__name__`` attribute
|
73
|
+
(normally set at class level by the ``<plone:tile />`` ZCML directive),
|
74
|
+
as well as a property ``id``.
|
75
|
+
The id may be set explicitly, either in code, or by sub-path traversal.
|
76
|
+
For example, if the tile name is ``example.tile``,
|
77
|
+
the id may be set to ``tile1`` using an URL like ``http://example.com/foo/@@example.tile/tile1``.
|
78
|
+
|
79
|
+
This tile is registered as a normal browser view,
|
80
|
+
alongside a utility that provides some information about the tile itself.
|
81
|
+
Normally, this is done using the ``<plone:tile />`` directive.
|
82
|
+
Here's how to create one manually:
|
83
|
+
|
84
|
+
.. code-block:: python
|
85
|
+
|
86
|
+
>>> from plone.tiles.type import TileType
|
87
|
+
>>> sampleTileType = TileType(
|
88
|
+
... u'sample.tile',
|
89
|
+
... u'Sample tile',
|
90
|
+
... 'dummy.Permission',
|
91
|
+
... 'dummy.Permission',
|
92
|
+
... description=u'A tile used for testing',
|
93
|
+
... schema=None)
|
94
|
+
|
95
|
+
The name should match the view name and the name the utility is registered under.
|
96
|
+
The title and description may be used by the UI.
|
97
|
+
The add permission is the name of a permission that will be required to insert the tile.
|
98
|
+
The schema attribute may be used to indicate schema interface describing the tile's configurable data - more on this below.
|
99
|
+
|
100
|
+
To register a tile in ZCML, we could do:
|
101
|
+
|
102
|
+
.. code-block:: xml
|
103
|
+
|
104
|
+
<plone:tile
|
105
|
+
name="sample.tile"
|
106
|
+
title="Sample tile"
|
107
|
+
description="A tile used for testing"
|
108
|
+
add_permission="dummy.Permission"
|
109
|
+
class=".mytiles.SampleTile"
|
110
|
+
for="*"
|
111
|
+
permission="zope.Public"
|
112
|
+
/>
|
113
|
+
|
114
|
+
.. note:: The tile name should be a dotted name, prefixed by a namespace you control.
|
115
|
+
It's a good idea to use a package name for this purpose.
|
116
|
+
|
117
|
+
It is also possible to specify a ``layer`` or ``template`` like the ``browser:page`` directive, as well as a ``schema``,
|
118
|
+
which we will describe below.
|
119
|
+
|
120
|
+
We'll register the sample tile directly here, for later testing.
|
121
|
+
|
122
|
+
.. code-block:: python
|
123
|
+
|
124
|
+
>>> from zope.component import provideAdapter, provideUtility
|
125
|
+
>>> from zope.interface import Interface
|
126
|
+
>>> from plone.tiles.interfaces import IBasicTile
|
127
|
+
|
128
|
+
>>> provideUtility(sampleTileType, name=u'sample.tile')
|
129
|
+
>>> provideAdapter(SampleTile, (Interface, Interface), IBasicTile, name=u'sample.tile')
|
130
|
+
|
131
|
+
Tile traversal
|
132
|
+
--------------
|
133
|
+
|
134
|
+
Tiles are publishable as a normal browser view.
|
135
|
+
They will normally be called with a sub-path that specifies a tile id.
|
136
|
+
This allows tiles to be made aware of their instance name.
|
137
|
+
The id is unique within the page layout where the tile is used,
|
138
|
+
and may be the basis for looking up tile data.
|
139
|
+
|
140
|
+
For example, a tile may be saved in a layout as a link like:
|
141
|
+
|
142
|
+
.. code-block:: html
|
143
|
+
|
144
|
+
<link rel="tile" target="mytile" href="./@@sample.tile/tile1" />
|
145
|
+
|
146
|
+
(The idea here is that the tile link tells the rendering algorithm to replace
|
147
|
+
the element with id ``mytile`` with the body of the rendered tile - see
|
148
|
+
``plone.app.blocks`` for details).
|
149
|
+
|
150
|
+
Let's create a sample context,
|
151
|
+
look up the view as it would be during traversal,
|
152
|
+
and verify how the tile is instantiated.
|
153
|
+
|
154
|
+
.. code-block:: python
|
155
|
+
|
156
|
+
>>> from zope.component import getMultiAdapter
|
157
|
+
>>> from zope.interface import classImplements
|
158
|
+
>>> from zope.interface import Interface
|
159
|
+
>>> from zope.interface import implementer
|
160
|
+
>>> from zope.publisher.browser import TestRequest
|
161
|
+
>>> from zope.annotation.interfaces import IAnnotations
|
162
|
+
>>> from zope.annotation.interfaces import IAttributeAnnotatable
|
163
|
+
>>> classImplements(TestRequest, IAttributeAnnotatable)
|
164
|
+
|
165
|
+
>>> class IContext(Interface):
|
166
|
+
... pass
|
167
|
+
|
168
|
+
>>> @implementer(IContext)
|
169
|
+
... class Context(object):
|
170
|
+
... pass
|
171
|
+
|
172
|
+
>>> context = Context()
|
173
|
+
>>> request = TestRequest()
|
174
|
+
|
175
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
176
|
+
>>> tile = tile['tile1'] # simulates sub-path traversal
|
177
|
+
|
178
|
+
The tile will now be aware of its name and id:
|
179
|
+
|
180
|
+
.. code-block:: python
|
181
|
+
|
182
|
+
>>> isinstance(tile, SampleTile)
|
183
|
+
True
|
184
|
+
>>> tile.__parent__ is context
|
185
|
+
True
|
186
|
+
>>> tile.id
|
187
|
+
'tile1'
|
188
|
+
>>> tile.__name__
|
189
|
+
'sample.tile'
|
190
|
+
|
191
|
+
The sub-path traversal is implemented using a custom ``__getitem__()`` method.
|
192
|
+
To look up a view on a tile,
|
193
|
+
you can traverse to it *after* you've traversed to the id sub-path:
|
194
|
+
|
195
|
+
.. code-block:: python
|
196
|
+
|
197
|
+
>>> from zope.component import adapts
|
198
|
+
>>> from zope.interface import Interface
|
199
|
+
>>> from zope.publisher.browser import BrowserView
|
200
|
+
>>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
|
201
|
+
|
202
|
+
>>> class TestView(BrowserView):
|
203
|
+
... adapts(SampleTile, IDefaultBrowserLayer)
|
204
|
+
... def __call__(self):
|
205
|
+
... return 'Dummy view'
|
206
|
+
|
207
|
+
>>> provideAdapter(TestView, provides=Interface, name='test-view')
|
208
|
+
|
209
|
+
>>> tile.id is not None
|
210
|
+
True
|
211
|
+
>>> tile['test-view']()
|
212
|
+
'Dummy view'
|
213
|
+
|
214
|
+
If there is no view and we have an id already, we will get a ``KeyError``:
|
215
|
+
|
216
|
+
.. code-block:: python
|
217
|
+
|
218
|
+
>>> tile['not-known'] # doctest: +ELLIPSIS
|
219
|
+
Traceback (most recent call last):
|
220
|
+
...
|
221
|
+
KeyError: 'not-known'
|
222
|
+
|
223
|
+
To ensure consistency with Zope's various tangles publication machines,
|
224
|
+
it is also possible to traverse using the ``publishTraverse`` method:
|
225
|
+
|
226
|
+
.. code-block:: python
|
227
|
+
|
228
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
229
|
+
>>> tile = tile.publishTraverse(request, 'tile1') # simulates sub-path traversal
|
230
|
+
|
231
|
+
>>> isinstance(tile, SampleTile)
|
232
|
+
True
|
233
|
+
>>> tile.__parent__ is context
|
234
|
+
True
|
235
|
+
>>> tile.id
|
236
|
+
'tile1'
|
237
|
+
>>> tile.__name__
|
238
|
+
'sample.tile'
|
239
|
+
|
240
|
+
Transient tile data
|
241
|
+
-------------------
|
242
|
+
|
243
|
+
Let us now consider how tiles may have data.
|
244
|
+
In the simplest case, tile data is passed on the query string, and described according to a schema.
|
245
|
+
A simple schema may look like:
|
246
|
+
|
247
|
+
.. code-block:: python
|
248
|
+
|
249
|
+
>>> import zope.schema
|
250
|
+
|
251
|
+
>>> class ISampleTileData(Interface):
|
252
|
+
... title = zope.schema.TextLine(title=u'Tile title')
|
253
|
+
... cssClass = zope.schema.ASCIILine(title=u'CSS class to apply')
|
254
|
+
... count = zope.schema.Int(title=u'Number of things to show in the tile')
|
255
|
+
|
256
|
+
We would normally have listed this interface when registering this tile in ZCML.
|
257
|
+
We can simply update the utility here.
|
258
|
+
|
259
|
+
.. code-block:: python
|
260
|
+
|
261
|
+
>>> sampleTileType.schema = ISampleTileData
|
262
|
+
|
263
|
+
Tile data is represented by a simple dictionary. For example:
|
264
|
+
|
265
|
+
.. code-block:: python
|
266
|
+
|
267
|
+
>>> data = {'title': u'My title', 'count': 5, 'cssClass': 'foo'}
|
268
|
+
|
269
|
+
The idea is that a tile add form is built from the schema interface, and its data saved to a dictionary.
|
270
|
+
|
271
|
+
For transient tiles,
|
272
|
+
this data is then encoded into the tile query string.
|
273
|
+
To help with this,
|
274
|
+
a utility function can be used to encode a dict to a query string,
|
275
|
+
applying Zope form marshalers according to the types described in the schema:
|
276
|
+
|
277
|
+
.. code-block:: python
|
278
|
+
|
279
|
+
>>> from plone.tiles.data import encode
|
280
|
+
>>> encode(data, ISampleTileData)
|
281
|
+
'title=My+title&cssClass=foo&count%3Along=5'
|
282
|
+
|
283
|
+
The ``count%3Along=5`` bit is the encoded version of ``count:long=5``.
|
284
|
+
|
285
|
+
Note that not all field types may be saved.
|
286
|
+
In particular, object, interface, set or frozen set fields may not be saved, and will result in a ``KeyError``.
|
287
|
+
Lengthy text fields or bytes fields with binary data may also be a problem.
|
288
|
+
For these types of fields,
|
289
|
+
look to use persistent tiles instead.
|
290
|
+
|
291
|
+
Furthermore, the conversion may not be perfect.
|
292
|
+
For example, Zope's form marshalers cannot distinguish between unicode and ascii fields.
|
293
|
+
Therefore, there is a corresponding ``decode()`` method that may be used to ensure that the values match the schema:
|
294
|
+
|
295
|
+
.. code-block:: python
|
296
|
+
|
297
|
+
>>> marshaled = {'title': u'My tile', 'count': 5, 'cssClass': u'foo'}
|
298
|
+
|
299
|
+
>>> from plone.tiles.data import decode
|
300
|
+
>>> sorted(decode(marshaled, ISampleTileData).items())
|
301
|
+
[('count', 5), ('cssClass', 'foo'), ('title', 'My tile')]
|
302
|
+
|
303
|
+
When saved into a layout, the tile link would now look like:
|
304
|
+
|
305
|
+
.. code-block:: html
|
306
|
+
|
307
|
+
<link rel="tile" target="mytile"
|
308
|
+
href="./@@sample.tile/tile1?title=My+title&count%3Along=5&cssClass=foo" />
|
309
|
+
|
310
|
+
Let's simulate traversal once more and see how the data is now available to the tile instance:
|
311
|
+
|
312
|
+
.. code-block:: python
|
313
|
+
|
314
|
+
>>> context = Context()
|
315
|
+
>>> request = TestRequest(form={'title': u'My title', 'count': 5, 'cssClass': u'foo'})
|
316
|
+
|
317
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
318
|
+
>>> tile = tile['tile1']
|
319
|
+
|
320
|
+
>>> sorted(tile.data.items())
|
321
|
+
[('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
|
322
|
+
|
323
|
+
Notice also how the data has been properly decoded according to the schema.
|
324
|
+
|
325
|
+
Transient tiles will get their data directly from the request parameters but,
|
326
|
+
if a `_tiledata` JSON-encoded parameter is present in the request,
|
327
|
+
this one will be used instead:
|
328
|
+
|
329
|
+
.. code-block:: python
|
330
|
+
|
331
|
+
>>> import json
|
332
|
+
|
333
|
+
>>> request = TestRequest(form={
|
334
|
+
... 'title': u'My title', 'count': 5, 'cssClass': u'foo',
|
335
|
+
... '_tiledata': json.dumps({'title': u'Your title', 'count': 6, 'cssClass': u'bar'})
|
336
|
+
... })
|
337
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
338
|
+
>>> tile = tile['tile1']
|
339
|
+
|
340
|
+
>>> sorted(tile.data.items())
|
341
|
+
[('count', 6), ('cssClass', 'bar'), ('title', 'Your title')]
|
342
|
+
|
343
|
+
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.
|
344
|
+
|
345
|
+
The tile data manager
|
346
|
+
---------------------
|
347
|
+
|
348
|
+
The ``data`` attribute is a convenience attribute to get hold of a (cached) copy of the data returned by an ``ITileDataManager``.
|
349
|
+
This interface provides three methods:
|
350
|
+
``get()``, to return the tile's data,
|
351
|
+
``set()``, to update it with a new dictionary of data,
|
352
|
+
and ``delete()``, to delete the data.
|
353
|
+
|
354
|
+
This adapter is mostly useful for writing UI around tiles.
|
355
|
+
Using our tile above, we can get the data like so:
|
356
|
+
|
357
|
+
.. code-block:: python
|
358
|
+
|
359
|
+
>>> from plone.tiles.interfaces import ITileDataManager
|
360
|
+
>>> dataManager = ITileDataManager(tile)
|
361
|
+
>>> dataManager.get() == tile.data
|
362
|
+
True
|
363
|
+
|
364
|
+
We can also update the tile data:
|
365
|
+
|
366
|
+
.. code-block:: python
|
367
|
+
|
368
|
+
>>> dataManager.set({'count': 1, 'cssClass': 'bar', 'title': u'Another title'})
|
369
|
+
>>> sorted(dataManager.get().items())
|
370
|
+
[('count', 1), ('cssClass', 'bar'), ('title', 'Another title')]
|
371
|
+
|
372
|
+
The data can also be deleted:
|
373
|
+
|
374
|
+
.. code-block:: python
|
375
|
+
|
376
|
+
>>> dataManager.delete()
|
377
|
+
>>> sorted(dataManager.get().items())
|
378
|
+
[('count', None), ('cssClass', None), ('title', None)]
|
379
|
+
|
380
|
+
Note that in the case of a transient tile,
|
381
|
+
all we are doing is modifying the ``form`` dictionary of the request
|
382
|
+
(or the `_tiledata` parameter of this dictionary, if present).
|
383
|
+
The data needs to be encoded into the query string,
|
384
|
+
either using the ``encode()`` method or via the tile's ``IAbsoluteURL`` adapter (see below for details).
|
385
|
+
|
386
|
+
For persistent tiles, the data manager is a bit more interesting.
|
387
|
+
|
388
|
+
Persistent tiles
|
389
|
+
----------------
|
390
|
+
|
391
|
+
Not all types of data can be placed in a query string.
|
392
|
+
For more substantial storage requirements,
|
393
|
+
you can use persistent tiles, which store data in annotations.
|
394
|
+
|
395
|
+
.. note:: If you have more intricate requirements,
|
396
|
+
you can also write your own ``ITileDataManager`` to handle data retrieval.
|
397
|
+
In this case, you probably still want to derive from ``PersistentTile``,
|
398
|
+
to get the appropriate ``IAbsoluteURL`` adapter, among other things.
|
399
|
+
|
400
|
+
First, we need to write up annotations support.
|
401
|
+
|
402
|
+
.. code-block:: python
|
403
|
+
|
404
|
+
>>> from zope.annotation.attribute import AttributeAnnotations
|
405
|
+
>>> provideAdapter(AttributeAnnotations)
|
406
|
+
|
407
|
+
We also need a context that is annotatable.
|
408
|
+
|
409
|
+
.. code-block:: python
|
410
|
+
|
411
|
+
>>> from zope.annotation.interfaces import IAttributeAnnotatable
|
412
|
+
>>> from zope.interface import alsoProvides
|
413
|
+
>>> alsoProvides(context, IAttributeAnnotatable)
|
414
|
+
|
415
|
+
Now, let's create a persistent tile with a schema.
|
416
|
+
|
417
|
+
.. code-block:: python
|
418
|
+
|
419
|
+
>>> class IPersistentSampleData(Interface):
|
420
|
+
... text = zope.schema.Text(title=u'Detailed text', missing_value=u'Missing!')
|
421
|
+
|
422
|
+
>>> from plone.tiles import PersistentTile
|
423
|
+
>>> class PersistentSampleTile(PersistentTile):
|
424
|
+
...
|
425
|
+
... __name__ = 'sample.persistenttile' # would normally be set by ZCML handler
|
426
|
+
...
|
427
|
+
... def __call__(self):
|
428
|
+
... return u'<b>You said</b> %s' % self.data['text']
|
429
|
+
|
430
|
+
>>> persistentSampleTileType = TileType(
|
431
|
+
... u'sample.persistenttile',
|
432
|
+
... u'Persistent sample tile',
|
433
|
+
... 'dummy.Permission',
|
434
|
+
... 'dummy.Permission',
|
435
|
+
... description=u'A tile used for testing',
|
436
|
+
... schema=IPersistentSampleData)
|
437
|
+
|
438
|
+
>>> provideUtility(persistentSampleTileType, name=u'sample.persistenttile')
|
439
|
+
>>> provideAdapter(PersistentSampleTile, (Interface, Interface), IBasicTile, name=u'sample.persistenttile')
|
440
|
+
|
441
|
+
We can now traverse to the tile as before.
|
442
|
+
By default, there is no data, and the field's missing value will be used.
|
443
|
+
|
444
|
+
.. code-block:: python
|
445
|
+
|
446
|
+
>>> request = TestRequest()
|
447
|
+
|
448
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.persistenttile')
|
449
|
+
>>> tile = tile['tile2']
|
450
|
+
>>> tile.__name__
|
451
|
+
'sample.persistenttile'
|
452
|
+
>>> tile.id
|
453
|
+
'tile2'
|
454
|
+
|
455
|
+
>>> tile()
|
456
|
+
'<b>You said</b> Missing!'
|
457
|
+
|
458
|
+
At this point, there is nothing in the annotations for the type either:
|
459
|
+
|
460
|
+
.. code-block:: python
|
461
|
+
|
462
|
+
>>> list(dict(getattr(context, '__annotations__', {})).keys())
|
463
|
+
[]
|
464
|
+
|
465
|
+
We can write data to the context's annotations using an ``ITileDataManager``:
|
466
|
+
|
467
|
+
.. code-block:: python
|
468
|
+
|
469
|
+
>>> dataManager = ITileDataManager(tile)
|
470
|
+
>>> dataManager.set({'text': 'Hello!'})
|
471
|
+
|
472
|
+
This writes data to annotations:
|
473
|
+
|
474
|
+
.. code-block:: python
|
475
|
+
|
476
|
+
>>> list(dict(context.__annotations__).keys())
|
477
|
+
['plone.tiles.data.tile2']
|
478
|
+
>>> context.__annotations__[u'plone.tiles.data.tile2']
|
479
|
+
{'text': 'Hello!'}
|
480
|
+
|
481
|
+
We can get this from the data manager too, of course:
|
482
|
+
|
483
|
+
.. code-block:: python
|
484
|
+
|
485
|
+
>>> dataManager.get()
|
486
|
+
{'text': 'Hello!'}
|
487
|
+
|
488
|
+
Note that as with transient tiles,
|
489
|
+
the ``data`` attribute is cached and will only be looked up once.
|
490
|
+
|
491
|
+
If we now look up the tile again, we will get the new value:
|
492
|
+
|
493
|
+
.. code-block:: python
|
494
|
+
|
495
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.persistenttile')
|
496
|
+
>>> tile = tile['tile2']
|
497
|
+
>>> tile()
|
498
|
+
'<b>You said</b> Hello!'
|
499
|
+
|
500
|
+
>>> tile.data
|
501
|
+
{'text': 'Hello!'}
|
502
|
+
|
503
|
+
We can also remove the annotation using the data manager:
|
504
|
+
|
505
|
+
.. code-block:: python
|
506
|
+
|
507
|
+
>>> dataManager.delete()
|
508
|
+
>>> sorted(dict(context.__annotations__).items()) # doctest: +ELLIPSIS
|
509
|
+
[]
|
510
|
+
|
511
|
+
Overriding transient data with persistent
|
512
|
+
-----------------------------------------
|
513
|
+
|
514
|
+
To be able to reuse the same centrally managed tile based layouts for multiple context objects,
|
515
|
+
but still allow optional customization for tiles,
|
516
|
+
it's possible to override otherwise transient tile configuration with context specific persistent configuration.
|
517
|
+
|
518
|
+
This is done by either by setting a client side request header or query param ``X-Tile-Persistent``:
|
519
|
+
|
520
|
+
.. code-block:: python
|
521
|
+
|
522
|
+
>>> request = TestRequest(
|
523
|
+
... form={'title': u'My title', 'count': 5, 'cssClass': u'foo',
|
524
|
+
... 'X-Tile-Persistent': 'yes'}
|
525
|
+
... )
|
526
|
+
|
527
|
+
Yet, just adding the flag, doesn't create new persistent annotations on GET requests:
|
528
|
+
|
529
|
+
.. code-block:: python
|
530
|
+
|
531
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
532
|
+
>>> ITileDataManager(tile)
|
533
|
+
<plone.tiles.data.PersistentTileDataManager object at ...>
|
534
|
+
|
535
|
+
>>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
|
536
|
+
[('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
|
537
|
+
|
538
|
+
>>> list(IAnnotations(context).keys())
|
539
|
+
[]
|
540
|
+
|
541
|
+
That's because the data is persistent only once it's set:
|
542
|
+
|
543
|
+
.. code-block:: python
|
544
|
+
|
545
|
+
>>> data = ITileDataManager(tile).get()
|
546
|
+
>>> data.update({'count': 6})
|
547
|
+
>>> ITileDataManager(tile).set(data)
|
548
|
+
>>> list(IAnnotations(context).keys())
|
549
|
+
['plone.tiles.data...']
|
550
|
+
|
551
|
+
>>> sorted(list(IAnnotations(context).values())[0].items(), key=lambda x: x[0])
|
552
|
+
[('count', 6), ('cssClass', 'foo'), ('title', 'My title')]
|
553
|
+
|
554
|
+
>>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
|
555
|
+
[('count', 6), ('cssClass', 'foo'), ('title', 'My title')]
|
556
|
+
|
557
|
+
Without the persistent flag, fixed transient data would be returned:
|
558
|
+
|
559
|
+
.. code-block:: python
|
560
|
+
|
561
|
+
>>> request = TestRequest(
|
562
|
+
... form={'title': u'My title', 'count': 5, 'cssClass': u'foo'},
|
563
|
+
... )
|
564
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
565
|
+
>>> ITileDataManager(tile)
|
566
|
+
<plone.tiles.data.TransientTileDataManager object at ...>
|
567
|
+
|
568
|
+
>>> data = ITileDataManager(tile).get()
|
569
|
+
>>> sorted(data.items(), key=lambda x: x[0])
|
570
|
+
[('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
|
571
|
+
|
572
|
+
Finally, the persistent override could also be deleted:
|
573
|
+
|
574
|
+
.. code-block:: python
|
575
|
+
|
576
|
+
>>> request = TestRequest(
|
577
|
+
... form={'title': u'My title', 'count': 5, 'cssClass': u'foo',
|
578
|
+
... 'X-Tile-Persistent': 'yes'}
|
579
|
+
... )
|
580
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
581
|
+
>>> ITileDataManager(tile)
|
582
|
+
<plone.tiles.data.PersistentTileDataManager object at ...>
|
583
|
+
|
584
|
+
>>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
|
585
|
+
[('count', 6), ('cssClass', 'foo'), ('title', 'My title')]
|
586
|
+
|
587
|
+
>>> ITileDataManager(tile).delete()
|
588
|
+
>>> list(IAnnotations(context).keys())
|
589
|
+
[]
|
590
|
+
|
591
|
+
>>> sorted(ITileDataManager(tile).get().items(), key=lambda x: x[0])
|
592
|
+
[('count', 5), ('cssClass', 'foo'), ('title', 'My title')]
|
593
|
+
|
594
|
+
>>> request = TestRequest(
|
595
|
+
... form={'title': u'My title', 'count': 5, 'cssClass': u'foo'},
|
596
|
+
... )
|
597
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.tile')
|
598
|
+
>>> ITileDataManager(tile)
|
599
|
+
<plone.tiles.data.TransientTileDataManager object at ...>
|
600
|
+
|
601
|
+
|
602
|
+
Tile URLs
|
603
|
+
---------
|
604
|
+
|
605
|
+
As we have seen, tiles have a canonical URL.
|
606
|
+
For transient tiles, this may also encode some tile data.
|
607
|
+
|
608
|
+
If you have a tile instance and you need to know the canonical tile URL,
|
609
|
+
you can use the ``IAbsoluteURL`` API.
|
610
|
+
|
611
|
+
For the purposes of testing,
|
612
|
+
we need to ensure that we can get an absolute URL for the context.
|
613
|
+
We'll achieve that with a dummy adapter:
|
614
|
+
|
615
|
+
.. code-block:: python
|
616
|
+
|
617
|
+
>>> from zope.interface import implementer
|
618
|
+
>>> from zope.component import adapter
|
619
|
+
|
620
|
+
>>> from zope.traversing.browser.interfaces import IAbsoluteURL
|
621
|
+
>>> from zope.publisher.interfaces.http import IHTTPRequest
|
622
|
+
|
623
|
+
>>> @implementer(IAbsoluteURL)
|
624
|
+
... @adapter(IContext, IHTTPRequest)
|
625
|
+
... class DummyAbsoluteURL(object):
|
626
|
+
...
|
627
|
+
... def __init__(self, context, request):
|
628
|
+
... self.context = context
|
629
|
+
... self.request = request
|
630
|
+
...
|
631
|
+
... def __unicode__(self):
|
632
|
+
... return u'http://example.com/context'
|
633
|
+
... def __str__(self):
|
634
|
+
... return u'http://example.com/context'
|
635
|
+
... def __call__(self):
|
636
|
+
... return self.__str__()
|
637
|
+
... def breadcrumbs(self):
|
638
|
+
... return ({'name': u'context', 'url': 'http://example.com/context'},)
|
639
|
+
>>> provideAdapter(DummyAbsoluteURL, name=u'absolute_url')
|
640
|
+
>>> provideAdapter(DummyAbsoluteURL)
|
641
|
+
|
642
|
+
>>> from zope.traversing.browser.absoluteurl import absoluteURL
|
643
|
+
>>> from zope.component import getMultiAdapter
|
644
|
+
|
645
|
+
>>> context = Context()
|
646
|
+
>>> request = TestRequest(form={'title': u'My title', 'count': 5, 'cssClass': u'foo'})
|
647
|
+
>>> transientTile = getMultiAdapter((context, request), name=u'sample.tile')
|
648
|
+
>>> transientTile = transientTile['tile1']
|
649
|
+
|
650
|
+
>>> absoluteURL(transientTile, request)
|
651
|
+
'http://example.com/context/@@sample.tile/tile1?title=My+title&cssClass=foo&count%3Along=5'
|
652
|
+
|
653
|
+
>>> getMultiAdapter((transientTile, request), IAbsoluteURL).breadcrumbs() == \
|
654
|
+
... ({'url': 'http://example.com/context', 'name': u'context'},
|
655
|
+
... {'url': 'http://example.com/context/@@sample.tile/tile1', 'name': 'sample.tile'})
|
656
|
+
True
|
657
|
+
|
658
|
+
For convenience, the tile URL is also available under the ``url`` property:
|
659
|
+
|
660
|
+
.. code-block:: python
|
661
|
+
|
662
|
+
>>> transientTile.url
|
663
|
+
'http://example.com/context/@@sample.tile/tile1?title=My+title&cssClass=foo&count%3Along=5'
|
664
|
+
|
665
|
+
The tile absolute URL structure remains unaltered if the data is
|
666
|
+
coming from a `_tiledata` JSON-encoded parameter instead of from the request
|
667
|
+
parameters directly:
|
668
|
+
|
669
|
+
.. code-block:: python
|
670
|
+
|
671
|
+
>>> request = TestRequest(form={'_tiledata': json.dumps({'title': u'Your title', 'count': 6, 'cssClass': u'bar'})})
|
672
|
+
>>> transientTile = getMultiAdapter((context, request), name=u'sample.tile')
|
673
|
+
>>> transientTile = transientTile['tile1']
|
674
|
+
|
675
|
+
>>> absoluteURL(transientTile, request)
|
676
|
+
'http://example.com/context/@@sample.tile/tile1?title=Your+title&cssClass=bar&count%3Along=6'
|
677
|
+
|
678
|
+
For persistent tiles, the are no data parameters:
|
679
|
+
|
680
|
+
.. code-block:: python
|
681
|
+
|
682
|
+
>>> context = Context()
|
683
|
+
>>> request = TestRequest(form={'title': u'Ignored', 'count': 0, 'cssClass': u'ignored'})
|
684
|
+
>>> persistentTile = getMultiAdapter((context, request), name=u'sample.persistenttile')
|
685
|
+
>>> persistentTile = persistentTile['tile2']
|
686
|
+
|
687
|
+
>>> absoluteURL(persistentTile, request)
|
688
|
+
'http://example.com/context/@@sample.persistenttile/tile2'
|
689
|
+
|
690
|
+
>>> getMultiAdapter((persistentTile, request), IAbsoluteURL).breadcrumbs() == \
|
691
|
+
... ({'url': 'http://example.com/context', 'name': u'context'},
|
692
|
+
... {'url': 'http://example.com/context/@@sample.persistenttile/tile2', 'name': 'sample.persistenttile'})
|
693
|
+
True
|
694
|
+
|
695
|
+
And again, for convenience:
|
696
|
+
|
697
|
+
.. code-block:: python
|
698
|
+
|
699
|
+
>>> persistentTile.url
|
700
|
+
'http://example.com/context/@@sample.persistenttile/tile2'
|
701
|
+
|
702
|
+
If the tile doesn't have an id, we don't get any sub-path:
|
703
|
+
|
704
|
+
.. code-block:: python
|
705
|
+
|
706
|
+
>>> request = TestRequest(form={'title': u'My title', 'count': 5, 'cssClass': u'foo'})
|
707
|
+
>>> transientTile = getMultiAdapter((context, request), name=u'sample.tile')
|
708
|
+
>>> absoluteURL(transientTile, request)
|
709
|
+
'http://example.com/context/@@sample.tile?title=My+title&cssClass=foo&count%3Along=5'
|
710
|
+
|
711
|
+
>>> request = TestRequest()
|
712
|
+
>>> persistentTile = getMultiAdapter((context, request), name=u'sample.persistenttile')
|
713
|
+
>>> absoluteURL(persistentTile, request)
|
714
|
+
'http://example.com/context/@@sample.persistenttile'
|
715
|
+
|
716
|
+
|
717
|
+
We can also disallow query parameters providing data into our tiles
|
718
|
+
|
719
|
+
.. code-block:: python
|
720
|
+
|
721
|
+
>>> import zope.schema
|
722
|
+
>>> from plone.tiles.directives import ignore_querystring
|
723
|
+
|
724
|
+
>>> class ISampleTileData(Interface):
|
725
|
+
... unfiltered = zope.schema.Text(title=u'Unfiltered data')
|
726
|
+
... ignore_querystring('unfiltered')
|
727
|
+
... filtered = zope.schema.Text(title=u'filtered data')
|
728
|
+
|
729
|
+
>>> sampleTileType.schema = ISampleTileData
|
730
|
+
|
731
|
+
And create a tile with our new schema
|
732
|
+
|
733
|
+
.. code-block:: python
|
734
|
+
|
735
|
+
>>> from plone.tiles import Tile
|
736
|
+
>>> class SampleTile(Tile):
|
737
|
+
...
|
738
|
+
... __name__ = 'sample.unfilteredtile' # would normally be set by a ZCML handler
|
739
|
+
...
|
740
|
+
... def __call__(self):
|
741
|
+
... return '<html><body><div>{}{}</div></body></html>'.format(
|
742
|
+
... self.data.get('unfiltered') or '',
|
743
|
+
... self.data.get('filtered') or '')
|
744
|
+
|
745
|
+
We'll register the sample unfiltered tile directly here, for testing.
|
746
|
+
|
747
|
+
.. code-block:: python
|
748
|
+
|
749
|
+
>>> from zope.component import provideAdapter, provideUtility
|
750
|
+
>>> from zope.interface import Interface
|
751
|
+
>>> from plone.tiles.interfaces import IBasicTile
|
752
|
+
|
753
|
+
>>> provideUtility(sampleTileType, name=u'sample.unfilteredtile')
|
754
|
+
>>> provideAdapter(SampleTile, (Interface, Interface), IBasicTile, name=u'sample.unfilteredtile')
|
755
|
+
|
756
|
+
|
757
|
+
Let's simulate traversal to test if form data is used:
|
758
|
+
|
759
|
+
.. code-block:: python
|
760
|
+
|
761
|
+
>>> context = Context()
|
762
|
+
>>> request = TestRequest(form={'unfiltered': 'foobar', 'filtered': 'safe'})
|
763
|
+
|
764
|
+
>>> tile = getMultiAdapter((context, request), name=u'sample.unfilteredtile')
|
765
|
+
>>> tile = tile['tile1']
|
766
|
+
|
767
|
+
Data should not contain unfiltered field:
|
768
|
+
|
769
|
+
.. code-block:: python
|
770
|
+
|
771
|
+
>>> sorted(tile.data.items())
|
772
|
+
[('filtered', 'safe')]
|
773
|
+
|
774
|
+
|
775
|
+
Rendering the tile should not include ignored query string:
|
776
|
+
|
777
|
+
.. code-block:: python
|
778
|
+
|
779
|
+
>>> 'foobar' in tile()
|
780
|
+
False
|
781
|
+
|
782
|
+
>>> tile()
|
783
|
+
'<html><body><div>safe</div></body></html>'
|