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/data.py
ADDED
@@ -0,0 +1,422 @@
|
|
1
|
+
from persistent.dict import PersistentDict
|
2
|
+
from plone.subrequest import ISubRequest
|
3
|
+
from plone.tiles.directives import IGNORE_QUERYSTRING_KEY
|
4
|
+
from plone.tiles.interfaces import IFieldTypeConverter
|
5
|
+
from plone.tiles.interfaces import IPersistentTile
|
6
|
+
from plone.tiles.interfaces import ITile
|
7
|
+
from plone.tiles.interfaces import ITileDataContext
|
8
|
+
from plone.tiles.interfaces import ITileDataManager
|
9
|
+
from plone.tiles.interfaces import ITileDataStorage
|
10
|
+
from plone.tiles.interfaces import ITileType
|
11
|
+
from urllib import parse
|
12
|
+
from zope.annotation.interfaces import IAnnotations
|
13
|
+
from zope.component import adapter
|
14
|
+
from zope.component import getMultiAdapter
|
15
|
+
from zope.component import queryUtility
|
16
|
+
from zope.interface import implementer
|
17
|
+
from zope.interface import Interface
|
18
|
+
from zope.interface.interfaces import ComponentLookupError
|
19
|
+
from zope.schema import getFields
|
20
|
+
from zope.schema import getFieldsInOrder
|
21
|
+
from zope.schema.interfaces import ISequence
|
22
|
+
|
23
|
+
import json
|
24
|
+
import logging
|
25
|
+
import pkg_resources
|
26
|
+
|
27
|
+
|
28
|
+
try:
|
29
|
+
pkg_resources.get_distribution("plone.rfc822")
|
30
|
+
except pkg_resources.DistributionNotFound:
|
31
|
+
HAS_RFC822 = False
|
32
|
+
else:
|
33
|
+
from plone.rfc822.interfaces import IPrimaryField
|
34
|
+
|
35
|
+
HAS_RFC822 = True
|
36
|
+
|
37
|
+
|
38
|
+
ANNOTATIONS_KEY_PREFIX = "plone.tiles.data"
|
39
|
+
LOGGER = logging.getLogger("plone.tiles")
|
40
|
+
|
41
|
+
|
42
|
+
@adapter(ITile)
|
43
|
+
@implementer(ITileDataManager)
|
44
|
+
def transientTileDataManagerFactory(tile):
|
45
|
+
if tile.request.get("X-Tile-Persistent"):
|
46
|
+
return PersistentTileDataManager(tile)
|
47
|
+
else:
|
48
|
+
return TransientTileDataManager(tile)
|
49
|
+
|
50
|
+
|
51
|
+
class BaseTileDataManager:
|
52
|
+
|
53
|
+
def get_default_request_data(self):
|
54
|
+
"""
|
55
|
+
from request form
|
56
|
+
"""
|
57
|
+
# try to use a '_tiledata' parameter in the request
|
58
|
+
if "_tiledata" in self.tile.request.form:
|
59
|
+
data = json.loads(self.tile.request.form["_tiledata"])
|
60
|
+
elif self.tileType is None or self.tileType.schema is None:
|
61
|
+
data = self.tile.request.form.copy()
|
62
|
+
else:
|
63
|
+
# Try to decode the form data properly if we can
|
64
|
+
try:
|
65
|
+
data = decode(
|
66
|
+
self.tile.request.form,
|
67
|
+
self.tileType.schema,
|
68
|
+
missing=True,
|
69
|
+
primary=True,
|
70
|
+
)
|
71
|
+
except (
|
72
|
+
ValueError,
|
73
|
+
UnicodeDecodeError,
|
74
|
+
):
|
75
|
+
LOGGER.exception("Could not convert form data to schema")
|
76
|
+
return self.data.copy()
|
77
|
+
# we're assuming this data is potentially unsafe so we need to check
|
78
|
+
# the ignore querystring field setting
|
79
|
+
|
80
|
+
# before we start, we allow it for sub-requests since in this case,
|
81
|
+
# the input is safe and we can trust it
|
82
|
+
if ISubRequest.providedBy(self.tile.request):
|
83
|
+
return data
|
84
|
+
|
85
|
+
# first off, we only care to filter if it is a GET request
|
86
|
+
if getattr(self.tile.request, "REQUEST_METHOD", "GET") != "GET":
|
87
|
+
return data
|
88
|
+
|
89
|
+
# now, pay attention to schema hints for form data
|
90
|
+
if self.tileType is not None and self.tileType.schema is not None:
|
91
|
+
for name in (
|
92
|
+
self.tileType.schema.queryTaggedValue(IGNORE_QUERYSTRING_KEY) or []
|
93
|
+
):
|
94
|
+
if name in data:
|
95
|
+
del data[name]
|
96
|
+
|
97
|
+
return data
|
98
|
+
|
99
|
+
|
100
|
+
@adapter(ITile)
|
101
|
+
@implementer(ITileDataManager)
|
102
|
+
class TransientTileDataManager(BaseTileDataManager):
|
103
|
+
"""A data manager for transient tile data, which reads data from the
|
104
|
+
request query string.
|
105
|
+
"""
|
106
|
+
|
107
|
+
def __init__(self, tile):
|
108
|
+
self.tile = tile
|
109
|
+
self.tileType = queryUtility(ITileType, name=tile.__name__)
|
110
|
+
|
111
|
+
self.context = getMultiAdapter(
|
112
|
+
(tile.context, tile.request, tile), ITileDataContext
|
113
|
+
)
|
114
|
+
self.storage = getMultiAdapter(
|
115
|
+
(self.context, tile.request, tile), ITileDataStorage
|
116
|
+
)
|
117
|
+
|
118
|
+
if IAnnotations.providedBy(self.storage):
|
119
|
+
self.key = ".".join([ANNOTATIONS_KEY_PREFIX, str(tile.id)])
|
120
|
+
else:
|
121
|
+
self.key = str(tile.id)
|
122
|
+
|
123
|
+
@property
|
124
|
+
def annotations(self): # BBB for < 0.7.0 support
|
125
|
+
return self.storage
|
126
|
+
|
127
|
+
def get(self):
|
128
|
+
# use explicitly set data (saved as annotation on the request)
|
129
|
+
if self.key in self.storage:
|
130
|
+
data = dict(self.storage[self.key])
|
131
|
+
|
132
|
+
if self.tileType is not None and self.tileType.schema is not None:
|
133
|
+
for name, field in getFields(self.tileType.schema).items():
|
134
|
+
if name not in data:
|
135
|
+
data[name] = field.missing_value
|
136
|
+
# fall back to the copy of request.form object itself
|
137
|
+
else:
|
138
|
+
data = self.get_default_request_data()
|
139
|
+
|
140
|
+
return data
|
141
|
+
|
142
|
+
def set(self, data):
|
143
|
+
self.storage[self.key] = data
|
144
|
+
|
145
|
+
def delete(self):
|
146
|
+
if self.key in self.storage:
|
147
|
+
self.storage[self.key] = {}
|
148
|
+
|
149
|
+
|
150
|
+
@adapter(IPersistentTile)
|
151
|
+
@implementer(ITileDataManager)
|
152
|
+
class PersistentTileDataManager(BaseTileDataManager):
|
153
|
+
"""A data reader for persistent tiles operating on annotatable contexts.
|
154
|
+
The data is retrieved from an annotation.
|
155
|
+
"""
|
156
|
+
|
157
|
+
def __init__(self, tile):
|
158
|
+
self.tile = tile
|
159
|
+
self.tileType = queryUtility(ITileType, name=tile.__name__)
|
160
|
+
|
161
|
+
self.context = getMultiAdapter(
|
162
|
+
(tile.context, tile.request, tile), ITileDataContext
|
163
|
+
)
|
164
|
+
self.storage = getMultiAdapter(
|
165
|
+
(self.context, tile.request, tile), ITileDataStorage
|
166
|
+
)
|
167
|
+
|
168
|
+
if IAnnotations.providedBy(self.storage):
|
169
|
+
self.key = ".".join([ANNOTATIONS_KEY_PREFIX, str(tile.id)])
|
170
|
+
else:
|
171
|
+
self.key = str(tile.id)
|
172
|
+
|
173
|
+
@property
|
174
|
+
def annotations(self): # BBB for < 0.7.0 support
|
175
|
+
return self.storage
|
176
|
+
|
177
|
+
def get(self):
|
178
|
+
data = self.get_default_request_data()
|
179
|
+
data.update(dict(self.storage.get(self.key, {})))
|
180
|
+
if self.tileType is not None and self.tileType.schema is not None:
|
181
|
+
for name, field in getFields(self.tileType.schema).items():
|
182
|
+
if name not in data:
|
183
|
+
data[name] = field.missing_value
|
184
|
+
return data
|
185
|
+
|
186
|
+
def set(self, data):
|
187
|
+
self.storage[self.key] = PersistentDict(data)
|
188
|
+
|
189
|
+
def delete(self):
|
190
|
+
if self.key in self.storage:
|
191
|
+
del self.storage[self.key]
|
192
|
+
|
193
|
+
|
194
|
+
@implementer(ITileDataContext)
|
195
|
+
@adapter(Interface, Interface, ITile)
|
196
|
+
def defaultTileDataContext(context, request, tile):
|
197
|
+
return tile.context
|
198
|
+
|
199
|
+
|
200
|
+
@implementer(ITileDataStorage)
|
201
|
+
@adapter(Interface, Interface, ITile)
|
202
|
+
def defaultTileDataStorage(context, request, tile):
|
203
|
+
if tile.request.get("X-Tile-Persistent"):
|
204
|
+
return defaultPersistentTileDataStorage(context, request, tile)
|
205
|
+
else:
|
206
|
+
return IAnnotations(tile.request, tile.request.form)
|
207
|
+
|
208
|
+
|
209
|
+
@implementer(ITileDataStorage)
|
210
|
+
@adapter(Interface, Interface, IPersistentTile)
|
211
|
+
def defaultPersistentTileDataStorage(context, request, tile):
|
212
|
+
return IAnnotations(context)
|
213
|
+
|
214
|
+
|
215
|
+
# Encoding
|
216
|
+
|
217
|
+
|
218
|
+
def map_to_pairs(encoded_name, value):
|
219
|
+
"""Given an encoded basename, e.g. "foo:record" or "foo:record:list" and
|
220
|
+
a dictionary value, yields (encoded_name, value) pairs to be included
|
221
|
+
in the final encode.
|
222
|
+
"""
|
223
|
+
prefix, postfix = encoded_name.split(":", 1)
|
224
|
+
postfix = postfix.replace("record:list", "records")
|
225
|
+
|
226
|
+
def guess_type(v):
|
227
|
+
if isinstance(v, str):
|
228
|
+
return ""
|
229
|
+
if isinstance(v, bool):
|
230
|
+
return ":boolean"
|
231
|
+
if isinstance(v, int):
|
232
|
+
return ":int"
|
233
|
+
if isinstance(v, float):
|
234
|
+
return ":float"
|
235
|
+
return ""
|
236
|
+
|
237
|
+
for item_name, item_value in value.items():
|
238
|
+
if isinstance(item_value, str):
|
239
|
+
item_value = item_value.encode("utf-8")
|
240
|
+
|
241
|
+
if isinstance(item_value, list) or isinstance(item_value, tuple):
|
242
|
+
for item_subvalue in item_value:
|
243
|
+
marshall_type = guess_type(item_subvalue)
|
244
|
+
if isinstance(item_subvalue, bool):
|
245
|
+
item_subvalue = item_subvalue and "1" or ""
|
246
|
+
elif isinstance(item_subvalue, str):
|
247
|
+
item_subvalue = item_subvalue.encode("utf-8")
|
248
|
+
encoded_name = "{}.{}{}:list:{}".format(
|
249
|
+
prefix, item_name, marshall_type, postfix
|
250
|
+
)
|
251
|
+
yield encoded_name, item_subvalue
|
252
|
+
else:
|
253
|
+
marshall_type = guess_type(item_value)
|
254
|
+
if isinstance(item_value, bool):
|
255
|
+
item_value = item_value and "1" or ""
|
256
|
+
elif isinstance(item_value, str):
|
257
|
+
item_value = item_value.encode("utf-8")
|
258
|
+
encoded_name = "{:s}.{:s}{:s}:{:s}".format(
|
259
|
+
prefix, item_name, marshall_type, postfix
|
260
|
+
)
|
261
|
+
yield encoded_name, item_value
|
262
|
+
|
263
|
+
|
264
|
+
def encode(data, schema, ignore=()):
|
265
|
+
"""Given a data dictionary with key/value pairs and schema, return an
|
266
|
+
encoded query string. This is similar to urllib.urlencode(), but field
|
267
|
+
names will include the appropriate field type converters, e.g. an int
|
268
|
+
field will be encoded as fieldname:int=123. Fields not found in the data
|
269
|
+
dict will be ignored, and items in the dict not in the schema will also
|
270
|
+
be ignored. Additional fields to ignore can be passed with the 'ignore'
|
271
|
+
parameter. If any fields cannot be converted, a ComponentLookupError
|
272
|
+
will be raised.
|
273
|
+
"""
|
274
|
+
|
275
|
+
encode = []
|
276
|
+
|
277
|
+
for name, field in getFieldsInOrder(schema):
|
278
|
+
if HAS_RFC822 and IPrimaryField.providedBy(field):
|
279
|
+
continue
|
280
|
+
|
281
|
+
if name in ignore or name not in data:
|
282
|
+
continue
|
283
|
+
|
284
|
+
converter = IFieldTypeConverter(field, None)
|
285
|
+
if converter is None:
|
286
|
+
raise ComponentLookupError(
|
287
|
+
f"Cannot URL encode {name} of type {field.__class__}"
|
288
|
+
)
|
289
|
+
|
290
|
+
encoded_name = name
|
291
|
+
if converter.token:
|
292
|
+
encoded_name = ":".join([name, converter.token])
|
293
|
+
|
294
|
+
value = data[name]
|
295
|
+
if value is None:
|
296
|
+
continue
|
297
|
+
elif isinstance(value, str):
|
298
|
+
value = value.encode("utf-8")
|
299
|
+
|
300
|
+
if ISequence.providedBy(field):
|
301
|
+
value_type_converter = IFieldTypeConverter(field.value_type, None)
|
302
|
+
if value_type_converter is None:
|
303
|
+
raise ComponentLookupError(
|
304
|
+
"Cannot URL encode value type for {} of type "
|
305
|
+
"{} : {}".format(name, field.__class__, field.value_type.__class__)
|
306
|
+
)
|
307
|
+
|
308
|
+
if value_type_converter.token:
|
309
|
+
encoded_name = ":".join(
|
310
|
+
[name, value_type_converter.token, converter.token]
|
311
|
+
)
|
312
|
+
|
313
|
+
for item in value:
|
314
|
+
|
315
|
+
if isinstance(item, bool):
|
316
|
+
item = item and "1" or ""
|
317
|
+
elif isinstance(item, str):
|
318
|
+
item = item.encode("utf-8")
|
319
|
+
|
320
|
+
if isinstance(item, dict):
|
321
|
+
encode.extend(map_to_pairs(encoded_name, item))
|
322
|
+
else:
|
323
|
+
encode.append(
|
324
|
+
(
|
325
|
+
encoded_name,
|
326
|
+
item,
|
327
|
+
)
|
328
|
+
)
|
329
|
+
|
330
|
+
else:
|
331
|
+
# The :bool converter just does bool() value, but urlencode() does
|
332
|
+
# str() on the object. The result is False => 'False' => True :(
|
333
|
+
if isinstance(value, bool):
|
334
|
+
value = value and "1" or ""
|
335
|
+
|
336
|
+
if isinstance(value, dict):
|
337
|
+
encode.extend(map_to_pairs(encoded_name, value))
|
338
|
+
else:
|
339
|
+
encode.append((encoded_name, value))
|
340
|
+
|
341
|
+
return parse.urlencode(encode)
|
342
|
+
|
343
|
+
|
344
|
+
# Decoding
|
345
|
+
|
346
|
+
|
347
|
+
def decode(data, schema, missing=True, primary=False):
|
348
|
+
"""Decode a data dict according to a schema. The returned dictionary will
|
349
|
+
contain only keys matching schema names, and will force type values
|
350
|
+
appropriately.
|
351
|
+
|
352
|
+
This function is only necessary because the encoders used by encode()
|
353
|
+
are not sufficiently detailed to always return the exact type expected
|
354
|
+
by a field, e.g. resulting in ascii/unicode discrepancies.
|
355
|
+
|
356
|
+
If missing is True, fields that are in the schema but not in the data will
|
357
|
+
be set to field.missing_value. Otherwise, they are ignored.
|
358
|
+
|
359
|
+
If primary is True, also fields that are marged as primary fields are
|
360
|
+
decoded from the data. (Primary fields are not decoded by default,
|
361
|
+
because primary field are mainly used for rich text or binary fields
|
362
|
+
and data is usually parsed from query string with length limitations.)
|
363
|
+
"""
|
364
|
+
|
365
|
+
decoded = {}
|
366
|
+
|
367
|
+
for name, field in getFields(schema).items():
|
368
|
+
if not primary and HAS_RFC822 and IPrimaryField.providedBy(field):
|
369
|
+
continue
|
370
|
+
|
371
|
+
if name not in data:
|
372
|
+
if missing:
|
373
|
+
decoded[name] = field.missing_value
|
374
|
+
continue
|
375
|
+
|
376
|
+
value = data[name]
|
377
|
+
if value is None:
|
378
|
+
continue
|
379
|
+
|
380
|
+
field_type = field._type
|
381
|
+
if isinstance(
|
382
|
+
field_type,
|
383
|
+
(
|
384
|
+
tuple,
|
385
|
+
list,
|
386
|
+
),
|
387
|
+
):
|
388
|
+
field_type = field_type[-1]
|
389
|
+
|
390
|
+
if ISequence.providedBy(field):
|
391
|
+
converted = []
|
392
|
+
|
393
|
+
value_type_field_type = field.value_type._type
|
394
|
+
if isinstance(
|
395
|
+
value_type_field_type,
|
396
|
+
(
|
397
|
+
tuple,
|
398
|
+
list,
|
399
|
+
),
|
400
|
+
):
|
401
|
+
value_type_field_type = value_type_field_type[-1]
|
402
|
+
|
403
|
+
for item in value:
|
404
|
+
if field.value_type._type and not isinstance(
|
405
|
+
item, field.value_type._type
|
406
|
+
):
|
407
|
+
item = value_type_field_type(item)
|
408
|
+
converted.append(item)
|
409
|
+
|
410
|
+
value = converted
|
411
|
+
elif isinstance(value, (tuple, list)) and value:
|
412
|
+
value = value[0]
|
413
|
+
|
414
|
+
if isinstance(value, bytes):
|
415
|
+
value = value.decode("utf-8")
|
416
|
+
|
417
|
+
if field._type is not None and not isinstance(value, field._type):
|
418
|
+
value = field_type(value)
|
419
|
+
|
420
|
+
decoded[name] = value
|
421
|
+
|
422
|
+
return decoded
|
plone/tiles/data.rst
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
======================
|
2
|
+
Data encoding/decoding
|
3
|
+
======================
|
4
|
+
|
5
|
+
This test exercises the ``encode()`` and ``decode()`` methods in ``plone.tiles.data``.
|
6
|
+
|
7
|
+
.. code-block:: python
|
8
|
+
|
9
|
+
>>> from zope.interface import Interface
|
10
|
+
>>> from zope import schema
|
11
|
+
|
12
|
+
>>> from plone.tiles.data import encode, decode
|
13
|
+
|
14
|
+
Encoding
|
15
|
+
--------
|
16
|
+
|
17
|
+
First, we'll create a simple schema that exercises several field types:
|
18
|
+
|
19
|
+
.. code-block:: python
|
20
|
+
|
21
|
+
>>> weekdays = [u'Monday', u'Tuesday', u'Wednesday', u'Thursday',
|
22
|
+
... u'Friday', u'Saturday', u'Sunday']
|
23
|
+
>>> class ISimple(Interface):
|
24
|
+
... text_line = schema.TextLine(title=u'Text')
|
25
|
+
... ascii_line = schema.ASCIILine(title=u'ASCII')
|
26
|
+
... text = schema.Text(title=u'Text', missing_value=u'Missing')
|
27
|
+
... ascii = schema.ASCII(title=u'ASCII')
|
28
|
+
... int = schema.Int(title=u'Int')
|
29
|
+
... float = schema.Float(title=u'Float')
|
30
|
+
... bool = schema.Bool(title=u'Bool')
|
31
|
+
... weekday = schema.Choice(title=u'Weekday', values=weekdays)
|
32
|
+
... list = schema.List(value_type=schema.TextLine())
|
33
|
+
... listchoice = schema.List(value_type=schema.Choice(vocabulary='foobar'))
|
34
|
+
|
35
|
+
A simple encode produces a query string:
|
36
|
+
|
37
|
+
.. code-block:: python
|
38
|
+
|
39
|
+
>>> data = dict(text_line=u'A', ascii_line='B', text=u'C\nD', ascii='E\nF', int=3, float=1.2, bool=False, weekday=u'Saturday')
|
40
|
+
>>> encode(data, ISimple)
|
41
|
+
'text_line=A&ascii_line=B&text%3Atext=C%0AD&ascii%3Atext=E%0AF&int%3Along=3&float%3Afloat=1.2&bool%3Aboolean=&weekday=Saturday'
|
42
|
+
|
43
|
+
Notice how a boolean is encoded as an empty value.
|
44
|
+
If it were true, it'd be encoded as 1:
|
45
|
+
|
46
|
+
.. code-block:: python
|
47
|
+
|
48
|
+
>>> data = dict(text_line=u'A', ascii_line='B', text=u'C\nD', ascii='E\nF', int=3, float=1.2, bool=True, weekday=u'Saturday')
|
49
|
+
>>> encode(data, ISimple)
|
50
|
+
'text_line=A&ascii_line=B&text%3Atext=C%0AD&ascii%3Atext=E%0AF&int%3Along=3&float%3Afloat=1.2&bool%3Aboolean=1&weekday=Saturday'
|
51
|
+
|
52
|
+
If the data dictionary has values not in the interface, they are ignored:
|
53
|
+
|
54
|
+
.. code-block:: python
|
55
|
+
|
56
|
+
>>> data = dict(text_line=u'A', ascii_line='B', text=u'C\nD', ascii='E\nF', int=3, float=1.2, bool=True, weekday=u'Saturday', foo=123)
|
57
|
+
>>> encode(data, ISimple)
|
58
|
+
'text_line=A&ascii_line=B&text%3Atext=C%0AD&ascii%3Atext=E%0AF&int%3Along=3&float%3Afloat=1.2&bool%3Aboolean=1&weekday=Saturday'
|
59
|
+
|
60
|
+
If the data dictionary omits some fields, they are ignored.
|
61
|
+
|
62
|
+
.. code-block:: python
|
63
|
+
|
64
|
+
>>> data = dict(text_line=u'A', ascii_line='B', text=u'C\nD', ascii='E\nF', float=1.2, bool=True, foo=123)
|
65
|
+
>>> encode(data, ISimple)
|
66
|
+
'text_line=A&ascii_line=B&text%3Atext=C%0AD&ascii%3Atext=E%0AF&float%3Afloat=1.2&bool%3Aboolean=1'
|
67
|
+
|
68
|
+
It is also possible to explicitly ignore some fields:
|
69
|
+
|
70
|
+
.. code-block:: python
|
71
|
+
|
72
|
+
>>> data = dict(text_line=u'A', ascii_line='B', text=u'C\nD', ascii='E\nF', float=1.2, bool=True, foo=123)
|
73
|
+
>>> encode(data, ISimple, ignore=('text_line', 'text',))
|
74
|
+
'ascii_line=B&ascii%3Atext=E%0AF&float%3Afloat=1.2&bool%3Aboolean=1'
|
75
|
+
|
76
|
+
Lists and tuples may also be encoded. The value type will be encoded as well.
|
77
|
+
|
78
|
+
.. code-block:: python
|
79
|
+
|
80
|
+
>>> class ISequences(Interface):
|
81
|
+
... list = schema.List(title=u'List', value_type=schema.ASCIILine(title=u'Text'))
|
82
|
+
... tuple = schema.Tuple(title=u'List', value_type=schema.Int(title=u'Int'))
|
83
|
+
|
84
|
+
>>> data = dict(list=['a', 'b'], tuple=(1,2,3))
|
85
|
+
>>> encode(data, ISequences)
|
86
|
+
'list%3Alist=a&list%3Alist=b&tuple%3Along%3Atuple=1&tuple%3Along%3Atuple=2&tuple%3Along%3Atuple=3'
|
87
|
+
|
88
|
+
Even dictionaries may be encoded. And the value type will be encoded as well.
|
89
|
+
|
90
|
+
.. code-block:: python
|
91
|
+
|
92
|
+
>>> class IRecords(Interface):
|
93
|
+
... record = schema.Dict(title=u'Record')
|
94
|
+
... records = schema.List(title=u'Records', value_type=schema.Dict())
|
95
|
+
|
96
|
+
>>> data = dict(record={'a': 'b', 'c': True}, records=[{'a': 'b', 'c': True}])
|
97
|
+
>>> encode(data, IRecords)
|
98
|
+
'record.a%3Arecord=b&record.c%3Aboolean%3Arecord=1&records.a%3Arecords=b&records.c%3Aboolean%3Arecords=1'
|
99
|
+
|
100
|
+
Unsupported fields will raise a ComponentLookupError.
|
101
|
+
This also applies to the value_type of a list or tuple:
|
102
|
+
|
103
|
+
.. code-block:: python
|
104
|
+
|
105
|
+
>>> class IUnsupported(Interface):
|
106
|
+
... decimal = schema.Decimal(title=u'Decimal')
|
107
|
+
... list = schema.List(title=u'Set', value_type=schema.Decimal(title=u'Decimal'))
|
108
|
+
... bytes_line = schema.BytesLine(title=u'Bytes line')
|
109
|
+
|
110
|
+
>>> from decimal import Decimal
|
111
|
+
>>> data = dict(decimal=Decimal(2), list=[Decimal(1), Decimal(2),], bytes_line='abc')
|
112
|
+
>>> encode(data, IUnsupported) # doctest: +ELLIPSIS
|
113
|
+
Traceback (most recent call last):
|
114
|
+
...
|
115
|
+
zope.interface.interfaces.ComponentLookupError: Cannot URL encode decimal of type <class 'zope.schema...Decimal'>
|
116
|
+
|
117
|
+
>>> encode(data, IUnsupported, ignore=('decimal',)) # doctest: +ELLIPSIS
|
118
|
+
Traceback (most recent call last):
|
119
|
+
...
|
120
|
+
zope.interface.interfaces.ComponentLookupError: Cannot URL encode value type for list of type <class 'zope.schema._field.List'> : <class 'zope.schema...Decimal'>
|
121
|
+
|
122
|
+
>>> encode(data, IUnsupported, ignore=('decimal', 'list',))
|
123
|
+
'bytes_line=abc'
|
124
|
+
|
125
|
+
Decoding
|
126
|
+
--------
|
127
|
+
|
128
|
+
The decoder exists because the Zope form marshalers are not perfect:
|
129
|
+
for instance, they cannot adequately deal with the differences between unicode and ASCII.
|
130
|
+
``zope.schema`` is picky about that sort of thing.
|
131
|
+
|
132
|
+
Let's use a data dictionary that may have come back from a query string like the first example above.
|
133
|
+
|
134
|
+
.. code-block:: python
|
135
|
+
|
136
|
+
>>> data = dict(text_line=u'A', ascii_line=u'B', text=u'C\nD', ascii=u'E\nF', int=3, float=1.2, bool=False, weekday=u'Saturday')
|
137
|
+
>>> sorted(decode(data, ISimple).items())
|
138
|
+
[('ascii', 'E\nF'), ('ascii_line', 'B'), ('bool', False), ('float', 1.2), ('int', 3), ('list', None), ('listchoice', None), ('text', 'C\nD'), ('text_line', 'A'), ('weekday', 'Saturday')]
|
139
|
+
|
140
|
+
If any values are missing from the input dictionary,
|
141
|
+
they will default to ``missing_value``.
|
142
|
+
|
143
|
+
.. code-block:: python
|
144
|
+
|
145
|
+
>>> data = dict(text_line=u'A', ascii_line=u'B', int=3, float=1.2, bool=False, weekday=u'Saturday')
|
146
|
+
>>> sorted(decode(data, ISimple).items())
|
147
|
+
[('ascii', None), ('ascii_line', 'B'), ('bool', False), ('float', 1.2), ('int', 3), ('list', None), ('listchoice', None), ('text', 'Missing'), ('text_line', 'A'), ('weekday', 'Saturday')]
|
148
|
+
|
149
|
+
If you pass ``missing=False``, the values are ignored instead.
|
150
|
+
|
151
|
+
.. code-block:: python
|
152
|
+
|
153
|
+
>>> data = dict(text_line=u'A', ascii_line=u'B', int=3, float=1.2, bool=False)
|
154
|
+
>>> sorted(decode(data, ISimple, missing=False).items())
|
155
|
+
[('ascii_line', 'B'), ('bool', False), ('float', 1.2), ('int', 3), ('text_line', 'A')]
|
156
|
+
|
157
|
+
Decoding also works for lists and their value types:
|
158
|
+
|
159
|
+
.. code-block:: python
|
160
|
+
|
161
|
+
>>> data = dict(list=[u'a', u'b'])
|
162
|
+
>>> sorted(decode(data, ISequences, missing=False).items())
|
163
|
+
[('list', ['a', 'b'])]
|
164
|
+
|
165
|
+
Decoding should work with lists and the ISimple schema
|
166
|
+
|
167
|
+
.. code-block:: python
|
168
|
+
|
169
|
+
>>> data = dict(list=['a', 'b'])
|
170
|
+
>>> sorted(decode(data, ISimple, missing=False).items())
|
171
|
+
[('list', ['a', 'b'])]
|
172
|
+
|
173
|
+
And list choice fields
|
174
|
+
|
175
|
+
.. code-block:: python
|
176
|
+
|
177
|
+
>>> data = dict(listchoice=['a', 'b'])
|
178
|
+
>>> sorted(decode(data, ISimple, missing=False).items())
|
179
|
+
[('listchoice', ['a', 'b'])]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from plone.supermodel.directives import MetadataListDirective
|
2
|
+
|
3
|
+
|
4
|
+
IGNORE_QUERYSTRING_KEY = "plone.tiles.ignore_querystring"
|
5
|
+
|
6
|
+
|
7
|
+
class ignore_querystring(MetadataListDirective):
|
8
|
+
"""Directive used to create fieldsets"""
|
9
|
+
|
10
|
+
key = IGNORE_QUERYSTRING_KEY
|
11
|
+
|
12
|
+
def factory(self, name):
|
13
|
+
return [name]
|