udata 10.0.1.dev32334__py2.py3-none-any.whl → 10.0.1.dev32350__py2.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.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api_fields.py +131 -70
- udata/static/chunks/{10.a99bb538cfbadb38dbcb.js → 10.dac55d18d0b4ef3cdacf.js} +3 -3
- udata/static/chunks/{10.a99bb538cfbadb38dbcb.js.map → 10.dac55d18d0b4ef3cdacf.js.map} +1 -1
- udata/static/chunks/{11.727465d72948bc466d43.js → 11.4a20a75f827c5a1125c3.js} +3 -3
- udata/static/chunks/{11.727465d72948bc466d43.js.map → 11.4a20a75f827c5a1125c3.js.map} +1 -1
- udata/static/chunks/{13.3c8337bec315adcd2700.js → 13.645dd0b7c0b9210f1b56.js} +2 -2
- udata/static/chunks/{13.3c8337bec315adcd2700.js.map → 13.645dd0b7c0b9210f1b56.js.map} +1 -1
- udata/static/chunks/{17.cbeb6d95129cad6481c2.js → 17.8e19985c4d12a3b7b0c0.js} +2 -2
- udata/static/chunks/{17.cbeb6d95129cad6481c2.js.map → 17.8e19985c4d12a3b7b0c0.js.map} +1 -1
- udata/static/chunks/{19.4f7a5b71ef006ac268c1.js → 19.825a43c330157e351fca.js} +3 -3
- udata/static/chunks/{19.4f7a5b71ef006ac268c1.js.map → 19.825a43c330157e351fca.js.map} +1 -1
- udata/static/chunks/{8.60610fd40b95ca119141.js → 8.5ee0cf635c848abbfc05.js} +2 -2
- udata/static/chunks/{8.60610fd40b95ca119141.js.map → 8.5ee0cf635c848abbfc05.js.map} +1 -1
- udata/static/chunks/{9.985935421e62c97a9f86.js → 9.df3c36f8d0d210621fbb.js} +3 -3
- udata/static/chunks/{9.985935421e62c97a9f86.js.map → 9.df3c36f8d0d210621fbb.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/test_api_fields.py +285 -0
- {udata-10.0.1.dev32334.dist-info → udata-10.0.1.dev32350.dist-info}/METADATA +2 -2
- {udata-10.0.1.dev32334.dist-info → udata-10.0.1.dev32350.dist-info}/RECORD +24 -23
- {udata-10.0.1.dev32334.dist-info → udata-10.0.1.dev32350.dist-info}/LICENSE +0 -0
- {udata-10.0.1.dev32334.dist-info → udata-10.0.1.dev32350.dist-info}/WHEEL +0 -0
- {udata-10.0.1.dev32334.dist-info → udata-10.0.1.dev32350.dist-info}/entry_points.txt +0 -0
- {udata-10.0.1.dev32334.dist-info → udata-10.0.1.dev32350.dist-info}/top_level.txt +0 -0
udata/api_fields.py
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
|
+
"""Enhance a MongoEngine document class to give it super powers by decorating it with @generate_fields.
|
|
2
|
+
|
|
3
|
+
The main goal of `generate_fields` is to remove duplication: we used to have fields declaration in
|
|
4
|
+
- models.py
|
|
5
|
+
- forms.py
|
|
6
|
+
- api_fields.py
|
|
7
|
+
|
|
8
|
+
Now they're defined in models.py, and adding the `generate_fields` decorator makes them available in the format we need them for the forms or the API.
|
|
9
|
+
|
|
10
|
+
- default_filterable_field: which field in this document should be the default filter, eg when filtering by Badge, you're actually filtering on `Badge.kind`
|
|
11
|
+
- searchable: boolean, if True, the document can be full-text searched using MongoEngine text search
|
|
12
|
+
- additional_sorts: add more sorts than the already available ones based on fields (see below). Eg, sort by metrics.
|
|
13
|
+
- additional_filters: filter on a field of a field (aka "join"), eg filter on `Reuse__organization__badge=PUBLIC_SERVICE`.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
On top of those functionalities added to the document by the `@generate_fields` decorator parameters,
|
|
17
|
+
the document fields are parsed and enhanced if they are wrapped in the `field` helper.
|
|
18
|
+
|
|
19
|
+
- sortable: boolean, if True, it'll be available in the list of sort options
|
|
20
|
+
- show_as_ref: add to the list of `ref_fields` (see below)
|
|
21
|
+
- readonly: don't add this field to the `write_fields`
|
|
22
|
+
- markdown: use Mardown to format this field instead of plain old text
|
|
23
|
+
- filterable: this field can be filtered on. It's either an empty dictionnary, either {`key`: `field_name`} if the `field_name` to use is different from the original field, eg `dataset` instead of `datasets`.
|
|
24
|
+
- description: use as the info on the field in the swagger forms.
|
|
25
|
+
- check: provide a function to validate the content of the field.
|
|
26
|
+
- thumbnail_info: add additional info for a thumbnail, eg `{ "size": BIGGEST_IMAGE_SIZE }`.
|
|
27
|
+
|
|
28
|
+
You may also use the `@function_field` decorator to treat a document method as a field.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
The following fields are added on the document class once decorated:
|
|
32
|
+
|
|
33
|
+
- ref_fields: list of fields to return when embedding/referencing a document, eg when querying Reuse.organization, only return a subset of the org fields
|
|
34
|
+
- read_fields: all of the fields to return when querying a document
|
|
35
|
+
- write_fields: list of fields to provide when creating a document, eg when creating a Reuse, we only provide organization IDs, not all the org fields
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
1
39
|
import functools
|
|
2
|
-
from typing import Any,
|
|
40
|
+
from typing import Any, Callable, Iterable
|
|
3
41
|
|
|
4
42
|
import flask_restx.fields as restx_fields
|
|
5
43
|
import mongoengine
|
|
6
44
|
import mongoengine.fields as mongo_fields
|
|
7
45
|
from bson import ObjectId
|
|
8
46
|
from flask_restx.inputs import boolean
|
|
47
|
+
from flask_restx.reqparse import RequestParser
|
|
9
48
|
from flask_storage.mongo import ImageField as FlaskStorageImageField
|
|
10
49
|
|
|
11
50
|
import udata.api.fields as custom_restx_fields
|
|
12
51
|
from udata.api import api, base_reference
|
|
13
52
|
from udata.mongo.errors import FieldValidationError
|
|
53
|
+
from udata.mongo.queryset import DBPaginator, UDataQuerySet
|
|
14
54
|
|
|
15
55
|
lazy_reference = api.model(
|
|
16
56
|
"LazyReference",
|
|
@@ -21,26 +61,27 @@ lazy_reference = api.model(
|
|
|
21
61
|
)
|
|
22
62
|
|
|
23
63
|
|
|
24
|
-
def convert_db_to_field(key, field, info):
|
|
25
|
-
"""
|
|
26
|
-
|
|
64
|
+
def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | None]:
|
|
65
|
+
"""Map a Mongo field to a Flask RestX field.
|
|
66
|
+
|
|
27
67
|
Most of the types are a simple 1-to-1 mapping except lists and references that requires
|
|
28
68
|
more work.
|
|
29
69
|
We currently only map the params that we use from Mongo to RestX (for example min_length / max_length…).
|
|
30
70
|
|
|
31
71
|
In the first part of the function we save the RestX constructor as a lambda because we need to call it with the
|
|
32
72
|
params. Since merging the params involve a litte bit of work (merging default params with read/write params and then with
|
|
33
|
-
user-supplied overrides, setting the readonly flag…), it's easier to have do this
|
|
73
|
+
user-supplied overrides, setting the readonly flag…), it's easier to have to do this only once at the end of the function.
|
|
74
|
+
|
|
34
75
|
"""
|
|
35
|
-
params = {}
|
|
76
|
+
params: dict = {}
|
|
36
77
|
params["required"] = field.required
|
|
37
78
|
|
|
38
|
-
read_params = {}
|
|
39
|
-
write_params = {}
|
|
79
|
+
read_params: dict = {}
|
|
80
|
+
write_params: dict = {}
|
|
40
81
|
|
|
41
|
-
constructor
|
|
42
|
-
constructor_read = None
|
|
43
|
-
constructor_write = None
|
|
82
|
+
constructor: Callable
|
|
83
|
+
constructor_read: Callable | None = None
|
|
84
|
+
constructor_write: Callable | None = None
|
|
44
85
|
|
|
45
86
|
if info.get("convert_to"):
|
|
46
87
|
# TODO: this is currently never used. We may remove it if the auto-conversion
|
|
@@ -66,7 +107,7 @@ def convert_db_to_field(key, field, info):
|
|
|
66
107
|
elif isinstance(field, mongo_fields.DictField):
|
|
67
108
|
constructor = restx_fields.Raw
|
|
68
109
|
elif isinstance(field, mongo_fields.ImageField) or isinstance(field, FlaskStorageImageField):
|
|
69
|
-
size = info.get("size", None)
|
|
110
|
+
size: int | None = info.get("size", None)
|
|
70
111
|
if size:
|
|
71
112
|
params["description"] = f"URL of the cropped and squared image ({size}x{size})"
|
|
72
113
|
else:
|
|
@@ -103,7 +144,7 @@ def convert_db_to_field(key, field, info):
|
|
|
103
144
|
# 1. `inner_field_info` inside `__additional_field_info__` on the parent
|
|
104
145
|
# 2. `__additional_field_info__` of the inner field
|
|
105
146
|
# 3. `__additional_field_info__` of the parent
|
|
106
|
-
inner_info = getattr(field.field, "__additional_field_info__", {})
|
|
147
|
+
inner_info: dict = getattr(field.field, "__additional_field_info__", {})
|
|
107
148
|
field_read, field_write = convert_db_to_field(
|
|
108
149
|
f"{key}.inner", field.field, {**info, **inner_info, **info.get("inner_field_info", {})}
|
|
109
150
|
)
|
|
@@ -130,7 +171,7 @@ def convert_db_to_field(key, field, info):
|
|
|
130
171
|
# For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
|
|
131
172
|
# the referenced model, if not we return a String (and RestX will call the `str()` of the model
|
|
132
173
|
# when returning from an endpoint)
|
|
133
|
-
nested_fields = info.get("nested_fields")
|
|
174
|
+
nested_fields: dict | None = info.get("nested_fields")
|
|
134
175
|
if nested_fields is None:
|
|
135
176
|
# If there is no `nested_fields` convert the object to the string representation.
|
|
136
177
|
constructor_read = restx_fields.String
|
|
@@ -167,9 +208,11 @@ def convert_db_to_field(key, field, info):
|
|
|
167
208
|
read_params = {**params, **read_params, **info}
|
|
168
209
|
write_params = {**params, **write_params, **info}
|
|
169
210
|
|
|
170
|
-
read =
|
|
211
|
+
read: Callable = (
|
|
212
|
+
constructor_read(**read_params) if constructor_read else constructor(**read_params)
|
|
213
|
+
)
|
|
171
214
|
if write_params.get("readonly", False) or (constructor_write is None and constructor is None):
|
|
172
|
-
write = None
|
|
215
|
+
write: Callable | None = None
|
|
173
216
|
else:
|
|
174
217
|
write = (
|
|
175
218
|
constructor_write(**write_params) if constructor_write else constructor(**write_params)
|
|
@@ -177,10 +220,11 @@ def convert_db_to_field(key, field, info):
|
|
|
177
220
|
return read, write
|
|
178
221
|
|
|
179
222
|
|
|
180
|
-
def get_fields(cls):
|
|
181
|
-
"""
|
|
182
|
-
|
|
183
|
-
|
|
223
|
+
def get_fields(cls) -> Iterable[tuple[str, Callable, dict]]:
|
|
224
|
+
"""Return all the document fields that are wrapped with the `field()` helper.
|
|
225
|
+
|
|
226
|
+
Also expand image fields to add thumbnail fields.
|
|
227
|
+
|
|
184
228
|
"""
|
|
185
229
|
for key, field in cls._fields.items():
|
|
186
230
|
info: dict | None = getattr(field, "__additional_field_info__", None)
|
|
@@ -197,27 +241,35 @@ def get_fields(cls):
|
|
|
197
241
|
)
|
|
198
242
|
|
|
199
243
|
|
|
200
|
-
def generate_fields(**kwargs):
|
|
201
|
-
"""
|
|
244
|
+
def generate_fields(**kwargs) -> Callable:
|
|
245
|
+
"""Mongoengine document decorator.
|
|
246
|
+
|
|
202
247
|
This decorator will create two auto-generated attributes on the class `__read_fields__` and `__write_fields__`
|
|
203
248
|
that can be used in API endpoint inside `expect()` and `marshall_with()`.
|
|
249
|
+
|
|
250
|
+
It will also
|
|
251
|
+
- generate an API parameter parser
|
|
252
|
+
- sort and filter a list of documents with the provided params using the `apply_sort_filters_and_pagination` helper
|
|
253
|
+
|
|
204
254
|
"""
|
|
205
255
|
|
|
206
|
-
def wrapper(cls):
|
|
207
|
-
|
|
208
|
-
write_fields = {}
|
|
209
|
-
ref_fields = {}
|
|
210
|
-
sortables = kwargs.get("additional_sorts", [])
|
|
256
|
+
def wrapper(cls) -> Callable:
|
|
257
|
+
from udata.models import db
|
|
211
258
|
|
|
212
|
-
|
|
213
|
-
|
|
259
|
+
read_fields: dict = {}
|
|
260
|
+
write_fields: dict = {}
|
|
261
|
+
ref_fields: dict = {}
|
|
262
|
+
sortables: list = kwargs.get("additional_sorts", [])
|
|
263
|
+
|
|
264
|
+
filterables: list[dict] = []
|
|
265
|
+
additional_filters: dict[str, dict] = get_fields_with_additional_filters(
|
|
214
266
|
kwargs.get("additional_filters", {})
|
|
215
267
|
)
|
|
216
268
|
|
|
217
269
|
read_fields["id"] = restx_fields.String(required=True, readonly=True)
|
|
218
270
|
|
|
219
271
|
for key, field, info in get_fields(cls):
|
|
220
|
-
sortable_key = info.get("sortable", False)
|
|
272
|
+
sortable_key: bool = info.get("sortable", False)
|
|
221
273
|
if sortable_key:
|
|
222
274
|
sortables.append(
|
|
223
275
|
{
|
|
@@ -226,41 +278,41 @@ def generate_fields(**kwargs):
|
|
|
226
278
|
}
|
|
227
279
|
)
|
|
228
280
|
|
|
229
|
-
filterable = info.get("filterable", None)
|
|
230
|
-
|
|
281
|
+
filterable: dict[str, Any] | None = info.get("filterable", None)
|
|
231
282
|
if filterable is not None:
|
|
232
283
|
filterables.append(compute_filter(key, field, info, filterable))
|
|
233
284
|
|
|
234
|
-
additional_filter = additional_filters.get(key, None)
|
|
285
|
+
additional_filter: dict | None = additional_filters.get(key, None)
|
|
235
286
|
if additional_filter:
|
|
236
287
|
if not isinstance(
|
|
237
288
|
field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField
|
|
238
289
|
):
|
|
239
290
|
raise Exception("Cannot use additional_filters on not a ref.")
|
|
240
291
|
|
|
241
|
-
ref_model = field.document_type
|
|
292
|
+
ref_model: db.Document = field.document_type
|
|
242
293
|
|
|
243
294
|
for child in additional_filter.get("children", []):
|
|
244
|
-
inner_field = getattr(ref_model, child["key"])
|
|
295
|
+
inner_field: str = getattr(ref_model, child["key"])
|
|
245
296
|
|
|
246
|
-
column = f"{key}__{child['key']}"
|
|
297
|
+
column: str = f"{key}__{child['key']}"
|
|
247
298
|
child["key"] = f"{key}_{child['key']}"
|
|
248
299
|
filterable = compute_filter(column, inner_field, info, child)
|
|
249
300
|
|
|
250
301
|
# Since MongoDB is not capable of doing joins with a column like `organization__slug` we need to
|
|
251
302
|
# do a custom filter by splitting the query in two.
|
|
252
303
|
|
|
253
|
-
def query(filterable, query, value):
|
|
304
|
+
def query(filterable, query, value) -> UDataQuerySet:
|
|
254
305
|
# We use the computed `filterable["column"]` here because the `compute_filter` function
|
|
255
306
|
# could have added a default filter at the end (for example `organization__badges` converted
|
|
256
307
|
# in `organization__badges__kind`)
|
|
257
308
|
parts = filterable["column"].split("__", 1)
|
|
258
|
-
models = ref_model.objects.filter(**{parts[1]: value}).only(
|
|
309
|
+
models: UDataQuerySet = ref_model.objects.filter(**{parts[1]: value}).only(
|
|
310
|
+
"id"
|
|
311
|
+
)
|
|
259
312
|
return query.filter(**{f"{parts[0]}__in": models})
|
|
260
313
|
|
|
261
314
|
# do a query-based filter instead of a column based one
|
|
262
315
|
filterable["query"] = functools.partial(query, filterable)
|
|
263
|
-
|
|
264
316
|
filterables.append(filterable)
|
|
265
317
|
|
|
266
318
|
read, write = convert_db_to_field(key, field, info)
|
|
@@ -289,8 +341,8 @@ def generate_fields(**kwargs):
|
|
|
289
341
|
if not callable(method):
|
|
290
342
|
continue
|
|
291
343
|
|
|
292
|
-
|
|
293
|
-
if
|
|
344
|
+
additional_field_info = getattr(method, "__additional_field_info__", None)
|
|
345
|
+
if additional_field_info is None:
|
|
294
346
|
continue
|
|
295
347
|
|
|
296
348
|
def make_lambda(method):
|
|
@@ -302,16 +354,16 @@ def generate_fields(**kwargs):
|
|
|
302
354
|
return lambda o: method(o)
|
|
303
355
|
|
|
304
356
|
read_fields[method_name] = restx_fields.String(
|
|
305
|
-
attribute=make_lambda(method), **{"readonly": True, **
|
|
357
|
+
attribute=make_lambda(method), **{"readonly": True, **additional_field_info}
|
|
306
358
|
)
|
|
307
|
-
if
|
|
359
|
+
if additional_field_info.get("show_as_ref", False):
|
|
308
360
|
ref_fields[key] = read_fields[method_name]
|
|
309
361
|
|
|
310
362
|
cls.__read_fields__ = api.model(f"{cls.__name__} (read)", read_fields, **kwargs)
|
|
311
363
|
cls.__write_fields__ = api.model(f"{cls.__name__} (write)", write_fields, **kwargs)
|
|
312
364
|
cls.__ref_fields__ = api.inherit(f"{cls.__name__}Reference", base_reference, ref_fields)
|
|
313
365
|
|
|
314
|
-
mask = kwargs.pop("mask", None)
|
|
366
|
+
mask: str | None = kwargs.pop("mask", None)
|
|
315
367
|
if mask is not None:
|
|
316
368
|
mask = "data{{{0}}},*".format(mask)
|
|
317
369
|
cls.__page_fields__ = api.model(
|
|
@@ -322,8 +374,8 @@ def generate_fields(**kwargs):
|
|
|
322
374
|
)
|
|
323
375
|
|
|
324
376
|
# Parser for index sort/filters
|
|
325
|
-
paginable = kwargs.get("paginable", True)
|
|
326
|
-
parser = api.parser()
|
|
377
|
+
paginable: bool = kwargs.get("paginable", True)
|
|
378
|
+
parser: RequestParser = api.parser()
|
|
327
379
|
|
|
328
380
|
if paginable:
|
|
329
381
|
parser.add_argument(
|
|
@@ -334,7 +386,7 @@ def generate_fields(**kwargs):
|
|
|
334
386
|
)
|
|
335
387
|
|
|
336
388
|
if sortables:
|
|
337
|
-
choices = [sortable["key"] for sortable in sortables] + [
|
|
389
|
+
choices: list[str] = [sortable["key"] for sortable in sortables] + [
|
|
338
390
|
"-" + sortable["key"] for sortable in sortables
|
|
339
391
|
]
|
|
340
392
|
parser.add_argument(
|
|
@@ -345,12 +397,13 @@ def generate_fields(**kwargs):
|
|
|
345
397
|
help="The field (and direction) on which sorting apply",
|
|
346
398
|
)
|
|
347
399
|
|
|
348
|
-
searchable = kwargs.pop("searchable", False)
|
|
400
|
+
searchable: bool = kwargs.pop("searchable", False)
|
|
349
401
|
if searchable:
|
|
350
402
|
parser.add_argument("q", type=str, location="args")
|
|
351
403
|
|
|
352
404
|
for filterable in filterables:
|
|
353
405
|
parser.add_argument(
|
|
406
|
+
# Use the custom label from `additional_filters` if there's one.
|
|
354
407
|
filterable.get("label", filterable["key"]),
|
|
355
408
|
type=filterable["type"],
|
|
356
409
|
location="args",
|
|
@@ -359,18 +412,17 @@ def generate_fields(**kwargs):
|
|
|
359
412
|
|
|
360
413
|
cls.__index_parser__ = parser
|
|
361
414
|
|
|
362
|
-
def apply_sort_filters_and_pagination(base_query):
|
|
415
|
+
def apply_sort_filters_and_pagination(base_query) -> DBPaginator:
|
|
363
416
|
args = cls.__index_parser__.parse_args()
|
|
364
417
|
|
|
365
418
|
if sortables and args["sort"]:
|
|
366
|
-
negate = args["sort"].startswith("-")
|
|
367
|
-
sort_key = args["sort"][1:] if negate else args["sort"]
|
|
419
|
+
negate: bool = args["sort"].startswith("-")
|
|
420
|
+
sort_key: str = args["sort"][1:] if negate else args["sort"]
|
|
368
421
|
|
|
369
|
-
sort_by = next(
|
|
422
|
+
sort_by: str | None = next(
|
|
370
423
|
(sortable["value"] for sortable in sortables if sortable["key"] == sort_key),
|
|
371
424
|
None,
|
|
372
425
|
)
|
|
373
|
-
|
|
374
426
|
if sort_by:
|
|
375
427
|
if negate:
|
|
376
428
|
sort_by = "-" + sort_by
|
|
@@ -378,10 +430,13 @@ def generate_fields(**kwargs):
|
|
|
378
430
|
base_query = base_query.order_by(sort_by)
|
|
379
431
|
|
|
380
432
|
if searchable and args.get("q"):
|
|
381
|
-
phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
433
|
+
phrase_query: str = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
382
434
|
base_query = base_query.search_text(phrase_query)
|
|
383
435
|
|
|
384
436
|
for filterable in filterables:
|
|
437
|
+
# If it's from an `additional_filter`, use the custom label instead of the key,
|
|
438
|
+
# eg use `organization_badge` instead of `organization.badges` which is
|
|
439
|
+
# computed to `organization_badges`.
|
|
385
440
|
filter = args.get(filterable.get("label", filterable["key"]))
|
|
386
441
|
if filter is not None:
|
|
387
442
|
for constraint in filterable.get("constraints", []):
|
|
@@ -412,7 +467,7 @@ def generate_fields(**kwargs):
|
|
|
412
467
|
return wrapper
|
|
413
468
|
|
|
414
469
|
|
|
415
|
-
def function_field(**info):
|
|
470
|
+
def function_field(**info) -> Callable:
|
|
416
471
|
def inner(func):
|
|
417
472
|
func.__additional_field_info__ = info
|
|
418
473
|
return func
|
|
@@ -421,18 +476,20 @@ def function_field(**info):
|
|
|
421
476
|
|
|
422
477
|
|
|
423
478
|
def field(inner, **kwargs):
|
|
424
|
-
"""
|
|
425
|
-
|
|
426
|
-
We can pass additional arguments that will be
|
|
479
|
+
"""Simple wrapper to make a document field visible for the API.
|
|
480
|
+
|
|
481
|
+
We can pass additional arguments that will be forwarded to the RestX field constructor.
|
|
482
|
+
|
|
427
483
|
"""
|
|
428
484
|
inner.__additional_field_info__ = kwargs
|
|
429
485
|
return inner
|
|
430
486
|
|
|
431
487
|
|
|
432
|
-
def patch(obj, request):
|
|
433
|
-
"""
|
|
434
|
-
|
|
488
|
+
def patch(obj, request) -> type:
|
|
489
|
+
"""Patch the object with the data from the request.
|
|
490
|
+
|
|
435
491
|
Only fields decorated with the `field()` decorator will be read (and not readonly).
|
|
492
|
+
|
|
436
493
|
"""
|
|
437
494
|
from udata.mongo.engine import db
|
|
438
495
|
|
|
@@ -480,7 +537,7 @@ def patch(obj, request):
|
|
|
480
537
|
return obj
|
|
481
538
|
|
|
482
539
|
|
|
483
|
-
def patch_and_save(obj, request):
|
|
540
|
+
def patch_and_save(obj, request) -> type:
|
|
484
541
|
obj = patch(obj, request)
|
|
485
542
|
|
|
486
543
|
try:
|
|
@@ -497,11 +554,12 @@ def wrap_primary_key(
|
|
|
497
554
|
value: str | None,
|
|
498
555
|
document_type=None,
|
|
499
556
|
):
|
|
500
|
-
"""
|
|
501
|
-
|
|
502
|
-
a `DBRef` from the database.
|
|
557
|
+
"""Wrap the `String` inside an `ObjectId`.
|
|
558
|
+
|
|
559
|
+
If the foreign ID is a `String`, get a `DBRef` from the database.
|
|
503
560
|
|
|
504
561
|
TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
|
|
562
|
+
|
|
505
563
|
"""
|
|
506
564
|
if value is None:
|
|
507
565
|
return value
|
|
@@ -547,13 +605,15 @@ def wrap_primary_key(
|
|
|
547
605
|
)
|
|
548
606
|
|
|
549
607
|
|
|
550
|
-
def get_fields_with_additional_filters(additional_filters:
|
|
551
|
-
"""
|
|
552
|
-
Right now we only support additional filters like "organization.badges".
|
|
608
|
+
def get_fields_with_additional_filters(additional_filters: dict[str, str]) -> dict[str, Any]:
|
|
609
|
+
"""Filter on additional related fields.
|
|
553
610
|
|
|
554
|
-
|
|
611
|
+
Right now we only support additional filters with a depth of two, eg "organization.badges".
|
|
612
|
+
|
|
613
|
+
The goal of this function is to key by the additional filters by the first part (`organization`) to
|
|
555
614
|
be able to compute them when we loop over all the fields (`title`, `organization`…)
|
|
556
615
|
|
|
616
|
+
|
|
557
617
|
The `additional_filters` property is a dict: {"label": "key"}, for example {"organization_badge": "organization.badges"}.
|
|
558
618
|
The `label` will be the name of the parser arg, like `?organization_badge=public-service`, which makes more
|
|
559
619
|
sense than `?organization_badges=public-service`.
|
|
@@ -570,6 +630,7 @@ def get_fields_with_additional_filters(additional_filters: Dict[str, str]) -> Di
|
|
|
570
630
|
|
|
571
631
|
results[parent]["children"].append(
|
|
572
632
|
{
|
|
633
|
+
# The name for the parser argument, so `organization_badge` instead of `organization_badges`.
|
|
573
634
|
"label": label,
|
|
574
635
|
"key": child,
|
|
575
636
|
"type": str,
|
|
@@ -581,7 +642,7 @@ def get_fields_with_additional_filters(additional_filters: Dict[str, str]) -> Di
|
|
|
581
642
|
return results
|
|
582
643
|
|
|
583
644
|
|
|
584
|
-
def compute_filter(column: str, field, info, filterable):
|
|
645
|
+
def compute_filter(column: str, field, info, filterable) -> dict:
|
|
585
646
|
# "key" is the param key in the URL
|
|
586
647
|
if "key" not in filterable:
|
|
587
648
|
filterable["key"] = column
|