udata 11.0.2.dev17__py3-none-any.whl → 11.0.2.dev19__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 +110 -58
- udata/core/dataservices/models.py +25 -5
- udata/core/dataservices/tasks.py +3 -0
- udata/core/pages/models.py +2 -2
- udata/core/reports/models.py +2 -2
- udata/core/reuse/models.py +5 -5
- udata/core/topic/factories.py +7 -1
- udata/core/topic/models.py +1 -3
- udata/static/chunks/{11.55ab79044cda0271b595.js → 11.83535504cd650ea08f65.js} +3 -3
- udata/static/chunks/{11.55ab79044cda0271b595.js.map → 11.83535504cd650ea08f65.js.map} +1 -1
- udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.d9c1735d14038b94c17e.js} +2 -2
- udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.d9c1735d14038b94c17e.js.map} +1 -1
- udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.81c57c0dedf812e43013.js} +2 -2
- udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.81c57c0dedf812e43013.js.map} +1 -1
- udata/static/chunks/{19.f03a102365af4315f9db.js → 19.df16abde17a42033a7f8.js} +3 -3
- udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.df16abde17a42033a7f8.js.map} +1 -1
- udata/static/chunks/{5.5660483641193b7f8295.js → 5.0fa1408dae4e76b87b2e.js} +3 -3
- udata/static/chunks/{5.5660483641193b7f8295.js.map → 5.0fa1408dae4e76b87b2e.js.map} +1 -1
- udata/static/chunks/{6.30dce49d17db07600b06.js → 6.d663709d877baa44a71e.js} +3 -3
- udata/static/chunks/{6.30dce49d17db07600b06.js.map → 6.d663709d877baa44a71e.js.map} +1 -1
- udata/static/chunks/{8.b58fcd977fcaf3415571.js → 8.462bb3029de008497675.js} +2 -2
- udata/static/chunks/{8.b58fcd977fcaf3415571.js.map → 8.462bb3029de008497675.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_dataservices_api.py +10 -0
- udata/tests/apiv2/test_topics.py +20 -3
- udata/tests/test_api_fields.py +23 -7
- udata/tests/topic/test_topic_tasks.py +8 -1
- {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/METADATA +1 -1
- {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/RECORD +34 -34
- {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/WHEEL +0 -0
- {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/entry_points.txt +0 -0
- {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/licenses/LICENSE +0 -0
- {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/top_level.txt +0 -0
udata/api_fields.py
CHANGED
|
@@ -1,39 +1,25 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""API field generation and metadata management for MongoEngine documents.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- forms.py
|
|
6
|
-
- api_fields.py
|
|
3
|
+
This module provides tools to automatically generate Flask-RESTX fields from MongoEngine
|
|
4
|
+
documents, reducing duplication between model definitions and API serialization.
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
Main components:
|
|
7
|
+
- `@generate_fields`: Decorator that adds API field generation to document classes
|
|
8
|
+
- `field()`: Universal function to add metadata to fields and methods
|
|
9
9
|
|
|
10
|
+
The `@generate_fields` decorator parameters:
|
|
10
11
|
- 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
12
|
- searchable: boolean, if True, the document can be full-text searched using MongoEngine text search
|
|
12
13
|
- additional_sorts: add more sorts than the already available ones based on fields (see below). Eg, sort by metrics.
|
|
13
|
-
-
|
|
14
|
+
- nested_filters: filter on a field of a field (aka "join"), eg filter on `Reuse__organization__badge=PUBLIC_SERVICE`.
|
|
15
|
+
- standalone_filters: filter on something else than a field. Should be a list of dicts with filterable attributes, as returned by `compute_filter`.
|
|
14
16
|
|
|
17
|
+
Generated attributes on decorated classes:
|
|
18
|
+
- ref_fields: Minimal fields for embedded/referenced documents
|
|
19
|
+
- read_fields: All fields returned when querying a document
|
|
20
|
+
- write_fields: Fields accepted when creating/updating a document
|
|
15
21
|
|
|
16
|
-
|
|
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
|
-
|
|
22
|
+
For field-specific metadata, see the `field()` function documentation.
|
|
37
23
|
"""
|
|
38
24
|
|
|
39
25
|
import functools
|
|
@@ -332,9 +318,9 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
332
318
|
ref_fields: dict = {}
|
|
333
319
|
sortables: list = kwargs.get("additional_sorts", [])
|
|
334
320
|
|
|
335
|
-
filterables: list[dict] = []
|
|
336
|
-
|
|
337
|
-
kwargs.get("
|
|
321
|
+
filterables: list[dict] = kwargs.get("standalone_filters", [])
|
|
322
|
+
nested_filters: dict[str, dict] = get_fields_with_nested_filters(
|
|
323
|
+
kwargs.get("nested_filters", {})
|
|
338
324
|
)
|
|
339
325
|
|
|
340
326
|
read_fields["id"] = restx_fields.String(required=True, readonly=True)
|
|
@@ -356,16 +342,16 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
356
342
|
if filterable is not None:
|
|
357
343
|
filterables.append(compute_filter(key, field, info, filterable))
|
|
358
344
|
|
|
359
|
-
|
|
360
|
-
if
|
|
345
|
+
nested_filter: dict | None = nested_filters.get(key, None)
|
|
346
|
+
if nested_filter:
|
|
361
347
|
if not isinstance(
|
|
362
348
|
field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField
|
|
363
349
|
):
|
|
364
|
-
raise Exception("Cannot use
|
|
350
|
+
raise Exception("Cannot use nested_filters on a field that is not a ref.")
|
|
365
351
|
|
|
366
352
|
ref_model: db.Document = field.document_type
|
|
367
353
|
|
|
368
|
-
for child in
|
|
354
|
+
for child in nested_filter.get("children", []):
|
|
369
355
|
inner_field: str = getattr(ref_model, child["key"])
|
|
370
356
|
|
|
371
357
|
column: str = f"{key}__{child['key']}"
|
|
@@ -401,7 +387,7 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
401
387
|
|
|
402
388
|
# The goal of this loop is to fetch all functions (getters) of the class
|
|
403
389
|
# If a function has an `__additional_field_info__` attribute it means
|
|
404
|
-
# it has been decorated with `@
|
|
390
|
+
# it has been decorated with `@field()` and should be included
|
|
405
391
|
# in the API response.
|
|
406
392
|
for method_name in dir(cls):
|
|
407
393
|
if method_name == "objects":
|
|
@@ -489,7 +475,7 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
489
475
|
|
|
490
476
|
for filterable in filterables:
|
|
491
477
|
parser.add_argument(
|
|
492
|
-
# Use the custom label from `
|
|
478
|
+
# Use the custom label from `nested_filters` if there's one.
|
|
493
479
|
filterable.get("label", filterable["key"]),
|
|
494
480
|
type=filterable["type"],
|
|
495
481
|
location="args",
|
|
@@ -520,7 +506,7 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
520
506
|
base_query = base_query.search_text(phrase_query)
|
|
521
507
|
|
|
522
508
|
for filterable in filterables:
|
|
523
|
-
# If it's from an `
|
|
509
|
+
# If it's from an `nested_filter`, use the custom label instead of the key,
|
|
524
510
|
# eg use `organization_badge` instead of `organization.badges` which is
|
|
525
511
|
# computed to `organization_badges`.
|
|
526
512
|
filter = args.get(filterable.get("label", filterable["key"]))
|
|
@@ -558,22 +544,91 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
558
544
|
return wrapper
|
|
559
545
|
|
|
560
546
|
|
|
561
|
-
def
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
547
|
+
def field(
|
|
548
|
+
inner=None,
|
|
549
|
+
sortable: bool | str | None = None,
|
|
550
|
+
filterable: dict[str, Any] | None = None,
|
|
551
|
+
readonly: bool | None = None,
|
|
552
|
+
show_as_ref: bool | None = None,
|
|
553
|
+
markdown: bool | None = None,
|
|
554
|
+
description: str | None = None,
|
|
555
|
+
auditable: bool | None = None,
|
|
556
|
+
checks: list[Callable] | None = None,
|
|
557
|
+
attribute: str | None = None,
|
|
558
|
+
thumbnail_info: dict[str, Any] | None = None,
|
|
559
|
+
example: str | None = None,
|
|
560
|
+
nested_fields: dict[str, Any] | None = None,
|
|
561
|
+
inner_field_info: dict[str, Any] | None = None,
|
|
562
|
+
size: int | None = None,
|
|
563
|
+
is_thumbnail: bool | None = None,
|
|
564
|
+
href: Callable | None = None,
|
|
565
|
+
generic: bool | None = None,
|
|
566
|
+
generic_key: str | None = None,
|
|
567
|
+
convert_to: Callable | None = None,
|
|
568
|
+
allow_null: bool | None = None,
|
|
569
|
+
**kwargs: Any, # Accept any additional parameters, forward to flask rest x constructor.
|
|
570
|
+
):
|
|
571
|
+
"""Universal field decorator/wrapper for API field metadata.
|
|
572
|
+
|
|
573
|
+
Can be used in two ways:
|
|
574
|
+
|
|
575
|
+
1. As a wrapper for MongoEngine fields:
|
|
576
|
+
title = field(db.StringField(required=True),
|
|
577
|
+
sortable=True,
|
|
578
|
+
description="The title of the item")
|
|
579
|
+
|
|
580
|
+
2. As a decorator for computed fields:
|
|
581
|
+
@field(description="Link to the API endpoint", show_as_ref=True)
|
|
582
|
+
def uri(self):
|
|
583
|
+
return f"/api/items/{self.id}"
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
inner: The MongoEngine field to wrap (or None when used as decorator)
|
|
587
|
+
sortable: If True, field can be sorted. If str, use as custom sort key
|
|
588
|
+
filterable: Filter configuration dict
|
|
589
|
+
readonly: If True, exclude from write_fields
|
|
590
|
+
show_as_ref: If True, include in ref_fields
|
|
591
|
+
markdown: If True, use Markdown formatter
|
|
592
|
+
description: Field description for Swagger
|
|
593
|
+
auditable: If False, exclude from audit trail
|
|
594
|
+
checks: List of validation functions
|
|
595
|
+
attribute: Custom attribute name for serialization
|
|
596
|
+
thumbnail_info: Thumbnail configuration dict
|
|
597
|
+
example: Example value for documentation
|
|
598
|
+
nested_fields: RestX model for nested objects
|
|
599
|
+
inner_field_info: Additional info for list inner fields
|
|
600
|
+
size: Image size for thumbnails
|
|
601
|
+
is_thumbnail: If True, this is a thumbnail field
|
|
602
|
+
href: Function to generate API link
|
|
603
|
+
generic: If True, handle generic embedded documents
|
|
604
|
+
generic_key: Key for generic type discrimination
|
|
605
|
+
convert_to: Custom converter for RestX
|
|
606
|
+
allow_null: If True, field can be null
|
|
607
|
+
**kwargs: Any additional parameters not explicitly defined
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
When used as wrapper: The field with __additional_field_info__ attached.
|
|
611
|
+
When used as decorator: A decorator function.
|
|
612
|
+
"""
|
|
613
|
+
# Build field_info from non-None parameters, excluding 'inner' and 'kwargs'
|
|
614
|
+
field_info = {
|
|
615
|
+
k: v for k, v in locals().items() if v is not None and k not in ("inner", "kwargs")
|
|
616
|
+
}
|
|
568
617
|
|
|
569
|
-
|
|
570
|
-
|
|
618
|
+
# Add any extra kwargs passed
|
|
619
|
+
field_info.update(kwargs)
|
|
571
620
|
|
|
572
|
-
|
|
621
|
+
if inner is None:
|
|
622
|
+
# Used as a decorator for methods
|
|
623
|
+
def decorator(func):
|
|
624
|
+
func.__additional_field_info__ = field_info
|
|
625
|
+
return func
|
|
573
626
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
627
|
+
return decorator
|
|
628
|
+
else:
|
|
629
|
+
# Used as a field wrapper
|
|
630
|
+
inner.__additional_field_info__ = field_info
|
|
631
|
+
return inner
|
|
577
632
|
|
|
578
633
|
|
|
579
634
|
def patch(obj, request) -> type:
|
|
@@ -738,21 +793,18 @@ def wrap_primary_key(
|
|
|
738
793
|
)
|
|
739
794
|
|
|
740
795
|
|
|
741
|
-
def
|
|
796
|
+
def get_fields_with_nested_filters(nested_filters: dict[str, str]) -> dict[str, Any]:
|
|
742
797
|
"""Filter on additional related fields.
|
|
743
798
|
|
|
744
|
-
Right now we only support additional filters with a depth of two, eg "organization.badges".
|
|
745
|
-
|
|
746
|
-
The goal of this function is to key by the additional filters by the first part (`organization`) to
|
|
747
799
|
be able to compute them when we loop over all the fields (`title`, `organization`…)
|
|
748
800
|
|
|
749
801
|
|
|
750
|
-
The `
|
|
802
|
+
The `nested_filters` property is a dict: {"label": "key"}, for example {"organization_badge": "organization.badges"}.
|
|
751
803
|
The `label` will be the name of the parser arg, like `?organization_badge=public-service`, which makes more
|
|
752
804
|
sense than `?organization_badges=public-service`.
|
|
753
805
|
"""
|
|
754
806
|
results: dict = {}
|
|
755
|
-
for label, key in
|
|
807
|
+
for label, key in nested_filters.items():
|
|
756
808
|
parts = key.split(".")
|
|
757
809
|
if len(parts) == 2:
|
|
758
810
|
parent = parts[0]
|
|
@@ -770,7 +822,7 @@ def get_fields_with_additional_filters(additional_filters: dict[str, str]) -> di
|
|
|
770
822
|
}
|
|
771
823
|
)
|
|
772
824
|
else:
|
|
773
|
-
raise Exception(f"Do not support `
|
|
825
|
+
raise Exception(f"Do not support `nested_filters` without two parts: {key}.")
|
|
774
826
|
|
|
775
827
|
return results
|
|
776
828
|
|
|
@@ -7,7 +7,7 @@ from mongoengine.signals import post_save
|
|
|
7
7
|
|
|
8
8
|
import udata.core.contact_point.api_fields as contact_api_fields
|
|
9
9
|
from udata.api import api, fields
|
|
10
|
-
from udata.api_fields import field,
|
|
10
|
+
from udata.api_fields import field, generate_fields
|
|
11
11
|
from udata.core.activity.models import Auditable
|
|
12
12
|
from udata.core.dataservices.constants import (
|
|
13
13
|
DATASERVICE_ACCESS_AUDIENCE_CONDITIONS,
|
|
@@ -134,9 +134,29 @@ def check_only_one_condition_per_role(access_audiences, **_kwargs):
|
|
|
134
134
|
)
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def filter_by_topic(base_query, filter_value):
|
|
138
|
+
from udata.core.topic.models import Topic
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
topic = Topic.objects.get(id=filter_value)
|
|
142
|
+
except Topic.DoesNotExist:
|
|
143
|
+
pass
|
|
144
|
+
else:
|
|
145
|
+
return base_query.filter(
|
|
146
|
+
id__in=[
|
|
147
|
+
elt.element.id
|
|
148
|
+
for elt in topic.elements
|
|
149
|
+
if elt.element.__class__.__name__ == "Dataservice"
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
137
154
|
@generate_fields(
|
|
138
155
|
searchable=True,
|
|
139
|
-
|
|
156
|
+
nested_filters={"organization_badge": "organization.badges"},
|
|
157
|
+
standalone_filters=[
|
|
158
|
+
{"key": "topic", "constraints": "objectid", "query": filter_by_topic, "type": str}
|
|
159
|
+
],
|
|
140
160
|
additional_sorts=[
|
|
141
161
|
{"key": "followers", "value": "metrics.followers"},
|
|
142
162
|
{"key": "views", "value": "metrics.views"},
|
|
@@ -284,7 +304,7 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
|
|
|
284
304
|
auditable=False,
|
|
285
305
|
)
|
|
286
306
|
|
|
287
|
-
@
|
|
307
|
+
@field(description="Link to the API endpoint for this dataservice")
|
|
288
308
|
def self_api_url(self, **kwargs):
|
|
289
309
|
return url_for(
|
|
290
310
|
"api.dataservice",
|
|
@@ -292,7 +312,7 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
|
|
|
292
312
|
**self._self_api_url_kwargs(**kwargs),
|
|
293
313
|
)
|
|
294
314
|
|
|
295
|
-
@
|
|
315
|
+
@field(description="Link to the udata web page for this dataservice", show_as_ref=True)
|
|
296
316
|
def self_web_url(self, **kwargs):
|
|
297
317
|
return cdata_url(f"/dataservices/{self._link_id(**kwargs)}/", **kwargs)
|
|
298
318
|
|
|
@@ -309,7 +329,7 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
|
|
|
309
329
|
return self.private or self.deleted_at or self.archived_at
|
|
310
330
|
|
|
311
331
|
@property
|
|
312
|
-
@
|
|
332
|
+
@field(
|
|
313
333
|
nested_fields=dataservice_permissions_fields,
|
|
314
334
|
)
|
|
315
335
|
def permissions(self):
|
udata/core/dataservices/tasks.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from celery.utils.log import get_task_logger
|
|
2
2
|
|
|
3
3
|
from udata.core.dataservices.models import Dataservice
|
|
4
|
+
from udata.core.topic.models import TopicElement
|
|
4
5
|
from udata.harvest.models import HarvestJob
|
|
5
6
|
from udata.models import Discussion, Follow, Transfer
|
|
6
7
|
from udata.tasks import job
|
|
@@ -20,5 +21,7 @@ def purge_dataservices(self):
|
|
|
20
21
|
HarvestJob.objects(items__dataservice=dataservice).update(set__items__S__dataservice=None)
|
|
21
22
|
# Remove associated Transfers
|
|
22
23
|
Transfer.objects(subject=dataservice).delete()
|
|
24
|
+
# Remove dataservices references in Topics
|
|
25
|
+
TopicElement.objects(element=dataservice).update(element=None)
|
|
23
26
|
# Remove dataservice
|
|
24
27
|
dataservice.delete()
|
udata/core/pages/models.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from udata.api import api, fields
|
|
2
|
-
from udata.api_fields import field,
|
|
2
|
+
from udata.api_fields import field, generate_fields
|
|
3
3
|
from udata.core.activity.models import Auditable
|
|
4
4
|
from udata.core.dataservices.models import Dataservice
|
|
5
5
|
from udata.core.dataset.api_fields import dataset_fields
|
|
@@ -91,7 +91,7 @@ class Page(Auditable, Owned, Datetimed, db.Document):
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
@property
|
|
94
|
-
@
|
|
94
|
+
@field(
|
|
95
95
|
nested_fields=page_permissions_fields,
|
|
96
96
|
)
|
|
97
97
|
def permissions(self):
|
udata/core/reports/models.py
CHANGED
|
@@ -4,7 +4,7 @@ from bson import DBRef
|
|
|
4
4
|
from flask import url_for
|
|
5
5
|
from mongoengine import DO_NOTHING, NULLIFY, signals
|
|
6
6
|
|
|
7
|
-
from udata.api_fields import field,
|
|
7
|
+
from udata.api_fields import field, generate_fields
|
|
8
8
|
from udata.core.user.api_fields import user_ref_fields
|
|
9
9
|
from udata.core.user.models import User
|
|
10
10
|
from udata.mongo import db
|
|
@@ -46,7 +46,7 @@ class Report(db.Document):
|
|
|
46
46
|
readonly=True,
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
-
@
|
|
49
|
+
@field(description="Link to the API endpoint for this report")
|
|
50
50
|
def self_api_url(self):
|
|
51
51
|
return url_for("api.report", report=self, _external=True)
|
|
52
52
|
|
udata/core/reuse/models.py
CHANGED
|
@@ -3,7 +3,7 @@ from flask import url_for
|
|
|
3
3
|
from mongoengine.signals import post_save, pre_save
|
|
4
4
|
from werkzeug.utils import cached_property
|
|
5
5
|
|
|
6
|
-
from udata.api_fields import field,
|
|
6
|
+
from udata.api_fields import field, generate_fields
|
|
7
7
|
from udata.core.activity.models import Auditable
|
|
8
8
|
from udata.core.dataset.api_fields import dataset_fields
|
|
9
9
|
from udata.core.linkable import Linkable
|
|
@@ -60,7 +60,7 @@ class ReuseBadgeMixin(BadgeMixin):
|
|
|
60
60
|
{"key": "followers", "value": "metrics.followers"},
|
|
61
61
|
{"key": "views", "value": "metrics.views"},
|
|
62
62
|
],
|
|
63
|
-
|
|
63
|
+
nested_filters={"organization_badge": "organization.badges"},
|
|
64
64
|
mask="*,datasets{id,title,uri,page}",
|
|
65
65
|
)
|
|
66
66
|
class Reuse(db.Datetimed, Auditable, WithMetrics, ReuseBadgeMixin, Linkable, Owned, db.Document):
|
|
@@ -197,16 +197,16 @@ class Reuse(db.Datetimed, Auditable, WithMetrics, ReuseBadgeMixin, Linkable, Own
|
|
|
197
197
|
"api.reuse", reuse=self._link_id(**kwargs), **self._self_api_url_kwargs(**kwargs)
|
|
198
198
|
)
|
|
199
199
|
|
|
200
|
-
@
|
|
200
|
+
@field(description="Link to the API endpoint for this reuse", show_as_ref=True)
|
|
201
201
|
def uri(self, *args, **kwargs):
|
|
202
202
|
return self.self_api_url(*args, **kwargs)
|
|
203
203
|
|
|
204
|
-
@
|
|
204
|
+
@field(description="Link to the udata web page for this reuse", show_as_ref=True)
|
|
205
205
|
def page(self, *args, **kwargs):
|
|
206
206
|
return self.self_web_url(*args, **kwargs)
|
|
207
207
|
|
|
208
208
|
@property
|
|
209
|
-
@
|
|
209
|
+
@field(
|
|
210
210
|
nested_fields=reuse_permissions_fields,
|
|
211
211
|
)
|
|
212
212
|
def permissions(self):
|
udata/core/topic/factories.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import factory
|
|
2
2
|
|
|
3
3
|
from udata import utils
|
|
4
|
+
from udata.core.dataservices.factories import DataserviceFactory
|
|
4
5
|
from udata.core.dataset.factories import DatasetFactory
|
|
5
6
|
from udata.core.reuse.factories import ReuseFactory
|
|
6
7
|
from udata.factories import ModelFactory
|
|
@@ -38,6 +39,10 @@ class TopicElementReuseFactory(TopicElementFactory):
|
|
|
38
39
|
element = factory.SubFactory(ReuseFactory)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
class TopicElementDataserviceFactory(TopicElementFactory):
|
|
43
|
+
element = factory.SubFactory(DataserviceFactory)
|
|
44
|
+
|
|
45
|
+
|
|
41
46
|
class TopicFactory(ModelFactory):
|
|
42
47
|
class Meta:
|
|
43
48
|
model = Topic
|
|
@@ -58,9 +63,10 @@ class TopicWithElementsFactory(TopicFactory):
|
|
|
58
63
|
# Create associated elements
|
|
59
64
|
TopicElementDatasetFactory.create_batch(2, topic=self)
|
|
60
65
|
TopicElementReuseFactory.create(topic=self)
|
|
66
|
+
TopicElementDataserviceFactory.create(topic=self)
|
|
61
67
|
|
|
62
68
|
@classmethod
|
|
63
|
-
def elements_as_payload(cls, elements: list) -> dict:
|
|
69
|
+
def elements_as_payload(cls, elements: list) -> list[dict]:
|
|
64
70
|
return [
|
|
65
71
|
{
|
|
66
72
|
"element": {"id": str(elt.element.id), "class": elt.element.__class__.__name__},
|
udata/core/topic/models.py
CHANGED
|
@@ -4,10 +4,8 @@ from mongoengine.signals import post_delete, post_save
|
|
|
4
4
|
|
|
5
5
|
from udata.api_fields import field
|
|
6
6
|
from udata.core.activity.models import Auditable
|
|
7
|
-
from udata.core.dataset.models import Dataset
|
|
8
7
|
from udata.core.linkable import Linkable
|
|
9
8
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
10
|
-
from udata.core.reuse.models import Reuse
|
|
11
9
|
from udata.models import SpatialCoverage, db
|
|
12
10
|
from udata.search import reindex
|
|
13
11
|
from udata.tasks import as_task_param
|
|
@@ -20,7 +18,7 @@ class TopicElement(Auditable, db.Document):
|
|
|
20
18
|
description = field(db.StringField(required=False))
|
|
21
19
|
tags = field(db.ListField(db.StringField()))
|
|
22
20
|
extras = field(db.ExtrasField())
|
|
23
|
-
element = field(db.GenericReferenceField(choices=[Dataset, Reuse]))
|
|
21
|
+
element = field(db.GenericReferenceField(choices=["Dataset", "Reuse", "Dataservice"]))
|
|
24
22
|
# Made optional to allow proper form handling with commit=False
|
|
25
23
|
topic = field(db.ReferenceField("Topic", required=False))
|
|
26
24
|
|