udata 14.0.0__py3-none-any.whl → 14.5.1.dev6__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 (152) hide show
  1. udata/api/__init__.py +2 -0
  2. udata/api_fields.py +35 -4
  3. udata/app.py +18 -20
  4. udata/auth/__init__.py +29 -6
  5. udata/auth/forms.py +2 -2
  6. udata/auth/views.py +13 -6
  7. udata/commands/dcat.py +1 -1
  8. udata/commands/serve.py +3 -11
  9. udata/commands/tests/test_fixtures.py +9 -9
  10. udata/core/access_type/api.py +1 -1
  11. udata/core/access_type/constants.py +12 -8
  12. udata/core/activity/api.py +5 -6
  13. udata/core/badges/tests/test_commands.py +6 -6
  14. udata/core/csv.py +5 -0
  15. udata/core/dataservices/api.py +8 -1
  16. udata/core/dataservices/apiv2.py +2 -5
  17. udata/core/dataservices/models.py +5 -2
  18. udata/core/dataservices/rdf.py +2 -1
  19. udata/core/dataservices/tasks.py +13 -2
  20. udata/core/dataset/api.py +10 -0
  21. udata/core/dataset/models.py +6 -6
  22. udata/core/dataset/permissions.py +31 -0
  23. udata/core/dataset/rdf.py +8 -2
  24. udata/core/dataset/tasks.py +23 -7
  25. udata/core/discussions/api.py +15 -1
  26. udata/core/discussions/models.py +6 -0
  27. udata/core/legal/__init__.py +0 -0
  28. udata/core/legal/mails.py +128 -0
  29. udata/core/organization/api.py +16 -5
  30. udata/core/organization/apiv2.py +2 -3
  31. udata/core/organization/mails.py +1 -1
  32. udata/core/organization/models.py +15 -2
  33. udata/core/organization/notifications.py +84 -0
  34. udata/core/organization/permissions.py +1 -1
  35. udata/core/organization/tasks.py +3 -0
  36. udata/core/pages/tests/test_api.py +32 -0
  37. udata/core/post/api.py +24 -69
  38. udata/core/post/models.py +84 -16
  39. udata/core/post/tests/test_api.py +24 -1
  40. udata/core/reports/api.py +18 -0
  41. udata/core/reports/models.py +42 -2
  42. udata/core/reuse/api.py +8 -0
  43. udata/core/reuse/apiv2.py +2 -5
  44. udata/core/reuse/models.py +1 -1
  45. udata/core/reuse/tasks.py +7 -0
  46. udata/core/spatial/forms.py +2 -2
  47. udata/core/topic/models.py +8 -2
  48. udata/core/user/api.py +10 -3
  49. udata/core/user/models.py +12 -2
  50. udata/features/notifications/api.py +7 -18
  51. udata/features/notifications/models.py +56 -0
  52. udata/features/notifications/tasks.py +25 -0
  53. udata/flask_mongoengine/engine.py +0 -4
  54. udata/flask_mongoengine/pagination.py +1 -1
  55. udata/frontend/markdown.py +2 -1
  56. udata/harvest/actions.py +21 -1
  57. udata/harvest/api.py +25 -8
  58. udata/harvest/backends/base.py +27 -1
  59. udata/harvest/backends/ckan/harvesters.py +11 -2
  60. udata/harvest/backends/dcat.py +4 -1
  61. udata/harvest/commands.py +33 -0
  62. udata/harvest/filters.py +17 -6
  63. udata/harvest/models.py +16 -0
  64. udata/harvest/permissions.py +27 -0
  65. udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
  66. udata/harvest/tests/test_actions.py +58 -5
  67. udata/harvest/tests/test_api.py +276 -122
  68. udata/harvest/tests/test_base_backend.py +86 -1
  69. udata/harvest/tests/test_dcat_backend.py +81 -10
  70. udata/harvest/tests/test_filters.py +6 -0
  71. udata/i18n.py +1 -4
  72. udata/mail.py +19 -1
  73. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  74. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  75. udata/mongo/slug_fields.py +1 -1
  76. udata/rdf.py +58 -10
  77. udata/routing.py +2 -2
  78. udata/settings.py +11 -0
  79. udata/tasks.py +1 -0
  80. udata/templates/mail/message.html +5 -31
  81. udata/tests/__init__.py +27 -2
  82. udata/tests/api/__init__.py +108 -21
  83. udata/tests/api/test_activities_api.py +36 -0
  84. udata/tests/api/test_auth_api.py +121 -95
  85. udata/tests/api/test_base_api.py +7 -4
  86. udata/tests/api/test_datasets_api.py +50 -19
  87. udata/tests/api/test_organizations_api.py +192 -197
  88. udata/tests/api/test_reports_api.py +157 -0
  89. udata/tests/api/test_reuses_api.py +147 -147
  90. udata/tests/api/test_security_api.py +12 -12
  91. udata/tests/api/test_swagger.py +4 -4
  92. udata/tests/api/test_tags_api.py +8 -8
  93. udata/tests/api/test_user_api.py +1 -1
  94. udata/tests/apiv2/test_search.py +30 -0
  95. udata/tests/apiv2/test_swagger.py +4 -4
  96. udata/tests/cli/test_cli_base.py +8 -9
  97. udata/tests/dataservice/test_dataservice_tasks.py +29 -0
  98. udata/tests/dataset/test_dataset_commands.py +4 -4
  99. udata/tests/dataset/test_dataset_model.py +66 -26
  100. udata/tests/dataset/test_dataset_rdf.py +99 -5
  101. udata/tests/dataset/test_dataset_tasks.py +25 -0
  102. udata/tests/frontend/test_auth.py +58 -1
  103. udata/tests/frontend/test_csv.py +0 -3
  104. udata/tests/helpers.py +31 -27
  105. udata/tests/organization/test_notifications.py +67 -2
  106. udata/tests/plugin.py +6 -261
  107. udata/tests/search/test_search_integration.py +33 -0
  108. udata/tests/site/test_site_csv_exports.py +22 -10
  109. udata/tests/test_activity.py +9 -9
  110. udata/tests/test_api_fields.py +10 -0
  111. udata/tests/test_dcat_commands.py +2 -2
  112. udata/tests/test_discussions.py +5 -5
  113. udata/tests/test_legal_mails.py +359 -0
  114. udata/tests/test_migrations.py +21 -21
  115. udata/tests/test_notifications.py +15 -57
  116. udata/tests/test_notifications_task.py +43 -0
  117. udata/tests/test_owned.py +81 -1
  118. udata/tests/test_storages.py +25 -19
  119. udata/tests/test_topics.py +77 -61
  120. udata/tests/test_uris.py +33 -0
  121. udata/tests/workers/test_jobs_commands.py +23 -23
  122. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  123. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  124. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  125. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  126. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  127. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  128. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  129. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  130. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  131. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  132. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  133. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  134. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  135. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  136. udata/translations/udata.pot +215 -106
  137. udata/uris.py +0 -2
  138. udata-14.5.1.dev6.dist-info/METADATA +109 -0
  139. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/RECORD +143 -140
  140. udata/core/post/forms.py +0 -30
  141. udata/flask_mongoengine/json.py +0 -38
  142. udata/templates/mail/base.html +0 -105
  143. udata/templates/mail/base.txt +0 -6
  144. udata/templates/mail/button.html +0 -3
  145. udata/templates/mail/layouts/1-column.html +0 -19
  146. udata/templates/mail/layouts/2-columns.html +0 -20
  147. udata/templates/mail/layouts/center-panel.html +0 -16
  148. udata-14.0.0.dist-info/METADATA +0 -132
  149. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/WHEEL +0 -0
  150. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/entry_points.txt +0 -0
  151. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/licenses/LICENSE +0 -0
  152. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/top_level.txt +0 -0
