udata 11.0.2.dev20__py3-none-any.whl → 11.0.2.dev22__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.

@@ -9,7 +9,7 @@ from mongoengine.signals import post_save
9
9
  from udata.api_fields import get_fields
10
10
  from udata.auth import current_user
11
11
  from udata.mongo import db
12
- from udata.utils import get_field_value_from_path
12
+ from udata.utils import filter_changed_fields, get_field_value_from_path
13
13
 
14
14
  from .signals import new_activity
15
15
 
@@ -130,16 +130,8 @@ class Auditable(object):
130
130
  cls.on_create.send(document)
131
131
  elif len(changed_fields):
132
132
  previous = getattr(document, "_previous_changed_fields", None)
133
- # make sure that changed fields have actually changed when comparing the document
134
- # once it has been reloaded. It may have been cleaned or normalized when saved to mongo.
135
- # We compare them one by one with the previous value stored in _previous_changed_fields.
136
- # See https://github.com/opendatateam/udata/pull/3412 for more context.
137
- document.reload()
138
- changed_fields = [
139
- field
140
- for field in changed_fields
141
- if previous[field] != get_field_value_from_path(document, field)
142
- ]
133
+ # Filter changed_fields since mongoengine raises some false positive occurences
134
+ changed_fields = filter_changed_fields(document, previous, changed_fields)
143
135
  if changed_fields:
144
136
  cls.on_update.send(document, changed_fields=changed_fields, previous=previous)
145
137
  if getattr(document, "deleted_at", None) or getattr(document, "deleted", None):
@@ -32,7 +32,7 @@ def dataservice_from_rdf(
32
32
  remote_url_prefix: str | None = None,
33
33
  ) -> Dataservice:
34
34
  """
35
- Create or update a dataset from a RDF/DCAT graph
35
+ Create or update a dataservice from a RDF/DCAT graph
36
36
  """
37
37
  if node is None: # Assume first match is the only match
38
38
  node = graph.value(predicate=RDF.type, object=DCAT.DataService)
@@ -57,7 +57,6 @@ def dataservice_from_rdf(
57
57
  contact_point for role in roles for contact_point in role
58
58
  ] or dataservice.contact_points
59
59
 
60
- datasets = []
61
60
  for dataset_node in d.objects(DCAT.servesDataset):
62
61
  id = dataset_node.value(DCT.identifier)
63
62
  dataset = next(
@@ -71,11 +70,9 @@ def dataservice_from_rdf(
71
70
  None,
72
71
  )
73
72
 
74
- if dataset is not None:
75
- datasets.append(dataset.id)
76
-
77
- if datasets:
78
- dataservice.datasets = datasets
73
+ # We append the dataset to the list of the current attached ones if not already attached
74
+ if dataset is not None and dataset not in dataservice.datasets:
75
+ dataservice.datasets.append(dataset)
79
76
 
80
77
  license = rdf_value(d, DCT.license)
81
78
  if license is not None:
@@ -8,8 +8,9 @@ from flask import current_app
8
8
  from lxml import etree
9
9
  from rdflib import Graph
10
10
 
11
+ from udata.core.dataservices.factories import DataserviceFactory
11
12
  from udata.core.dataservices.models import Dataservice
12
- from udata.core.dataset.factories import LicenseFactory, ResourceSchemaMockData
13
+ from udata.core.dataset.factories import DatasetFactory, LicenseFactory, ResourceSchemaMockData
13
14
  from udata.core.dataset.rdf import dataset_from_rdf
14
15
  from udata.core.organization.factories import OrganizationFactory
15
16
  from udata.harvest.models import HarvestJob
@@ -187,6 +188,49 @@ class DcatBackendTest:
187
188
  == "https://data.paris2024.org/api/explore/v2.1/console"
188
189
  )
189
190
 
