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/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>'