@@ -27,7 +27,7 @@ class FakeFormAPI(API):
27
27
  class OptionsCORSTest(APITestCase):
28
28
  def test_should_allow_options_and_cors(self):
29
29
  """Should allow OPTIONS operation and give cors parameters"""
30
- response = self.client.options(
30
+ response = self.options(
31
31
  url_for("api.fake-options"),
32
32
  headers={
33
33
  "Origin": "http://localhost",
@@ -38,7 +38,7 @@ class OptionsCORSTest(APITestCase):
38
38
  self.assert204(response)
39
39
  self.assertEqual(response.headers["Access-Control-Allow-Origin"], "http://localhost")
40
40
 
41
- response = self.client.options(
41
+ response = self.options(
42
42
  url_for("api.fake-options"),
43
43
  headers={
44
44
  "Origin": "http://localhost",
@@ -55,8 +55,11 @@ class OptionsCORSTest(APITestCase):
55
55
  class JSONFormRequestTest(APITestCase):
56
56
  def test_non_json_content_type(self):
57
57
  """We expect JSON requests for forms and enforce it"""
58
- response = self.client.post(
59
- url_for("api.fake-form"), {}, headers={"Content-Type": "multipart/form-data"}
58
+ response = self.post(
59
+ url_for("api.fake-form"),
60
+ {},
61
+ json=False,
62
+ headers={"Content-Type": "multipart/form-data"},
60
63
  )
61
64
  self.assert400(response)
62
65
  self.assertEqual(response.json, {"errors": {"Content-Type": "expecting application/json"}})
@@ -724,6 +724,19 @@ class DatasetAPITest(APITestCase):
724
724
  dataset = Dataset.objects.first()
725
725
  self.assertEqual(dataset.spatial.geom, SAMPLE_GEOM)
726
726
 
727
+ def test_dataset_api_create_with_invalid_geom_coordinates(self):
728
+ """It should return 400 with invalid GeoJSON coordinates, not 500"""
729
+ self.login()
730
+ data = DatasetFactory.as_dict()
731
+ # Invalid GeoJSON: {} in coordinates instead of numbers (Sentry issue)
732
+ data["spatial"] = {"geom": {"type": "Point", "coordinates": {}}}
733
+ response = self.post(url_for("api.datasets"), data)
734
+ self.assert400(response)
735
+ self.assertEqual(Dataset.objects.count(), 0)
736
+ # Verify error is properly captured in form validation errors
737
+ self.assertIn("errors", response.json)
738
+ self.assertIn("spatial", response.json["errors"])
739
+
727
740
  def test_dataset_api_create_with_legacy_frequency(self):
728
741
  """It should create a dataset from the API with a legacy frequency"""
729
742
  self.login()
@@ -1693,6 +1706,12 @@ class DatasetResourceAPITest(APITestCase):
1693
1706
  self.dataset.reload()
1694
1707
  self.assertEqual(len(self.dataset.resources), 2)
1695
1708
 
1709
+ def test_create_with_list_returns_400(self):
1710
+ """It should return 400 when sending a list instead of a dict"""
1711
+ data = [ResourceFactory.as_dict()]
1712
+ response = self.post(url_for("api.resources", dataset=self.dataset), data)
1713
+ self.assert400(response)
1714
+
1696
1715
  def test_create_with_file(self):
1697
1716
  """It should create a resource from the API with a file"""
1698
1717
  user = self.login()
@@ -1834,6 +1853,18 @@ class DatasetResourceAPITest(APITestCase):
1834
1853
  f"Resource ids must match existing ones in dataset, ie: {set(str(r.id) for r in self.dataset.resources)}",
1835
1854
  )
1836
1855
 
1856
+ def test_invalid_reorder_dict_without_id(self):
1857
+ """It should return 400 when dict in resources list has no 'id' key"""
1858
+ self.dataset.resources = ResourceFactory.build_batch(3)
1859
+ self.dataset.save()
1860
+
1861
+ # Dict without 'id' key should fail gracefully, not raise KeyError
1862
+ wrong_order_dict_without_id = [{"title": "foo"}, {"title": "bar"}, {"title": "baz"}]
1863
+ response = self.put(
1864
+ url_for("api.resources", dataset=self.dataset), wrong_order_dict_without_id
1865
+ )
1866
+ self.assertStatus(response, 400)
1867
+
1837
1868
  def test_update_local(self):
1838
1869
  resource = ResourceFactory()
1839
1870
  self.dataset.resources.append(resource)
@@ -2506,13 +2537,13 @@ class ResourcesTypesAPITest(APITestCase):
2506
2537
 
2507
2538
 
2508
2539
  class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2509
- def test_dataset_schemas_api_list(self, api, rmock, app):
2540
+ def test_dataset_schemas_api_list(self, rmock, app):
2510
2541
  # Can't use @pytest.mark.options otherwise a request will be
2511
2542
  # made before setting up rmock at module load, resulting in a 404
2512
2543
  app.config["SCHEMA_CATALOG_URL"] = "https://example.com/schemas"
2513
2544
 
2514
2545
  rmock.get("https://example.com/schemas", json=ResourceSchemaMockData.get_mock_data())
2515
- response = api.get(url_for("api.schemas"))
2546
+ response = self.get(url_for("api.schemas"))
2516
2547
 
2517
2548
  assert200(response)
2518
2549
  assert (
@@ -2520,27 +2551,27 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2520
2551
  )
2521
2552
 
2522
2553
  @pytest.mark.options(SCHEMA_CATALOG_URL=None)
2523
- def test_dataset_schemas_api_list_no_catalog_url(self, api):
2524
- response = api.get(url_for("api.schemas"))
2554
+ def test_dataset_schemas_api_list_no_catalog_url(self):
2555
+ response = self.get(url_for("api.schemas"))
2525
2556
 
2526
2557
  assert200(response)
2527
2558
  assert response.json == []
2528
2559
 
2529
2560
  @pytest.mark.options(SCHEMA_CATALOG_URL="https://example.com/notfound")
2530
- def test_dataset_schemas_api_list_not_found(self, api, rmock):
2561
+ def test_dataset_schemas_api_list_not_found(self, rmock):
2531
2562
  rmock.get("https://example.com/notfound", status_code=404)
2532
- response = api.get(url_for("api.schemas"))
2563
+ response = self.get(url_for("api.schemas"))
2533
2564
  assert404(response)
2534
2565
 
2535
2566
  @pytest.mark.options(SCHEMA_CATALOG_URL="https://example.com/schemas")
2536
- def test_dataset_schemas_api_list_error_no_cache(self, api, rmock):
2567
+ def test_dataset_schemas_api_list_error_no_cache(self, rmock):
2537
2568
  rmock.get("https://example.com/schemas", status_code=500)
2538
2569
 
2539
- response = api.get(url_for("api.schemas"))
2570
+ response = self.get(url_for("api.schemas"))
2540
2571
  assert response.status_code == 503
2541
2572
 
2542
2573
  @pytest.mark.options(SCHEMA_CATALOG_URL="https://example.com/schemas")
2543
- def test_dataset_schemas_api_list_error_w_cache(self, api, rmock, mocker):
2574
+ def test_dataset_schemas_api_list_error_w_cache(self, rmock, mocker):
2544
2575
  cache_mock_set = mocker.patch.object(cache, "set")
2545
2576
  mocker.patch.object(
2546
2577
  cache, "get", return_value=ResourceSchemaMockData.get_mock_data()["schemas"]
@@ -2548,7 +2579,7 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2548
2579
 
2549
2580
  # Fill cache
2550
2581
  rmock.get("https://example.com/schemas", json=ResourceSchemaMockData.get_mock_data())
2551
- response = api.get(url_for("api.schemas"))
2582
+ response = self.get(url_for("api.schemas"))
2552
2583
  assert200(response)
2553
2584
  assert (
2554
2585
  response.json == ResourceSchemaMockData.get_expected_assignable_schemas_from_mock_data()
@@ -2559,7 +2590,7 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2559
2590
  rmock.get("https://example.com/schemas", status_code=500)
2560
2591
 
2561
2592
  # Long term cache is used
2562
- response = api.get(url_for("api.schemas"))
2593
+ response = self.get(url_for("api.schemas"))
2563
2594
  assert200(response)
2564
2595
  assert (
2565
2596
  response.json == ResourceSchemaMockData.get_expected_assignable_schemas_from_mock_data()
@@ -2567,7 +2598,7 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2567
2598
 
2568
2599
 
2569
2600
  class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2570
- def test_dataset_with_harvest_metadata(self, api):
2601
+ def test_dataset_with_harvest_metadata(self):
2571
2602
  date = datetime(2022, 2, 22, tzinfo=pytz.UTC)
2572
2603
  harvest_metadata = HarvestDatasetMetadata(
2573
2604
  backend="DCAT",
@@ -2585,7 +2616,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2585
2616
  )
2586
2617
  dataset = DatasetFactory(harvest=harvest_metadata)
2587
2618
 
2588
- response = api.get(url_for("api.dataset", dataset=dataset))
2619
+ response = self.get(url_for("api.dataset", dataset=dataset))
2589
2620
  assert200(response)
2590
2621
  assert response.json["harvest"] == {
2591
2622
  "backend": "DCAT",
@@ -2602,7 +2633,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2602
2633
  "archived": "not-on-remote",
2603
2634
  }
2604
2635
 
2605
- def test_dataset_with_resource_harvest_metadata(self, api):
2636
+ def test_dataset_with_resource_harvest_metadata(self):
2606
2637
  date = datetime(2022, 2, 22, tzinfo=pytz.UTC)
2607
2638
 
2608
2639
  harvest_metadata = HarvestResourceMetadata(
@@ -2612,7 +2643,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2612
2643
  )
2613
2644
  dataset = DatasetFactory(resources=[ResourceFactory(harvest=harvest_metadata)])
2614
2645
 
2615
- response = api.get(url_for("api.dataset", dataset=dataset))
2646
+ response = self.get(url_for("api.dataset", dataset=dataset))
2616
2647
  assert200(response)
2617
2648
  assert response.json["resources"][0]["harvest"] == {
2618
2649
  "issued_at": date.isoformat(),
@@ -2620,7 +2651,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2620
2651
  "uri": "http://domain.gouv.fr/dataset/uri",
2621
2652
  }
2622
2653
 
2623
- def test_dataset_with_harvest_computed_dates(self, api):
2654
+ def test_dataset_with_harvest_computed_dates(self):
2624
2655
  # issued_date takes precedence over internal creation date and harvest created_at on dataset
2625
2656
  issued_date = datetime(2022, 2, 22, tzinfo=pytz.UTC)
2626
2657
  creation_date = datetime(2022, 2, 23, tzinfo=pytz.UTC)
@@ -2631,7 +2662,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2631
2662
  modified_at=modification_date,
2632
2663
  )
2633
2664
  dataset = DatasetFactory(harvest=harvest_metadata)
2634
- response = api.get(url_for("api.dataset", dataset=dataset))
2665
+ response = self.get(url_for("api.dataset", dataset=dataset))
2635
2666
  assert200(response)
2636
2667
  assert response.json["created_at"] == issued_date.isoformat()
2637
2668
  assert response.json["last_modified"] == modification_date.isoformat()
@@ -2643,7 +2674,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2643
2674
  modified_at=modification_date,
2644
2675
  )
2645
2676
  dataset = DatasetFactory(harvest=harvest_metadata)
2646
- response = api.get(url_for("api.dataset", dataset=dataset))
2677
+ response = self.get(url_for("api.dataset", dataset=dataset))
2647
2678
  assert200(response)
2648
2679
  assert response.json["created_at"] == creation_date.isoformat()
2649
2680
  assert response.json["last_modified"] == modification_date.isoformat()
@@ -2654,7 +2685,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2654
2685
  modified_at=modification_date,
2655
2686
  )
2656
2687
  dataset = DatasetFactory(resources=[ResourceFactory(harvest=resource_harvest_metadata)])
2657
- response = api.get(url_for("api.dataset", dataset=dataset))
2688
+ response = self.get(url_for("api.dataset", dataset=dataset))
2658
2689
  assert200(response)
2659
2690
  assert response.json["resources"][0]["created_at"] == issued_date.isoformat()
2660
2691
  assert response.json["resources"][0]["last_modified"] == modification_date.isoformat()