udata 9.2.5.dev32170__py2.py3-none-any.whl → 9.2.5.dev32222__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/__init__.py +1 -0
- udata/api_fields.py +143 -37
- udata/core/badges/factories.py +3 -5
- udata/core/badges/forms.py +0 -3
- udata/core/badges/models.py +19 -24
- udata/core/badges/tests/test_commands.py +3 -4
- udata/core/badges/tests/test_model.py +37 -28
- udata/core/dataservices/api.py +1 -1
- udata/core/dataservices/apiv2.py +29 -0
- udata/core/dataservices/models.py +4 -1
- udata/core/dataservices/search.py +118 -0
- udata/core/dataset/api.py +10 -0
- udata/core/dataset/models.py +16 -6
- udata/core/dataset/search.py +5 -4
- udata/core/organization/api.py +11 -0
- udata/core/organization/models.py +23 -10
- udata/core/organization/search.py +8 -5
- udata/core/reuse/api.py +10 -0
- udata/core/reuse/models.py +15 -5
- udata/core/reuse/search.py +5 -4
- udata/search/fields.py +8 -3
- udata/search/query.py +4 -2
- udata/static/chunks/{10.dac55d18d0b4ef3cdacf.js → 10.a99bb538cfbadb38dbcb.js} +3 -3
- udata/static/chunks/{10.dac55d18d0b4ef3cdacf.js.map → 10.a99bb538cfbadb38dbcb.js.map} +1 -1
- udata/static/chunks/{11.4a20a75f827c5a1125c3.js → 11.bb1c1fb39f740fbbeec0.js} +3 -3
- udata/static/chunks/{11.4a20a75f827c5a1125c3.js.map → 11.bb1c1fb39f740fbbeec0.js.map} +1 -1
- udata/static/chunks/{13.645dd0b7c0b9210f1b56.js → 13.bef5fdb3e147e94fea99.js} +2 -2
- udata/static/chunks/{13.645dd0b7c0b9210f1b56.js.map → 13.bef5fdb3e147e94fea99.js.map} +1 -1
- udata/static/chunks/{17.8e19985c4d12a3b7b0c0.js → 17.b91d28f550dc44bc4979.js} +2 -2
- udata/static/chunks/{17.8e19985c4d12a3b7b0c0.js.map → 17.b91d28f550dc44bc4979.js.map} +1 -1
- udata/static/chunks/{19.825a43c330157e351fca.js → 19.2c615ffee1e807000770.js} +3 -3
- udata/static/chunks/{19.825a43c330157e351fca.js.map → 19.2c615ffee1e807000770.js.map} +1 -1
- udata/static/chunks/{8.5ee0cf635c848abbfc05.js → 8.291bde987ed97294e4de.js} +2 -2
- udata/static/chunks/{8.5ee0cf635c848abbfc05.js.map → 8.291bde987ed97294e4de.js.map} +1 -1
- udata/static/chunks/{9.df3c36f8d0d210621fbb.js → 9.985935421e62c97a9f86.js} +3 -3
- udata/static/chunks/{9.df3c36f8d0d210621fbb.js.map → 9.985935421e62c97a9f86.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_dataservices_api.py +21 -1
- udata/tests/api/test_datasets_api.py +15 -0
- udata/tests/api/test_organizations_api.py +15 -4
- udata/tests/api/test_reuses_api.py +15 -0
- udata/tests/apiv2/test_datasets.py +17 -0
- udata/tests/organization/test_organization_model.py +21 -0
- udata/tests/site/test_site_metrics.py +1 -3
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/METADATA +4 -1
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/RECORD +51 -49
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/LICENSE +0 -0
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/WHEEL +0 -0
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/entry_points.txt +0 -0
- {udata-9.2.5.dev32170.dist-info → udata-9.2.5.dev32222.dist-info}/top_level.txt +0 -0
udata/api/__init__.py
CHANGED
|
@@ -303,6 +303,7 @@ def init_app(app):
|
|
|
303
303
|
import udata.core.dataset.api # noqa
|
|
304
304
|
import udata.core.dataset.apiv2 # noqa
|
|
305
305
|
import udata.core.dataservices.api # noqa
|
|
306
|
+
import udata.core.dataservices.apiv2 # noqa
|
|
306
307
|
import udata.core.discussions.api # noqa
|
|
307
308
|
import udata.core.reuse.api # noqa
|
|
308
309
|
import udata.core.reuse.apiv2 # noqa
|
udata/api_fields.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
1
4
|
import flask_restx.fields as restx_fields
|
|
2
5
|
import mongoengine
|
|
3
6
|
import mongoengine.fields as mongo_fields
|
|
@@ -114,13 +117,15 @@ def convert_db_to_field(key, field, info):
|
|
|
114
117
|
# But we want to keep the `constructor_write` to allow changing the list.
|
|
115
118
|
def constructor_write(**kwargs):
|
|
116
119
|
return restx_fields.List(field_write, **kwargs)
|
|
120
|
+
|
|
117
121
|
elif isinstance(
|
|
118
122
|
field, (mongo_fields.GenericReferenceField, mongoengine.fields.GenericLazyReferenceField)
|
|
119
123
|
):
|
|
120
124
|
|
|
121
125
|
def constructor(**kwargs):
|
|
122
126
|
return restx_fields.Nested(lazy_reference, **kwargs)
|
|
123
|
-
|
|
127
|
+
|
|
128
|
+
elif isinstance(field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField):
|
|
124
129
|
# For reference we accept while writing a String representing the ID of the referenced model.
|
|
125
130
|
# For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
|
|
126
131
|
# the referenced model, if not we return a String (and RestX will call the `str()` of the model
|
|
@@ -142,6 +147,7 @@ def convert_db_to_field(key, field, info):
|
|
|
142
147
|
|
|
143
148
|
def constructor(**kwargs):
|
|
144
149
|
return restx_fields.Nested(nested_fields, **kwargs)
|
|
150
|
+
|
|
145
151
|
elif hasattr(field.document_type_obj, "__read_fields__"):
|
|
146
152
|
|
|
147
153
|
def constructor_read(**kwargs):
|
|
@@ -149,6 +155,7 @@ def convert_db_to_field(key, field, info):
|
|
|
149
155
|
|
|
150
156
|
def constructor_write(**kwargs):
|
|
151
157
|
return restx_fields.Nested(field.document_type_obj.__write_fields__, **kwargs)
|
|
158
|
+
|
|
152
159
|
else:
|
|
153
160
|
raise ValueError(
|
|
154
161
|
f"EmbeddedDocumentField `{key}` requires a `nested_fields` param to serialize/deserialize or a `@generate_fields()` definition."
|
|
@@ -200,8 +207,12 @@ def generate_fields(**kwargs):
|
|
|
200
207
|
read_fields = {}
|
|
201
208
|
write_fields = {}
|
|
202
209
|
ref_fields = {}
|
|
203
|
-
sortables = kwargs.get("
|
|
210
|
+
sortables = kwargs.get("additional_sorts", [])
|
|
211
|
+
|
|
204
212
|
filterables = []
|
|
213
|
+
additional_filters = get_fields_with_additional_filters(
|
|
214
|
+
kwargs.get("additional_filters", {})
|
|
215
|
+
)
|
|
205
216
|
|
|
206
217
|
read_fields["id"] = restx_fields.String(required=True, readonly=True)
|
|
207
218
|
|
|
@@ -209,38 +220,48 @@ def generate_fields(**kwargs):
|
|
|
209
220
|
sortable_key = info.get("sortable", False)
|
|
210
221
|
if sortable_key:
|
|
211
222
|
sortables.append(
|
|
212
|
-
{
|
|
223
|
+
{
|
|
224
|
+
"key": sortable_key if isinstance(sortable_key, str) else key,
|
|
225
|
+
"value": key,
|
|
226
|
+
}
|
|
213
227
|
)
|
|
214
228
|
|
|
215
229
|
filterable = info.get("filterable", None)
|
|
230
|
+
|
|
216
231
|
if filterable is not None:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
(mongo_fields.ReferenceField, mongo_fields.LazyReferenceField),
|
|
231
|
-
)
|
|
232
|
-
):
|
|
233
|
-
filterable["constraints"].append("objectid")
|
|
232
|
+
filterables.append(compute_filter(key, field, info, filterable))
|
|
233
|
+
|
|
234
|
+
additional_filter = additional_filters.get(key, None)
|
|
235
|
+
if additional_filter:
|
|
236
|
+
if not isinstance(
|
|
237
|
+
field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField
|
|
238
|
+
):
|
|
239
|
+
raise Exception("Cannot use additional_filters on not a ref.")
|
|
240
|
+
|
|
241
|
+
ref_model = field.document_type
|
|
242
|
+
|
|
243
|
+
for child in additional_filter.get("children", []):
|
|
244
|
+
inner_field = getattr(ref_model, child["key"])
|
|
234
245
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
filterable["type"] = boolean
|
|
246
|
+
column = f"{key}__{child['key']}"
|
|
247
|
+
child["key"] = f"{key}_{child['key']}"
|
|
248
|
+
filterable = compute_filter(column, inner_field, info, child)
|
|
239
249
|
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
# Since MongoDB is not capable of doing joins with a column like `organization__slug` we need to
|
|
251
|
+
# do a custom filter by splitting the query in two.
|
|
242
252
|
|
|
243
|
-
|
|
253
|
+
def query(filterable, query, value):
|
|
254
|
+
# We use the computed `filterable["column"]` here because the `compute_filter` function
|
|
255
|
+
# could have added a default filter at the end (for example `organization__badges` converted
|
|
256
|
+
# in `organization__badges__kind`)
|
|
257
|
+
parts = filterable["column"].split("__", 1)
|
|
258
|
+
models = ref_model.objects.filter(**{parts[1]: value}).only("id")
|
|
259
|
+
return query.filter(**{f"{parts[0]}__in": models})
|
|
260
|
+
|
|
261
|
+
# do a query-based filter instead of a column based one
|
|
262
|
+
filterable["query"] = functools.partial(query, filterable)
|
|
263
|
+
|
|
264
|
+
filterables.append(filterable)
|
|
244
265
|
|
|
245
266
|
read, write = convert_db_to_field(key, field, info)
|
|
246
267
|
|
|
@@ -330,9 +351,10 @@ def generate_fields(**kwargs):
|
|
|
330
351
|
|
|
331
352
|
for filterable in filterables:
|
|
332
353
|
parser.add_argument(
|
|
333
|
-
filterable["key"],
|
|
354
|
+
filterable.get("label", filterable["key"]),
|
|
334
355
|
type=filterable["type"],
|
|
335
356
|
location="args",
|
|
357
|
+
choices=filterable.get("choices", None),
|
|
336
358
|
)
|
|
337
359
|
|
|
338
360
|
cls.__index_parser__ = parser
|
|
@@ -360,18 +382,23 @@ def generate_fields(**kwargs):
|
|
|
360
382
|
base_query = base_query.search_text(phrase_query)
|
|
361
383
|
|
|
362
384
|
for filterable in filterables:
|
|
363
|
-
|
|
364
|
-
|
|
385
|
+
filter = args.get(filterable.get("label", filterable["key"]))
|
|
386
|
+
if filter is not None:
|
|
387
|
+
for constraint in filterable.get("constraints", []):
|
|
365
388
|
if constraint == "objectid" and not ObjectId.is_valid(
|
|
366
389
|
args[filterable["key"]]
|
|
367
390
|
):
|
|
368
391
|
api.abort(400, f'`{filterable["key"]}` must be an identifier')
|
|
369
392
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
query = filterable.get("query", None)
|
|
394
|
+
if query:
|
|
395
|
+
base_query = filterable["query"](base_query, filter)
|
|
396
|
+
else:
|
|
397
|
+
base_query = base_query.filter(
|
|
398
|
+
**{
|
|
399
|
+
filterable["column"]: filter,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
375
402
|
|
|
376
403
|
if paginable:
|
|
377
404
|
base_query = base_query.paginate(args["page"], args["page_size"])
|
|
@@ -379,6 +406,7 @@ def generate_fields(**kwargs):
|
|
|
379
406
|
return base_query
|
|
380
407
|
|
|
381
408
|
cls.apply_sort_filters_and_pagination = apply_sort_filters_and_pagination
|
|
409
|
+
cls.__additional_class_info__ = kwargs
|
|
382
410
|
return cls
|
|
383
411
|
|
|
384
412
|
return wrapper
|
|
@@ -417,12 +445,12 @@ def patch(obj, request):
|
|
|
417
445
|
value = model_attribute.from_input(value)
|
|
418
446
|
elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
419
447
|
model_attribute.field,
|
|
420
|
-
|
|
448
|
+
mongo_fields.ReferenceField | mongo_fields.LazyReferenceField,
|
|
421
449
|
):
|
|
422
450
|
# TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
|
|
423
451
|
value = [wrap_primary_key(key, model_attribute.field, id) for id in value]
|
|
424
452
|
elif isinstance(
|
|
425
|
-
model_attribute,
|
|
453
|
+
model_attribute, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField
|
|
426
454
|
):
|
|
427
455
|
value = wrap_primary_key(key, model_attribute, value)
|
|
428
456
|
elif isinstance(
|
|
@@ -517,3 +545,81 @@ def wrap_primary_key(
|
|
|
517
545
|
raise ValueError(
|
|
518
546
|
f"Unknown ID field type {id_field.__class__} for {document_type} (ID field name is {id_field_name}, value was {value})"
|
|
519
547
|
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def get_fields_with_additional_filters(additional_filters: Dict[str, str]) -> Dict[str, Any]:
|
|
551
|
+
"""
|
|
552
|
+
Right now we only support additional filters like "organization.badges".
|
|
553
|
+
|
|
554
|
+
The goal of this function is to key the additional filters by the first part (`organization`) to
|
|
555
|
+
be able to compute them when we loop over all the fields (`title`, `organization`…)
|
|
556
|
+
|
|
557
|
+
The `additional_filters` property is a dict: {"label": "key"}, for example {"organization_badge": "organization.badges"}.
|
|
558
|
+
The `label` will be the name of the parser arg, like `?organization_badge=public-service`, which makes more
|
|
559
|
+
sense than `?organization_badges=public-service`.
|
|
560
|
+
"""
|
|
561
|
+
results: dict = {}
|
|
562
|
+
for label, key in additional_filters.items():
|
|
563
|
+
parts = key.split(".")
|
|
564
|
+
if len(parts) == 2:
|
|
565
|
+
parent = parts[0]
|
|
566
|
+
child = parts[1]
|
|
567
|
+
|
|
568
|
+
if parent not in results:
|
|
569
|
+
results[parent] = {"children": []}
|
|
570
|
+
|
|
571
|
+
results[parent]["children"].append(
|
|
572
|
+
{
|
|
573
|
+
"label": label,
|
|
574
|
+
"key": child,
|
|
575
|
+
"type": str,
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
raise Exception(f"Do not support `additional_filters` without two parts: {key}.")
|
|
580
|
+
|
|
581
|
+
return results
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def compute_filter(column: str, field, info, filterable):
|
|
585
|
+
# "key" is the param key in the URL
|
|
586
|
+
if "key" not in filterable:
|
|
587
|
+
filterable["key"] = column
|
|
588
|
+
|
|
589
|
+
# If we do a filter on a embed document, get the class info
|
|
590
|
+
# of this document to see if there is a default filter value
|
|
591
|
+
embed_info = None
|
|
592
|
+
if isinstance(field, mongo_fields.EmbeddedDocumentField):
|
|
593
|
+
embed_info = field.get("__additional_class_info__", None)
|
|
594
|
+
elif isinstance(field, mongo_fields.EmbeddedDocumentListField):
|
|
595
|
+
embed_info = getattr(field.field.document_type, "__additional_class_info__", None)
|
|
596
|
+
|
|
597
|
+
if embed_info and embed_info.get("default_filterable_field", None):
|
|
598
|
+
# There is a default filterable field so append it to the column and replace the
|
|
599
|
+
# field to use the inner one (for example using the `kind` `StringField` instead of
|
|
600
|
+
# the embed `Badge` field.)
|
|
601
|
+
filterable["column"] = f"{column}__{embed_info['default_filterable_field']}"
|
|
602
|
+
field = getattr(field.field.document_type, embed_info["default_filterable_field"])
|
|
603
|
+
else:
|
|
604
|
+
filterable["column"] = column
|
|
605
|
+
|
|
606
|
+
if "constraints" not in filterable:
|
|
607
|
+
filterable["constraints"] = []
|
|
608
|
+
|
|
609
|
+
if isinstance(field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField) or (
|
|
610
|
+
isinstance(field, mongo_fields.ListField)
|
|
611
|
+
and isinstance(field.field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField)
|
|
612
|
+
):
|
|
613
|
+
filterable["constraints"].append("objectid")
|
|
614
|
+
|
|
615
|
+
if "type" not in filterable:
|
|
616
|
+
if isinstance(field, mongo_fields.BooleanField):
|
|
617
|
+
filterable["type"] = boolean
|
|
618
|
+
else:
|
|
619
|
+
filterable["type"] = str
|
|
620
|
+
|
|
621
|
+
filterable["choices"] = info.get("choices", None)
|
|
622
|
+
if hasattr(field, "choices") and field.choices:
|
|
623
|
+
filterable["choices"] = field.choices
|
|
624
|
+
|
|
625
|
+
return filterable
|
udata/core/badges/factories.py
CHANGED
|
@@ -2,14 +2,12 @@ from factory.fuzzy import FuzzyChoice
|
|
|
2
2
|
|
|
3
3
|
from udata.factories import ModelFactory
|
|
4
4
|
|
|
5
|
-
from .models import Badge
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
def badge_factory(model):
|
|
6
|
+
def badge_factory(model_):
|
|
9
7
|
class BadgeFactory(ModelFactory):
|
|
10
8
|
class Meta:
|
|
11
|
-
model =
|
|
9
|
+
model = model_._fields["badges"].field.document_type
|
|
12
10
|
|
|
13
|
-
kind = FuzzyChoice(
|
|
11
|
+
kind = FuzzyChoice(model_.__badges__)
|
|
14
12
|
|
|
15
13
|
return BadgeFactory
|
udata/core/badges/forms.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from udata.forms import ModelForm, fields, validators
|
|
2
2
|
from udata.i18n import lazy_gettext as _
|
|
3
|
-
from udata.models import Badge
|
|
4
3
|
|
|
5
4
|
__all__ = ("badge_form",)
|
|
6
5
|
|
|
@@ -9,8 +8,6 @@ def badge_form(model):
|
|
|
9
8
|
"""A form factory for a given model badges"""
|
|
10
9
|
|
|
11
10
|
class BadgeForm(ModelForm):
|
|
12
|
-
model_class = Badge
|
|
13
|
-
|
|
14
11
|
kind = fields.RadioField(
|
|
15
12
|
_("Kind"),
|
|
16
13
|
[validators.DataRequired()],
|
udata/core/badges/models.py
CHANGED
|
@@ -3,7 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
|
|
4
4
|
from mongoengine.signals import post_save
|
|
5
5
|
|
|
6
|
-
from udata.api_fields import field
|
|
6
|
+
from udata.api_fields import field, generate_fields
|
|
7
7
|
from udata.auth import current_user
|
|
8
8
|
from udata.core.badges.fields import badge_fields
|
|
9
9
|
from udata.mongo import db
|
|
@@ -12,10 +12,19 @@ from .signals import on_badge_added, on_badge_removed
|
|
|
12
12
|
|
|
13
13
|
log = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
-
__all__ = ("Badge", "BadgeMixin")
|
|
16
15
|
|
|
16
|
+
__all__ = ["Badge", "BadgeMixin", "BadgesList"]
|
|
17
17
|
|
|
18
|
+
DEFAULT_BADGES_LIST_PARAMS = {
|
|
19
|
+
"readonly": True,
|
|
20
|
+
"inner_field_info": {"nested_fields": badge_fields},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@generate_fields(default_filterable_field="kind")
|
|
18
25
|
class Badge(db.EmbeddedDocument):
|
|
26
|
+
meta = {"allow_inheritance": True}
|
|
27
|
+
# The following field should be overloaded in descendants.
|
|
19
28
|
kind = db.StringField(required=True)
|
|
20
29
|
created = db.DateTimeField(default=datetime.utcnow, required=True)
|
|
21
30
|
created_by = db.ReferenceField("User")
|
|
@@ -23,30 +32,16 @@ class Badge(db.EmbeddedDocument):
|
|
|
23
32
|
def __str__(self):
|
|
24
33
|
return self.kind
|
|
25
34
|
|
|
26
|
-
def validate(self, clean=True):
|
|
27
|
-
badges = getattr(self._instance, "__badges__", {})
|
|
28
|
-
if self.kind not in badges.keys():
|
|
29
|
-
raise db.ValidationError("Unknown badge type %s" % self.kind)
|
|
30
|
-
return super(Badge, self).validate(clean=clean)
|
|
31
|
-
|
|
32
35
|
|
|
33
36
|
class BadgesList(db.EmbeddedDocumentListField):
|
|
34
|
-
def __init__(self, *args, **kwargs):
|
|
35
|
-
return super(BadgesList, self).__init__(
|
|
36
|
-
|
|
37
|
-
def validate(self, value):
|
|
38
|
-
kinds = [b.kind for b in value]
|
|
39
|
-
if len(kinds) > len(set(kinds)):
|
|
40
|
-
raise db.ValidationError("Duplicate badges for a given kind is not allowed")
|
|
41
|
-
return super(BadgesList, self).validate(value)
|
|
37
|
+
def __init__(self, badge_model, *args, **kwargs):
|
|
38
|
+
return super(BadgesList, self).__init__(badge_model, *args, **kwargs)
|
|
42
39
|
|
|
43
40
|
|
|
44
|
-
class BadgeMixin
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
inner_field_info={"nested_fields": badge_fields},
|
|
49
|
-
)
|
|
41
|
+
class BadgeMixin:
|
|
42
|
+
default_badges_list_params = DEFAULT_BADGES_LIST_PARAMS
|
|
43
|
+
# The following field should be overloaded in descendants.
|
|
44
|
+
badges = field(BadgesList(Badge), **DEFAULT_BADGES_LIST_PARAMS)
|
|
50
45
|
|
|
51
46
|
def get_badge(self, kind):
|
|
52
47
|
"""Get a badge given its kind if present"""
|
|
@@ -61,7 +56,7 @@ class BadgeMixin(object):
|
|
|
61
56
|
if kind not in getattr(self, "__badges__", {}):
|
|
62
57
|
msg = "Unknown badge type for {model}: {kind}"
|
|
63
58
|
raise db.ValidationError(msg.format(model=self.__class__.__name__, kind=kind))
|
|
64
|
-
badge =
|
|
59
|
+
badge = self._fields["badges"].field.document_type(kind=kind)
|
|
65
60
|
if current_user.is_authenticated:
|
|
66
61
|
badge.created_by = current_user.id
|
|
67
62
|
|
|
@@ -88,5 +83,5 @@ class BadgeMixin(object):
|
|
|
88
83
|
|
|
89
84
|
def badge_label(self, badge):
|
|
90
85
|
"""Display the badge label for a given kind"""
|
|
91
|
-
kind = badge.kind if isinstance(badge,
|
|
86
|
+
kind = badge.kind if isinstance(badge, self.badge) else badge
|
|
92
87
|
return self.__badges__[kind]
|
|
@@ -4,7 +4,6 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
from udata.core.organization.constants import CERTIFIED, PUBLIC_SERVICE
|
|
6
6
|
from udata.core.organization.factories import OrganizationFactory
|
|
7
|
-
from udata.models import Badge
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
@pytest.mark.usefixtures("clean_db")
|
|
@@ -21,9 +20,9 @@ class BadgeCommandTest:
|
|
|
21
20
|
assert org.badges[0].kind == PUBLIC_SERVICE
|
|
22
21
|
|
|
23
22
|
def test_toggle_badge_off(self, cli):
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
org
|
|
23
|
+
org = OrganizationFactory()
|
|
24
|
+
org.add_badge(PUBLIC_SERVICE)
|
|
25
|
+
org.add_badge(CERTIFIED)
|
|
27
26
|
|
|
28
27
|
cli("badges", "toggle", str(org.id), PUBLIC_SERVICE)
|
|
29
28
|
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
+
from udata.api_fields import field
|
|
1
2
|
from udata.auth import login_user
|
|
2
3
|
from udata.core.user.factories import UserFactory
|
|
3
4
|
from udata.mongo import db
|
|
4
5
|
from udata.tests import DBTestMixin, TestCase
|
|
5
6
|
|
|
6
|
-
from ..models import Badge, BadgeMixin
|
|
7
|
+
from ..models import Badge, BadgeMixin, BadgesList
|
|
7
8
|
|
|
8
9
|
TEST = "test"
|
|
9
10
|
OTHER = "other"
|
|
10
11
|
|
|
12
|
+
BADGES = {
|
|
13
|
+
TEST: "Test",
|
|
14
|
+
OTHER: "Other",
|
|
15
|
+
}
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
class FakeBadge(Badge):
|
|
19
|
+
kind = db.StringField(required=True, choices=list(BADGES.keys()))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FakeBadgeMixin(BadgeMixin):
|
|
23
|
+
badges = field(BadgesList(FakeBadge), **BadgeMixin.default_badges_list_params)
|
|
24
|
+
__badges__ = BADGES
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Fake(db.Document, FakeBadgeMixin):
|
|
28
|
+
pass
|
|
17
29
|
|
|
18
30
|
|
|
19
31
|
class BadgeMixinTest(DBTestMixin, TestCase):
|
|
@@ -22,15 +34,24 @@ class BadgeMixinTest(DBTestMixin, TestCase):
|
|
|
22
34
|
fake = Fake.objects.create()
|
|
23
35
|
self.assertIsInstance(fake.badges, (list, tuple))
|
|
24
36
|
|
|
37
|
+
def test_choices(self):
|
|
38
|
+
"""It should have a choice list on the badge field."""
|
|
39
|
+
self.assertEqual(
|
|
40
|
+
Fake._fields["badges"].field.document_type.kind.choices, list(Fake.__badges__.keys())
|
|
41
|
+
)
|
|
42
|
+
|
|
25
43
|
def test_get_badge_found(self):
|
|
26
44
|
"""It allow to get a badge by kind if present"""
|
|
27
|
-
fake = Fake.objects.create(
|
|
45
|
+
fake = Fake.objects.create()
|
|
46
|
+
fake.add_badge(TEST)
|
|
47
|
+
fake.add_badge(OTHER)
|
|
28
48
|
badge = fake.get_badge(TEST)
|
|
29
49
|
self.assertEqual(badge.kind, TEST)
|
|
30
50
|
|
|
31
51
|
def test_get_badge_not_found(self):
|
|
32
52
|
"""It should return None if badge is absent"""
|
|
33
|
-
fake = Fake.objects.create(
|
|
53
|
+
fake = Fake.objects.create()
|
|
54
|
+
fake.add_badge(OTHER)
|
|
34
55
|
badge = fake.get_badge(TEST)
|
|
35
56
|
self.assertIsNone(badge)
|
|
36
57
|
|
|
@@ -49,7 +70,8 @@ class BadgeMixinTest(DBTestMixin, TestCase):
|
|
|
49
70
|
|
|
50
71
|
def test_add_2nd_badge(self):
|
|
51
72
|
"""It should add badges to the top of the list"""
|
|
52
|
-
fake = Fake.objects.create(
|
|
73
|
+
fake = Fake.objects.create()
|
|
74
|
+
fake.add_badge(OTHER)
|
|
53
75
|
|
|
54
76
|
result = fake.add_badge(TEST)
|
|
55
77
|
|
|
@@ -86,8 +108,8 @@ class BadgeMixinTest(DBTestMixin, TestCase):
|
|
|
86
108
|
|
|
87
109
|
def test_remove_badge(self):
|
|
88
110
|
"""It should remove a badge given its kind"""
|
|
89
|
-
|
|
90
|
-
fake
|
|
111
|
+
fake = Fake.objects.create()
|
|
112
|
+
fake.add_badge(TEST)
|
|
91
113
|
|
|
92
114
|
fake.remove_badge(TEST)
|
|
93
115
|
|
|
@@ -121,28 +143,15 @@ class BadgeMixinTest(DBTestMixin, TestCase):
|
|
|
121
143
|
|
|
122
144
|
def test_toggle_remove_badge(self):
|
|
123
145
|
"""Toggle should remove a badge given its kind if present"""
|
|
124
|
-
|
|
125
|
-
fake
|
|
146
|
+
fake = Fake.objects.create()
|
|
147
|
+
fake.add_badge(TEST)
|
|
126
148
|
|
|
127
149
|
fake.toggle_badge(TEST)
|
|
128
150
|
|
|
129
151
|
self.assertEqual(len(fake.badges), 0)
|
|
130
152
|
|
|
131
|
-
def test_create_with_badges(self):
|
|
132
|
-
"""It should allow object creation with badges"""
|
|
133
|
-
fake = Fake.objects.create(badges=[Badge(kind=TEST), Badge(kind=OTHER)])
|
|
134
|
-
|
|
135
|
-
self.assertEqual(len(fake.badges), 2)
|
|
136
|
-
for badge, kind in zip(fake.badges, (TEST, OTHER)):
|
|
137
|
-
self.assertEqual(badge.kind, kind)
|
|
138
|
-
self.assertIsNotNone(badge.created)
|
|
139
|
-
|
|
140
|
-
def test_create_disallow_duplicate_badges(self):
|
|
141
|
-
"""It should not allow object creation with duplicate badges"""
|
|
142
|
-
with self.assertRaises(db.ValidationError):
|
|
143
|
-
Fake.objects.create(badges=[Badge(kind=TEST), Badge(kind=TEST)])
|
|
144
|
-
|
|
145
153
|
def test_create_disallow_unknown_badges(self):
|
|
146
154
|
"""It should not allow object creation with unknown badges"""
|
|
147
155
|
with self.assertRaises(db.ValidationError):
|
|
148
|
-
Fake.objects.create(
|
|
156
|
+
fake = Fake.objects.create()
|
|
157
|
+
fake.add_badge("unknown")
|
udata/core/dataservices/api.py
CHANGED
|
@@ -7,8 +7,8 @@ from flask_login import current_user
|
|
|
7
7
|
|
|
8
8
|
from udata.api import API, api, fields
|
|
9
9
|
from udata.api_fields import patch
|
|
10
|
+
from udata.core.dataservices.permissions import OwnablePermission
|
|
10
11
|
from udata.core.dataset.models import Dataset
|
|
11
|
-
from udata.core.dataset.permissions import OwnablePermission
|
|
12
12
|
from udata.core.followers.api import FollowAPI
|
|
13
13
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
14
14
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from flask import request
|
|
2
|
+
|
|
3
|
+
from udata import search
|
|
4
|
+
from udata.api import API, apiv2
|
|
5
|
+
from udata.core.dataservices.models import Dataservice, HarvestMetadata
|
|
6
|
+
from udata.utils import multi_to_dict
|
|
7
|
+
|
|
8
|
+
from .search import DataserviceSearch
|
|
9
|
+
|
|
10
|
+
apiv2.inherit("DataservicePage", Dataservice.__page_fields__)
|
|
11
|
+
apiv2.inherit("Dataservice (read)", Dataservice.__read_fields__)
|
|
12
|
+
apiv2.inherit("HarvestMetadata (read)", HarvestMetadata.__read_fields__)
|
|
13
|
+
|
|
14
|
+
ns = apiv2.namespace("dataservices", "Dataservice related operations")
|
|
15
|
+
|
|
16
|
+
search_parser = DataserviceSearch.as_request_parser()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@ns.route("/search/", endpoint="dataservice_search")
|
|
20
|
+
class DataserviceSearchAPI(API):
|
|
21
|
+
"""Dataservices collection search endpoint"""
|
|
22
|
+
|
|
23
|
+
@apiv2.doc("search_dataservices")
|
|
24
|
+
@apiv2.expect(search_parser)
|
|
25
|
+
@apiv2.marshal_with(Dataservice.__page_fields__)
|
|
26
|
+
def get(self):
|
|
27
|
+
"""Search all dataservices"""
|
|
28
|
+
search_parser.parse_args()
|
|
29
|
+
return search.query(DataserviceSearch, **multi_to_dict(request.args))
|
|
@@ -95,7 +95,10 @@ class HarvestMetadata(db.EmbeddedDocument):
|
|
|
95
95
|
archived_at = field(db.DateTimeField())
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
@generate_fields(
|
|
98
|
+
@generate_fields(
|
|
99
|
+
searchable=True,
|
|
100
|
+
additional_filters={"organization_badge": "organization.badges"},
|
|
101
|
+
)
|
|
99
102
|
class Dataservice(WithMetrics, Owned, db.Document):
|
|
100
103
|
meta = {
|
|
101
104
|
"indexes": [
|