udata 11.0.2.dev18__py3-none-any.whl → 11.0.2.dev20__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 (28) hide show
  1. udata/api_fields.py +18 -20
  2. udata/core/dataservices/models.py +21 -1
  3. udata/core/dataservices/tasks.py +3 -0
  4. udata/core/reuse/models.py +1 -1
  5. udata/core/topic/factories.py +7 -1
  6. udata/core/topic/models.py +1 -3
  7. udata/static/chunks/{11.0f04e49a40a0a381bcce.js → 11.b6f741fcc366abfad9c4.js} +3 -3
  8. udata/static/chunks/{11.0f04e49a40a0a381bcce.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
  9. udata/static/chunks/{13.d9c1735d14038b94c17e.js → 13.2d06442dd9a05d9777b5.js} +2 -2
  10. udata/static/chunks/{13.d9c1735d14038b94c17e.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
  11. udata/static/chunks/{17.81c57c0dedf812e43013.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
  12. udata/static/chunks/{17.81c57c0dedf812e43013.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
  13. udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.f03a102365af4315f9db.js} +3 -3
  14. udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
  15. udata/static/chunks/{8.494b003a94383b142c18.js → 8.778091d55cd8ea39af6b.js} +2 -2
  16. udata/static/chunks/{8.494b003a94383b142c18.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
  17. udata/static/common.js +1 -1
  18. udata/static/common.js.map +1 -1
  19. udata/tests/api/test_dataservices_api.py +10 -0
  20. udata/tests/apiv2/test_topics.py +20 -3
  21. udata/tests/test_api_fields.py +21 -5
  22. udata/tests/topic/test_topic_tasks.py +8 -1
  23. {udata-11.0.2.dev18.dist-info → udata-11.0.2.dev20.dist-info}/METADATA +2 -2
  24. {udata-11.0.2.dev18.dist-info → udata-11.0.2.dev20.dist-info}/RECORD +28 -28
  25. {udata-11.0.2.dev18.dist-info → udata-11.0.2.dev20.dist-info}/WHEEL +0 -0
  26. {udata-11.0.2.dev18.dist-info → udata-11.0.2.dev20.dist-info}/entry_points.txt +0 -0
  27. {udata-11.0.2.dev18.dist-info → udata-11.0.2.dev20.dist-info}/licenses/LICENSE +0 -0
  28. {udata-11.0.2.dev18.dist-info → udata-11.0.2.dev20.dist-info}/top_level.txt +0 -0
udata/api_fields.py CHANGED
@@ -8,10 +8,11 @@ Main components:
8
8
  - `field()`: Universal function to add metadata to fields and methods
9
9
 
10
10
  The `@generate_fields` decorator parameters:
11
- - default_filterable_field: Default field for filtering (e.g., Badge.kind)
12
- - searchable: Enables full-text search with param `q` via MongoEngine on indexed text fields
13
- - additional_sorts: Custom sort options beyond field-based sorts
14
- - additional_filters: Cross-document filtering (e.g., Reuse__organization__badge)
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`
12
+ - searchable: boolean, if True, the document can be full-text searched using MongoEngine text search
13
+ - additional_sorts: add more sorts than the already available ones based on fields (see below). Eg, sort by metrics.
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`.
15
16
 
16
17
  Generated attributes on decorated classes:
17
18
  - ref_fields: Minimal fields for embedded/referenced documents
@@ -317,9 +318,9 @@ def generate_fields(**kwargs) -> Callable:
317
318
  ref_fields: dict = {}
318
319
  sortables: list = kwargs.get("additional_sorts", [])
319
320
 
320
- filterables: list[dict] = []
321
- additional_filters: dict[str, dict] = get_fields_with_additional_filters(
322
- 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", {})
323
324
  )
324
325
 
325
326
  read_fields["id"] = restx_fields.String(required=True, readonly=True)
@@ -341,16 +342,16 @@ def generate_fields(**kwargs) -> Callable:
341
342
  if filterable is not None:
342
343
  filterables.append(compute_filter(key, field, info, filterable))
343
344
 
344
- additional_filter: dict | None = additional_filters.get(key, None)
345
- if additional_filter:
345
+ nested_filter: dict | None = nested_filters.get(key, None)
346
+ if nested_filter:
346
347
  if not isinstance(
347
348
  field, mongo_fields.ReferenceField | mongo_fields.LazyReferenceField
348
349
  ):
349
- 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.")
350
351
 
351
352
  ref_model: db.Document = field.document_type
352
353
 
353
- for child in additional_filter.get("children", []):
354
+ for child in nested_filter.get("children", []):
354
355
  inner_field: str = getattr(ref_model, child["key"])
355
356
 
356
357
  column: str = f"{key}__{child['key']}"
@@ -474,7 +475,7 @@ def generate_fields(**kwargs) -> Callable:
474
475
 
475
476
  for filterable in filterables:
476
477
  parser.add_argument(
477
- # Use the custom label from `additional_filters` if there's one.
478
+ # Use the custom label from `nested_filters` if there's one.
478
479
  filterable.get("label", filterable["key"]),
479
480
  type=filterable["type"],
480
481
  location="args",
@@ -505,7 +506,7 @@ def generate_fields(**kwargs) -> Callable:
505
506
  base_query = base_query.search_text(phrase_query)
506
507
 
507
508
  for filterable in filterables:
508
- # 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,
509
510
  # eg use `organization_badge` instead of `organization.badges` which is
510
511
  # computed to `organization_badges`.
511
512
  filter = args.get(filterable.get("label", filterable["key"]))
@@ -792,21 +793,18 @@ def wrap_primary_key(
792
793
  )
793
794
 
794
795
 
795
- 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]:
796
797
  """Filter on additional related fields.
797
798
 
798
- Right now we only support additional filters with a depth of two, eg "organization.badges".
799
-
800
- The goal of this function is to key by the additional filters by the first part (`organization`) to
801
799
  be able to compute them when we loop over all the fields (`title`, `organization`…)
802
800
 
803
801
 
804
- 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"}.
805
803
  The `label` will be the name of the parser arg, like `?organization_badge=public-service`, which makes more
806
804
  sense than `?organization_badges=public-service`.
807
805
  """
808
806
  results: dict = {}
809
- for label, key in additional_filters.items():
807
+ for label, key in nested_filters.items():
810
808
  parts = key.split(".")
811
809
  if len(parts) == 2:
812
810
  parent = parts[0]
@@ -824,7 +822,7 @@ def get_fields_with_additional_filters(additional_filters: dict[str, str]) -> di
824
822
  }
825
823
  )
826
824
  else:
827
- 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}.")
828
826
 
829
827
  return results
830
828
 
@@ -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"},
@@ -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()
@@ -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):
@@ -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