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.

Files changed (34) hide show
  1. udata/api_fields.py +110 -58
  2. udata/core/dataservices/models.py +25 -5
  3. udata/core/dataservices/tasks.py +3 -0
  4. udata/core/pages/models.py +2 -2
  5. udata/core/reports/models.py +2 -2
  6. udata/core/reuse/models.py +5 -5
  7. udata/core/topic/factories.py +7 -1
  8. udata/core/topic/models.py +1 -3
  9. udata/static/chunks/{11.55ab79044cda0271b595.js → 11.83535504cd650ea08f65.js} +3 -3
  10. udata/static/chunks/{11.55ab79044cda0271b595.js.map → 11.83535504cd650ea08f65.js.map} +1 -1
  11. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.d9c1735d14038b94c17e.js} +2 -2
  12. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.d9c1735d14038b94c17e.js.map} +1 -1
  13. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.81c57c0dedf812e43013.js} +2 -2
  14. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.81c57c0dedf812e43013.js.map} +1 -1
  15. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.df16abde17a42033a7f8.js} +3 -3
  16. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.df16abde17a42033a7f8.js.map} +1 -1
  17. udata/static/chunks/{5.5660483641193b7f8295.js → 5.0fa1408dae4e76b87b2e.js} +3 -3
  18. udata/static/chunks/{5.5660483641193b7f8295.js.map → 5.0fa1408dae4e76b87b2e.js.map} +1 -1
  19. udata/static/chunks/{6.30dce49d17db07600b06.js → 6.d663709d877baa44a71e.js} +3 -3
  20. udata/static/chunks/{6.30dce49d17db07600b06.js.map → 6.d663709d877baa44a71e.js.map} +1 -1
  21. udata/static/chunks/{8.b58fcd977fcaf3415571.js → 8.462bb3029de008497675.js} +2 -2
  22. udata/static/chunks/{8.b58fcd977fcaf3415571.js.map → 8.462bb3029de008497675.js.map} +1 -1
  23. udata/static/common.js +1 -1
  24. udata/static/common.js.map +1 -1
  25. udata/tests/api/test_dataservices_api.py +10 -0
  26. udata/tests/apiv2/test_topics.py +20 -3
  27. udata/tests/test_api_fields.py +23 -7
  28. udata/tests/topic/test_topic_tasks.py +8 -1
  29. {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/METADATA +1 -1
  30. {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/RECORD +34 -34
  31. {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/WHEEL +0 -0
  32. {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/entry_points.txt +0 -0
  33. {udata-11.0.2.dev17.dist-info → udata-11.0.2.dev19.dist-info}/licenses/LICENSE +0 -0
  34. {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
- """Enhance a MongoEngine document class to give it super powers by decorating it with @generate_fields.
1
+ """API field generation and metadata management for MongoEngine documents.
2
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
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
- 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.
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
- - additional_filters: filter on a field of a field (aka "join"), eg filter on `Reuse__organization__badge=PUBLIC_SERVICE`.
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
- 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
-
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
- additional_filters: dict[str, dict] = get_fields_with_additional_filters(
337
- kwargs.get("additional_filters", {})
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
- additional_filter: dict | None = additional_filters.get(key, None)
360
- if additional_filter:
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 additional_filters on a field that is not a ref.")
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 additional_filter.get("children", []):
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 `@function_field()` and should be included
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 `additional_filters` if there's one.
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 `additional_filter`, use the custom label instead of the key,
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 function_field(**info) -> Callable:
562
- def inner(func):
563
- func.__additional_field_info__ = info
564
- return func
565
-
566
- return inner
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
- def field(inner, **kwargs):
570
- """Simple wrapper to make a document field visible for the API.
618
+ # Add any extra kwargs passed
619
+ field_info.update(kwargs)
571
620
 
572
- We can pass additional arguments that will be forwarded to the RestX field constructor.
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
- inner.__additional_field_info__ = kwargs
576
- return inner
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 get_fields_with_additional_filters(additional_filters: dict[str, str]) -> dict[str, Any]:
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 `additional_filters` property is a dict: {"label": "key"}, for example {"organization_badge": "organization.badges"}.
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 additional_filters.items():
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 `additional_filters` without two parts: {key}.")
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, function_field, generate_fields
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
- additional_filters={"organization_badge": "organization.badges"},
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
- @function_field(description="Link to the API endpoint for this dataservice")
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
- @function_field(description="Link to the udata web page for this dataservice", show_as_ref=True)
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
- @function_field(
332
+ @field(
313
333
  nested_fields=dataservice_permissions_fields,
314
334
  )
315
335
  def permissions(self):
@@ -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()
@@ -1,5 +1,5 @@
1
1
  from udata.api import api, fields
2
- from udata.api_fields import field, function_field, generate_fields
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
- @function_field(
94
+ @field(
95
95
  nested_fields=page_permissions_fields,
96
96
  )
97
97
  def permissions(self):
@@ -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, function_field, generate_fields
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
- @function_field(description="Link to the API endpoint for this report")
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
 
@@ -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, function_field, generate_fields
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
- additional_filters={"organization_badge": "organization.badges"},
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
- @function_field(description="Link to the API endpoint for this reuse", show_as_ref=True)
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
- @function_field(description="Link to the udata web page for this reuse", show_as_ref=True)
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
- @function_field(
209
+ @field(
210
210
  nested_fields=reuse_permissions_fields,
211
211
  )
212
212
  def permissions(self):
@@ -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__},
@@ -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