191
+ def test_harvest_dataservices_keep_attached_associated_datasets(self, rmock):
192
+ """It should update the existing list of dataservice.datasets and not overwrite existing ones"""
193
+ rmock.get("https://example.com/schemas", json=ResourceSchemaMockData.get_mock_data())
194
+
195
+ filename = "bnodes.xml"
196
+ url = mock_dcat(rmock, filename)
197
+ org = OrganizationFactory()
198
+ source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
199
+
200
+ previously_attached_dataset = DatasetFactory()
201
+ previously_harvested_dataset = DatasetFactory(
202
+ harvest={
203
+ "remote_id": "2",
204
+ "domain": source.domain,
205
+ "source_id": str(source.id),
206
+ }
207
+ )
208
+ existing_dataservice = DataserviceFactory(
209
+ # Two datasets are already attached, the first one NOT connected via harvesting
210
+ # when the second one is connected with dcat:servesDataset in harvest graph
211
+ datasets=[
212
+ previously_attached_dataset,
213
+ previously_harvested_dataset,
214
+ ],
215
+ harvest={
216
+ "remote_id": "https://data.paris2024.org/api/explore/v2.1/",
217
+ "domain": source.domain,
218
+ "source_id": str(source.id),
219
+ },
220
+ )
221
+
222
+ actions.run(source)
223
+
224
+ existing_dataservice.reload()
225
+
226
+ assert len(Dataservice.objects) == 1
227
+ assert existing_dataservice.title == "Explore API v2"
228
+ assert (
229
+ len(existing_dataservice.datasets) == 2 + 1
230
+ ) # The previsouly harvested dataset, the previously attached one and a new harvested dataset
231
+ assert previously_attached_dataset in existing_dataservice.datasets
232
+ assert previously_harvested_dataset in existing_dataservice.datasets
233
+
190
234
  def test_harvest_dataservices_ignore_accessservices(self, rmock):
191
235
  rmock.get("https://example.com/schemas", json=ResourceSchemaMockData.get_mock_data())
192
236
 
@@ -26,6 +26,7 @@ class FakeAuditableSubject(Auditable, db.Document):
26
26
  tags = field(db.TagListField())
27
27
  some_date = field(db.DateField())
28
28
  daterange_embedded = field(db.EmbeddedDocumentField(db.DateRange))
29
+ some_list = field(db.ListField(db.StringField()))
29
30
  embedded_list = field(db.ListField(db.EmbeddedDocumentField("FakeEmbedded")))
30
31
  ref_list = field(db.ListField(db.ReferenceField("FakeSubject")))
31
32
  not_auditable = field(db.StringField(), auditable=False)
@@ -128,6 +129,7 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
128
129
  tags=["some", "tags"],
129
130
  some_date=date(2020, 1, 1),
130
131
  daterange_embedded={"start": date(2020, 1, 1), "end": date(2020, 12, 31)},
132
+ some_list=["some", "list"],
131
133
  embedded_list=[FakeEmbedded(name=f"fake_embedded_{i}") for i in range(3)],
132
134
  ref_list=[FakeSubject.objects.create(name=f"fake_ref_{i}") for i in range(3)],
133
135
  not_auditable="original",
@@ -150,6 +152,10 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
150
152
  with assert_emit(post_save, FakeAuditableSubject.on_update):
151
153
  fake.save()
152
154
 
155
+ fake.some_list = ["other", "list"]
156
+ with assert_emit(post_save, FakeAuditableSubject.on_update):
157
+ fake.save()
158
+
153
159
  fake.embedded_list[1].name = "other"
154
160
  with assert_emit(post_save, FakeAuditableSubject.on_update):
155
161
  fake.save()
@@ -181,6 +187,13 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
181
187
  fake.reload()
182
188
  self.assertEqual(fake.some_date, date(2027, 7, 7))
183
189
 
190
+ # 3. Reordering of some elements in a list
191
+ fake.some_list = ["list", "other"]
192
+ with assert_not_emit(FakeAuditableSubject.on_update):
193
+ fake.save()
194
+ fake.reload()
195
+ self.assertEqual(fake.some_list, ["list", "other"])
196
+
184
197
  # The deletion should trigger a delete signal
185
198
  with assert_not_emit(FakeAuditableSubject.on_update):
186
199
  fake.delete()
@@ -192,6 +205,7 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
192
205
  tags=["some", "tags"],
193
206
  some_date=date(2020, 1, 1),
194
207
  daterange_embedded={"start": date(2020, 1, 1), "end": date(2020, 12, 31)},
208
+ some_list=["some", "list"],
195
209
  embedded_list=[FakeEmbedded(name=f"fake_embedded_{i}") for i in range(3)],
196
210
  ref_list=[FakeSubject.objects.create(name=f"fake_ref_{i}") for i in range(3)],
197
211
  not_auditable="original",
