udata 14.5.1.dev9__py3-none-any.whl → 14.7.3.dev4__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.
Files changed (55) hide show
  1. udata/api_fields.py +85 -15
  2. udata/auth/forms.py +1 -1
  3. udata/core/badges/tests/test_tasks.py +0 -2
  4. udata/core/dataservices/apiv2.py +1 -1
  5. udata/core/dataset/models.py +15 -3
  6. udata/core/dataset/rdf.py +10 -14
  7. udata/core/organization/apiv2.py +1 -1
  8. udata/core/organization/models.py +25 -5
  9. udata/core/pages/models.py +49 -0
  10. udata/core/pages/tests/test_api.py +165 -1
  11. udata/core/post/api.py +1 -1
  12. udata/core/post/constants.py +8 -0
  13. udata/core/post/models.py +27 -3
  14. udata/core/post/tests/test_api.py +116 -2
  15. udata/core/post/tests/test_models.py +24 -0
  16. udata/core/reuse/apiv2.py +1 -1
  17. udata/core/user/models.py +21 -6
  18. udata/features/notifications/models.py +4 -1
  19. udata/features/transfer/actions.py +2 -0
  20. udata/features/transfer/models.py +17 -0
  21. udata/features/transfer/notifications.py +96 -0
  22. udata/harvest/backends/ckan/harvesters.py +10 -2
  23. udata/migrations/2021-08-17-harvest-integrity.py +23 -16
  24. udata/migrations/2025-12-16-create-transfer-request-notifications.py +69 -0
  25. udata/migrations/2026-01-14-add-default-kind-to-posts.py +17 -0
  26. udata/tasks.py +1 -0
  27. udata/tests/apiv2/test_dataservices.py +14 -0
  28. udata/tests/apiv2/test_organizations.py +9 -0
  29. udata/tests/apiv2/test_reuses.py +11 -0
  30. udata/tests/dataset/test_dataset_rdf.py +49 -0
  31. udata/tests/search/test_search_integration.py +37 -0
  32. udata/tests/test_transfer.py +181 -2
  33. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  34. udata/translations/ar/LC_MESSAGES/udata.po +310 -158
  35. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  36. udata/translations/de/LC_MESSAGES/udata.po +314 -160
  37. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  38. udata/translations/es/LC_MESSAGES/udata.po +313 -160
  39. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  40. udata/translations/fr/LC_MESSAGES/udata.po +476 -202
  41. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  42. udata/translations/it/LC_MESSAGES/udata.po +318 -162
  43. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  44. udata/translations/pt/LC_MESSAGES/udata.po +316 -161
  45. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  46. udata/translations/sr/LC_MESSAGES/udata.po +324 -164
  47. udata/translations/udata.pot +169 -124
  48. udata/utils.py +23 -0
  49. {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/METADATA +2 -2
  50. {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/RECORD +54 -50
  51. udata/tests/apiv2/test_search.py +0 -30
  52. {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/WHEEL +0 -0
  53. {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/entry_points.txt +0 -0
  54. {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/licenses/LICENSE +0 -0
  55. {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,14 @@
1
+ from udata.core.dataservices.factories import DataserviceFactory
2
+ from udata.tests.api import APITestCase
3
+
4
+
5
+ class DataserviceSearchAPIV2Test(APITestCase):
6
+ def test_dataservice_search_with_model_query_param(self):
7
+ """Searching dataservices with 'model' as query param should not crash.
8
+
9
+ Regression test for: TypeError: query() got multiple values for argument 'model'
10
+ """
11
+ DataserviceFactory.create_batch(3)
12
+
13
+ response = self.get("/api/2/dataservices/search/?model=malicious")
14
+ self.assert200(response)
@@ -4,6 +4,15 @@ from udata.core.organization.factories import Member, OrganizationFactory
4
4
  from udata.tests.api import APITestCase
5
5
 
6
6
 
7
+ class OrganizationSearchAPIV2Test(APITestCase):
8
+ def test_organization_search_with_model_query_param(self):
9
+ """Searching organizations with 'model' as query param should not crash."""
10
+ OrganizationFactory.create_batch(3)
11
+
12
+ response = self.get("/api/2/organizations/search/?model=malicious")
13
+ self.assert200(response)
14
+
15
+
7
16
  class OrganizationExtrasAPITest(APITestCase):
8
17
  def setUp(self):
9
18
  self.login()
@@ -0,0 +1,11 @@
1
+ from udata.core.reuse.factories import ReuseFactory
2
+ from udata.tests.api import APITestCase
3
+
4
+
5
+ class ReuseSearchAPIV2Test(APITestCase):
6
+ def test_reuse_search_with_model_query_param(self):
7
+ """Searching reuses with 'model' as query param should not crash."""
8
+ ReuseFactory.create_batch(3)
9
+
10
+ response = self.get("/api/2/reuses/search/?model=malicious")
11
+ self.assert200(response)
@@ -518,6 +518,22 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
518
518
  assert isinstance(dataset, Dataset)
519
519
  assert dataset.harvest.modified_at is None
520
520
 
521
+ def test_unparseable_modified_at(self):
522
+ """Regression test: template strings like {{modified:toISO}} should not crash parsing."""
523
+ node = BNode()
524
+ g = Graph()
525
+
526
+ g.add((node, RDF.type, DCAT.Dataset))
527
+ g.add((node, DCT.identifier, Literal(faker.uuid4())))
528
+ g.add((node, DCT.title, Literal(faker.sentence())))
529
+ g.add((node, DCT.modified, Literal("{{modified:toISO}}")))
530
+
531
+ dataset = dataset_from_rdf(g)
532
+ dataset.validate()
533
+
534
+ assert isinstance(dataset, Dataset)
535
+ assert dataset.harvest.modified_at is None
536
+
521
537
  def test_contact_point_individual_vcard(self):
522
538
  g = Graph()
523
539
  node = URIRef("https://test.org/dataset")
@@ -864,6 +880,39 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
864
880
  assert resource.harvest.modified_at.date() == modified.date()
865
881
  assert resource.format == "csv"
866
882
 
883
+ def test_resource_future_modified_at(self):
884
+ node = BNode()
885
+ g = Graph()
886
+
887
+ modified = faker.future_datetime()
888
+
889
+ g.add((node, RDF.type, DCAT.Distribution))
890
+ g.add((node, DCT.title, Literal(faker.sentence())))
891
+ g.add((node, DCAT.downloadURL, Literal(faker.uri())))
892
+ g.add((node, DCT.modified, Literal(modified)))
893
+
894
+ resource = resource_from_rdf(g)
895
+ resource.validate()
896
+
897
+ assert isinstance(resource, Resource)
898
+ assert resource.harvest.modified_at is None
899
+
900
+ def test_resource_unparseable_modified_at(self):
901
+ """Regression test: template strings like {{modified:toISO}} should not crash parsing."""
902
+ node = BNode()
903
+ g = Graph()
904
+
905
+ g.add((node, RDF.type, DCAT.Distribution))
906
+ g.add((node, DCT.title, Literal(faker.sentence())))
907
+ g.add((node, DCAT.downloadURL, Literal(faker.uri())))
908
+ g.add((node, DCT.modified, Literal("{{modified:toISO}}")))
909
+
910
+ resource = resource_from_rdf(g)
911
+ resource.validate()
912
+
913
+ assert isinstance(resource, Resource)
914
+ assert resource.harvest.modified_at is None
915
+
867
916
  def test_download_url_over_access_url(self):
868
917
  node = BNode()
869
918
  g = Graph()
@@ -3,6 +3,8 @@ import time
3
3
  import pytest
4
4
 
5
5
  from udata.core.dataset.factories import DatasetFactory
6
+ from udata.core.organization.factories import OrganizationFactory
7
+ from udata.core.reuse.factories import VisibleReuseFactory
6
8
  from udata.tests.api import APITestCase
7
9
  from udata.tests.helpers import requires_search_service
8
10
 
@@ -31,3 +33,38 @@ class SearchIntegrationTest(APITestCase):
31
33
 
32
34
  titles = [d["title"] for d in response.json["data"]]
33
35
  assert "Données spectaculaires sur les transports" in titles
36
+
37
+ def test_reuse_search_with_organization_filter(self):
38
+ """
39
+ Regression test for: 500 Server Error when None values are passed to search service.
40
+
41
+ When searching reuses with only an organization filter, other params should not be
42
+ sent as literal 'None' strings (e.g. ?q=None&tag=None).
43
+ """
44
+ org = OrganizationFactory()
45
+ reuse = VisibleReuseFactory(organization=org)
46
+
47
+ time.sleep(1)
48
+
49
+ response = self.get(f"/api/2/reuses/search/?organization={org.id}")
50
+ self.assert200(response)
51
+ assert response.json["total"] >= 1
52
+ ids = [r["id"] for r in response.json["data"]]
53
+ assert str(reuse.id) in ids
54
+
55
+ def test_organization_search_with_query(self):
56
+ """
57
+ Regression test for: 500 Server Error when None values are passed to search service.
58
+
59
+ When searching organizations, other params should not be sent as literal
60
+ 'None' strings (e.g. ?badge=None).
61
+ """
62
+ org = OrganizationFactory(name="Organisation Unique Test")
63
+
64
+ time.sleep(1)
65
+
66
+ response = self.get("/api/2/organizations/search/?q=unique")
67
+ self.assert200(response)
68
+ assert response.json["total"] >= 1
69
+ ids = [o["id"] for o in response.json["data"]]
70
+ assert str(org.id) in ids
@@ -10,11 +10,13 @@ from udata.core.user.factories import UserFactory
10
10
  from udata.core.user.metrics import (
11
11
  update_owner_metrics, # noqa needed to register signals
12
12
  )
13
- from udata.features.transfer.actions import accept_transfer, request_transfer
13
+ from udata.features.notifications.models import Notification
14
+ from udata.features.transfer.actions import accept_transfer, refuse_transfer, request_transfer
14
15
  from udata.features.transfer.factories import TransferFactory
15
16
  from udata.features.transfer.notifications import transfer_request_notifications
16
17
  from udata.models import Member
17
- from udata.tests.api import PytestOnlyDBTestCase
18
+ from udata.tests.api import DBTestCase, PytestOnlyDBTestCase
19
+ from udata.tests.helpers import assert_equal_dates
18
20
  from udata.utils import faker
19
21
 
20
22
 
@@ -218,3 +220,180 @@ class TransferNotificationsTest(PytestOnlyDBTestCase):
218
220
  transfer = transfers[details["id"]]
219
221
  assert details["subject"]["class"] == "dataset"
220
222
  assert details["subject"]["id"] == transfer.subject.id
223
+
224
+
225
+ class TransferRequestNotificationTest(DBTestCase):
226
+ def test_notification_created_for_user_recipient(self):
227
+ """Notification is created for user recipient when transfer is requested"""
228
+ owner = UserFactory()
229
+ recipient = UserFactory()
230
+ dataset = DatasetFactory(owner=owner)
231
+
232
+ login_user(owner)
233
+ transfer = request_transfer(dataset, recipient, faker.sentence())
234
+
235
+ notifications = Notification.objects.all()
236
+ assert len(notifications) == 1
237
+
238
+ notification = notifications[0]
239
+ assert notification.user == recipient
240
+ assert notification.details.transfer_owner == owner
241
+ assert notification.details.transfer_recipient == recipient
242
+ assert notification.details.transfer_subject == dataset
243
+ assert_equal_dates(notification.created_at, transfer.created)
244
+
245
+ def test_notification_created_for_org_admins_only(self):
246
+ """Notifications are created for all admin users of recipient org, not editors"""
247
+ owner = UserFactory()
248
+ admin1 = UserFactory()
249
+ admin2 = UserFactory()
250
+ editor = UserFactory()
251
+ members = [
252
+ Member(user=editor, role="editor"),
253
+ Member(user=admin1, role="admin"),
254
+ Member(user=admin2, role="admin"),
255
+ ]
256
+ org = OrganizationFactory(members=members)
257
+ dataset = DatasetFactory(owner=owner)
258
+
259
+ login_user(owner)
260
+ transfer = request_transfer(dataset, org, faker.sentence())
261
+
262
+ notifications = Notification.objects.all()
263
+ assert len(notifications) == 2
264
+
265
+ admin_users = [notif.user for notif in notifications]
266
+ self.assertIn(admin1, admin_users)
267
+ self.assertIn(admin2, admin_users)
268
+
269
+ for notification in notifications:
270
+ assert notification.details.transfer_owner == owner
271
+ assert notification.details.transfer_recipient == org
272
+ assert notification.details.transfer_subject == dataset
273
+ assert_equal_dates(notification.created_at, transfer.created)
274
+
275
+ def test_no_duplicate_notifications(self):
276
+ """Duplicate notifications are not created for same transfer"""
277
+ owner = UserFactory()
278
+ recipient = UserFactory()
279
+ dataset = DatasetFactory(owner=owner)
280
+
281
+ login_user(owner)
282
+ request_transfer(dataset, recipient, faker.sentence())
283
+ request_transfer(dataset, recipient, faker.sentence())
284
+
285
+ assert Notification.objects.count() == 1
286
+
287
+ def test_multiple_transfers_create_separate_notifications(self):
288
+ """Multiple transfer requests create separate notifications"""
289
+ owner = UserFactory()
290
+ recipient = UserFactory()
291
+ dataset1 = DatasetFactory(owner=owner)
292
+ dataset2 = DatasetFactory(owner=owner)
293
+
294
+ login_user(owner)
295
+ request_transfer(dataset1, recipient, faker.sentence())
296
+ request_transfer(dataset2, recipient, faker.sentence())
297
+
298
+ notifications = Notification.objects.all()
299
+ assert len(notifications) == 2
300
+
301
+ subjects = [notif.details.transfer_subject for notif in notifications]
302
+ self.assertIn(dataset1, subjects)
303
+ self.assertIn(dataset2, subjects)
304
+
305
+ def test_notification_not_created_if_previous_exists(self):
306
+ """Notification is created when transferring from org to user"""
307
+ admin = UserFactory()
308
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
309
+ dataset = DatasetFactory(organization=org)
310
+ recipient = UserFactory()
311
+
312
+ login_user(admin)
313
+ request_transfer(dataset, recipient, faker.sentence())
314
+
315
+ notifications = Notification.objects.all()
316
+ assert len(notifications) == 1
317
+
318
+ request_transfer(dataset, recipient, faker.sentence())
319
+
320
+ notifications = Notification.objects.all()
321
+ assert len(notifications) == 1
322
+
323
+ def test_notification_created_if_previous_handled(self):
324
+ """Notification is created when transferring from org to user"""
325
+ admin = UserFactory()
326
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
327
+ dataset = DatasetFactory(organization=org)
328
+ recipient = UserFactory()
329
+
330
+ login_user(admin)
331
+ transfer = request_transfer(dataset, recipient, faker.sentence())
332
+
333
+ login_user(recipient)
334
+ refuse_transfer(transfer)
335
+
336
+ notifications = Notification.objects.all()
337
+ assert len(notifications) == 1
338
+
339
+ login_user(admin)
340
+ request_transfer(dataset, recipient, faker.sentence())
341
+
342
+ notifications = Notification.objects.all()
343
+ assert len(notifications) == 2
344
+
345
+ def test_notification_created_for_org_to_user_transfer(self):
346
+ """Notification is created when transferring from org to user"""
347
+ admin = UserFactory()
348
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
349
+ dataset = DatasetFactory(organization=org)
350
+ recipient = UserFactory()
351
+
352
+ login_user(admin)
353
+ transfer = request_transfer(dataset, recipient, faker.sentence())
354
+
355
+ notifications = Notification.objects.all()
356
+ assert len(notifications) == 1
357
+
358
+ notification = notifications[0]
359
+ assert notification.user == recipient
360
+ assert notification.details.transfer_owner == org
361
+ assert notification.details.transfer_recipient == recipient
362
+ assert notification.details.transfer_subject == dataset
363
+ assert_equal_dates(notification.created_at, transfer.created)
364
+
365
+ def test_notification_handled_when_transfer_accepted(self):
366
+ """Notification's handled_at is updated when transfer is accepted"""
367
+ owner = UserFactory()
368
+ recipient = UserFactory()
369
+ dataset = DatasetFactory(owner=owner)
370
+
371
+ login_user(owner)
372
+ # First create the notification by requesting the transfer
373
+ transfer = request_transfer(dataset, recipient, faker.sentence())
374
+ # Then accept the transfer
375
+ login_user(recipient)
376
+ accept_transfer(transfer)
377
+
378
+ # Check that the notification has been handled
379
+ notifications = Notification.objects.all()
380
+ assert len(notifications) == 1
381
+ assert notifications[0].handled_at is not None
382
+
383
+ def test_notification_handled_when_transfer_refused(self):
384
+ """Notification's handled_at is updated when transfer is refused"""
385
+ owner = UserFactory()
386
+ recipient = UserFactory()
387
+ dataset = DatasetFactory(owner=owner)
388
+
389
+ login_user(owner)
390
+ # First create the notification by requesting the transfer
391
+ transfer = request_transfer(dataset, recipient, faker.sentence())
392
+ # Then refuse the transfer
393
+ login_user(recipient)
394
+ refuse_transfer(transfer)
395
+
396
+ # Check that the notification has been handled
397
+ notifications = Notification.objects.all()
398
+ assert len(notifications) == 1
399
+ assert notifications[0].handled_at is not None
Binary file