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