@@ -204,6 +218,7 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
204
218
  "name",
205
219
  "tags",
206
220
  "some_date",
221
+ "some_list",
207
222
  "daterange_embedded.start",
208
223
  "daterange_embedded.end",
209
224
  "embedded_list.1.name",
@@ -214,6 +229,7 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
214
229
  self.assertEqual(args[1]["previous"]["some_date"], date(2020, 1, 1))
215
230
  self.assertEqual(args[1]["previous"]["daterange_embedded.start"], date(2020, 1, 1))
216
231
  self.assertEqual(args[1]["previous"]["daterange_embedded.end"], date(2020, 12, 31))
232
+ self.assertEqual(args[1]["previous"]["some_list"], ["some", "list"])
217
233
  self.assertEqual(args[1]["previous"]["embedded_list.1.name"], "fake_embedded_1")
218
234
 
219
235
  with assert_emit(FakeAuditableSubject.on_update, assertions_callback=check_signal_update):
@@ -222,6 +238,7 @@ class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
222
238
  fake.some_date = date(2027, 7, 7)
223
239
  fake.daterange_embedded.start = date(2017, 7, 7)
224
240
  fake.daterange_embedded.end = date(2027, 7, 7)
241
+ fake.some_list = ["other", "list"]
225
242
  fake.embedded_list[1].name = "other"
226
243
  # Modification of a reference document should not be taken into account in changed_fields
227
244
  fake.ref_list[1].name = "other"
udata/utils.py CHANGED
@@ -1,9 +1,11 @@
1
1
  import hashlib
2
+ import itertools
2
3
  import math
3
4
  import re
5
+ from collections import Counter
4
6
  from datetime import date, datetime
5
7
  from math import ceil
6
- from typing import Any
8
+ from typing import Any, Hashable
7
9
  from uuid import UUID, uuid4
8
10
  from xml.sax.saxutils import escape
9
11
 
@@ -53,6 +55,35 @@ def get_field_value_from_path(document, field_path: str):
53
55
  return doc_field
54
56
 
55
57
 
58
+ def filter_changed_fields(document, previous, changed_fields: list[str]):
59
+ # Make sure that changed fields have actually changed.
60
+ # We compare the document values once it has been reloaded.
61
+ # It may have been cleaned or normalized when saved to mongo.
62
+ # We compare the field values one by one with the previous value stored in _previous_changed_fields.
63
+ # We also ignore reordering in the case of list, ex tags or contact points.
64
+ # See https://github.com/opendatateam/udata/pull/3412 for more context.
65
+ document.reload()
66
+ filtered_changed_fields = []
67
+ for field in changed_fields:
68
+ previous_value = previous[field]
69
+ current_value = get_field_value_from_path(document, field)
70
+ # Filter out special case of list reordering, does not support unhashable types
71
+ if (
72
+ isinstance(previous_value, list)
73
+ and isinstance(current_value, list)
74
+ and all(
75
+ isinstance(value, Hashable)
76
+ for value in itertools.chain(previous_value, current_value)
77
+ )
78
+ ):
79
+ if Counter(previous_value) != Counter(current_value):
80
+ filtered_changed_fields.append(field)
81
+ # Direct comparison for the rest of the fields
82
+ elif previous_value != current_value:
83
+ filtered_changed_fields.append(field)
84
+ return filtered_changed_fields
85
+
86
+
56
87
  FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)")
57
88
  ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])")
58
89
  UUID_LENGTH = 36
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: udata
3
- Version: 11.0.2.dev20
3
+ Version: 11.0.2.dev22
4
4
  Summary: Open data portal
5
5
  Author-email: Opendata Team <opendatateam@data.gouv.fr>
6
6
  Maintainer-email: Opendata Team <opendatateam@data.gouv.fr>
@@ -18,7 +18,7 @@ udata/tasks.py,sha256=Sv01dhvATtq_oHOBp3J1j1VT1HQe0Pab7zxwIeIdKoo,5122
18
18
  udata/terms.md,sha256=nFx978tUQ3vTEv6POykXaZvcQ5e_gcvmO4ZgcfbSWXo,187
19
19
  udata/tracking.py,sha256=WOcqA1RlHN8EPFuEc2kNau54mec4-pvi-wUFrMXevzg,345
20
20
  udata/uris.py,sha256=1wOrsxu6lmZJ1h4634kNHjqOjaOO0D5cIWKF_v_Gtn4,4264
21
- udata/utils.py,sha256=W2-pZwpEYQULrovX4msGNjBvj6EvP9eJm6ueQ2ujbP0,9681
21
+ udata/utils.py,sha256=mtosjF91SPuSM-63EyxjLLnxs5DT0iSBdz-ECNeTYGU,11128
22
22
  udata/worker.py,sha256=K-Wafye5-uXP4kQlffRKws2J9YbJ6m6n2QjcVsY8Nsg,118
23
23
  udata/wsgi.py,sha256=MY8en9K9eDluvJYUxTdzqSDoYaDgCVZ69ZcUvxAvgqA,77
24
24
  udata/admin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -57,7 +57,7 @@ udata/core/linkable.py,sha256=8YuTzZo5Y36CVIYxESC-EPJ-zKsQTRbi4OIZTp-wWig,587
57
57
  udata/core/owned.py,sha256=OQT7wdk7dAqGvWDiJRVkKJxerDc9_Io910nvLmfBAVI,5561
58
58
  udata/core/activity/__init__.py,sha256=dLTseBmVYbQzxB7OR7Dz0LtIwEjNQvgUSR7SwfInb68,399
59
59
  udata/core/activity/api.py,sha256=dwdEkJxxwbQ2PsCSnv6fEE8Ps1FiJ4fEHpTog_gk4Rs,4291
60
- udata/core/activity/models.py,sha256=qZ0BMwrtXu_SZLICaVauDvcWA5CDMgZij2Hxro6CbTQ,5326
60
+ udata/core/activity/models.py,sha256=wJ5SyvTJXsaTUbJRQGK1ZvkJCG8bmlHSDrro1afsrhM,4926
61
61
  udata/core/activity/signals.py,sha256=Io2A43as3yR-DZ5R3wzM_bTpn528pxWsZDUFZ9xtj2Y,191
62
62
  udata/core/activity/tasks.py,sha256=F3PY12dnpT5Z8VuYfuOyDP6VPKPJmq1Sm4lSiPfmUCA,1498
63
63
  udata/core/badges/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -88,7 +88,7 @@ udata/core/dataservices/csv.py,sha256=HWI2JrN_Vuw0te9FHlJ6eyqcRcKHOKXuzg45D4Ti6F
88
88
  udata/core/dataservices/factories.py,sha256=pKVoArNSCIbvGA-cWUc7vr8TmjYsUvOXzzcuUB5JyF4,964
89
89
  udata/core/dataservices/models.py,sha256=Kkgf_9TfuuijtckoD6c4mM6bN7DZq508HmL_vUkNoqI,12384
90
90
  udata/core/dataservices/permissions.py,sha256=98zM_R4v2ZtRubflB7ajaVQz-DVc-pZBMgtKUYy34oI,169
91
- udata/core/dataservices/rdf.py,sha256=MKzoHDcsEu_Be5gI5l41vPrrlHCql8AnqTbrxhXv2D0,7949
91
+ udata/core/dataservices/rdf.py,sha256=lbOeH-2IG_4zz6WQtD4L_ADq9iZfdSQLAiutanxd8gU,8023
92
92
  udata/core/dataservices/search.py,sha256=Tt7CUqb49Rl4hvkfGO3AiNs1Oc7HhTeBp80xQK8wwXc,4911
93
93
  udata/core/dataservices/tasks.py,sha256=fHG1r5ymfJRXJ_Lug6je3VKZoK30XKXE2rQ8x0R-jUk,1068
94
94
  udata/core/dataset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -318,7 +318,7 @@ udata/harvest/tests/person.jsonld,sha256=I7Ynh-PQlNeD51I1LrCgYOEjhL-WBeb65xzIE_s
318
318
  udata/harvest/tests/test_actions.py,sha256=d5TTFTbs4PdBydWICqDtfoeo3zLyzcNDzv4aMH9spxo,25881
319
319
  udata/harvest/tests/test_api.py,sha256=gSuICkPy3KVRUhHAyudXVf_gLwiB7SoriUp3DLXWDdA,21611
320
320
  udata/harvest/tests/test_base_backend.py,sha256=ow8ecGtD836mUqyPWYjkS5nx0STyT5RMLgBdDyOhts4,19233
321
- udata/harvest/tests/test_dcat_backend.py,sha256=yREZBXgghpb4SFWdOWKLV_teJPe3A3D4wjHCTSeS6Ns,48645
321
+ udata/harvest/tests/test_dcat_backend.py,sha256=8w_wZepgkb0_j9Ah5f4qMwJIpuH7bTqmc4u5MaaP7G4,50551
322
322
  udata/harvest/tests/test_filters.py,sha256=PT2qopEIoXsqi8MsNDRuhNH7jGXiQo8r0uJrCOUd4aM,2465
323
323
  udata/harvest/tests/test_models.py,sha256=f9NRR2_S4oZFgF8qOumg0vv-lpnEBJbI5vNtcwFdSqM,831
324
324
  udata/harvest/tests/test_notifications.py,sha256=MMzTzkv-GXMNFeOwAi31rdTsAXyLCLOSna41zOtaJG0,816
@@ -631,7 +631,7 @@ udata/tests/helpers.py,sha256=VP8cz13WBxkMoTUbOwnMV2-bzE4nVHI6l_z6m3u1DIE,5975
631
631
  udata/tests/models.py,sha256=5oTC-cgKSL0sUdlqjUiJ6U8-YZBQanObb-MhZhQIV3M,238
632
632
  udata/tests/plugin.py,sha256=QDY6fqjozck1_KrNGqN4wkIUAdACpAnUDOW6GTjKmqQ,12480
633
633
  udata/tests/schemas.json,sha256=szM1jDpkogfOG4xWbjIGjLgG8l9-ZyE3JKQtecJyD1E,4990
634
- udata/tests/test_activity.py,sha256=u-gao2T7Q2N-isw3_qCh6z5nFiXmMV3N8jEjqJlP2cQ,9164
634
+ udata/tests/test_activity.py,sha256=6VpJbSNPSwBOcPXg1gHZF7kpEMzpvfd9TOvyqePQzRA,9871
635
635
  udata/tests/test_api_fields.py,sha256=NCUTtOMEaTM5-tK-YUxhjEud2IPIDOHR3vbZWAQdECg,12786
636
636
  udata/tests/test_cors.py,sha256=b_pyxKeIyqhnsXxXryPf4d0V0QxaLQ1P_VjY89Q_j3g,3233
637
637
  udata/tests/test_dcat_commands.py,sha256=fDAnAjkja8AXw_qzaAWnVTgglkBAvK2mjPMHUCtqrrU,919
@@ -764,9 +764,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=U0abG-nBwCIoYxRZNsc4KOLeIRSqTV
764
764
  udata/translations/pt/LC_MESSAGES/udata.po,sha256=eCG35rMzYLHXyLbsnLSexS1g0N_K-WpNHqrt_8y6I4E,48590
765
765
  udata/translations/sr/LC_MESSAGES/udata.mo,sha256=IBcCAdmcvkeK7ZeRBNRI-wJ0jzWNM0eXM5VXAc1frWI,28692
766
766
  udata/translations/sr/LC_MESSAGES/udata.po,sha256=yFxHEEB4behNwQ7JnyoYheiCKLNnMS_NV4guzgyzWcE,55332
767
- udata-11.0.2.dev20.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
768
- udata-11.0.2.dev20.dist-info/METADATA,sha256=TCEyw8Z7EE7cNZwnWV3jJoEOJboAJYOWEDXjv3OMel8,6769
769
- udata-11.0.2.dev20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
770
- udata-11.0.2.dev20.dist-info/entry_points.txt,sha256=v2u12qO11i2lyLNIp136WmLJ-NHT-Kew3Duu8J-AXPM,614
771
- udata-11.0.2.dev20.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
772
- udata-11.0.2.dev20.dist-info/RECORD,,
767
+ udata-11.0.2.dev22.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
768
+ udata-11.0.2.dev22.dist-info/METADATA,sha256=TjMQIGWeKQ9tIiKPyr0UxYK0XFjjBU9hGQiJAxrQj0I,6769
769
+ udata-11.0.2.dev22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
770
+ udata-11.0.2.dev22.dist-info/entry_points.txt,sha256=v2u12qO11i2lyLNIp136WmLJ-NHT-Kew3Duu8J-AXPM,614
771
+ udata-11.0.2.dev22.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
772
+ udata-11.0.2.dev22.dist-info/RECORD,,