udata 14.0.0__py3-none-any.whl → 14.4.1.dev7__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 (130) hide show
  1. udata/api_fields.py +35 -4
  2. udata/app.py +18 -20
  3. udata/auth/__init__.py +29 -6
  4. udata/auth/forms.py +2 -2
  5. udata/auth/views.py +6 -3
  6. udata/commands/serve.py +3 -11
  7. udata/commands/tests/test_fixtures.py +9 -9
  8. udata/core/access_type/api.py +1 -1
  9. udata/core/access_type/constants.py +12 -8
  10. udata/core/activity/api.py +5 -6
  11. udata/core/badges/tests/test_commands.py +6 -6
  12. udata/core/csv.py +5 -0
  13. udata/core/dataservices/models.py +1 -1
  14. udata/core/dataservices/tasks.py +7 -0
  15. udata/core/dataset/api.py +2 -0
  16. udata/core/dataset/models.py +2 -2
  17. udata/core/dataset/permissions.py +31 -0
  18. udata/core/dataset/tasks.py +17 -5
  19. udata/core/discussions/models.py +1 -0
  20. udata/core/organization/api.py +8 -5
  21. udata/core/organization/mails.py +1 -1
  22. udata/core/organization/models.py +9 -1
  23. udata/core/organization/notifications.py +84 -0
  24. udata/core/organization/permissions.py +1 -1
  25. udata/core/organization/tasks.py +3 -0
  26. udata/core/pages/tests/test_api.py +32 -0
  27. udata/core/post/api.py +24 -69
  28. udata/core/post/models.py +84 -16
  29. udata/core/post/tests/test_api.py +24 -1
  30. udata/core/reports/api.py +18 -0
  31. udata/core/reports/models.py +42 -2
  32. udata/core/reuse/models.py +1 -1
  33. udata/core/reuse/tasks.py +7 -0
  34. udata/core/spatial/forms.py +2 -2
  35. udata/core/user/models.py +5 -1
  36. udata/features/notifications/api.py +7 -18
  37. udata/features/notifications/models.py +56 -0
  38. udata/features/notifications/tasks.py +25 -0
  39. udata/flask_mongoengine/engine.py +0 -4
  40. udata/frontend/markdown.py +2 -1
  41. udata/harvest/actions.py +21 -1
  42. udata/harvest/api.py +25 -8
  43. udata/harvest/backends/base.py +27 -1
  44. udata/harvest/backends/ckan/harvesters.py +11 -2
  45. udata/harvest/commands.py +33 -0
  46. udata/harvest/filters.py +17 -6
  47. udata/harvest/models.py +16 -0
  48. udata/harvest/permissions.py +27 -0
  49. udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
  50. udata/harvest/tests/test_actions.py +58 -5
  51. udata/harvest/tests/test_api.py +276 -122
  52. udata/harvest/tests/test_base_backend.py +86 -1
  53. udata/harvest/tests/test_dcat_backend.py +57 -10
  54. udata/harvest/tests/test_filters.py +6 -0
  55. udata/i18n.py +1 -4
  56. udata/mail.py +5 -1
  57. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  58. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  59. udata/mongo/slug_fields.py +1 -1
  60. udata/rdf.py +45 -6
  61. udata/routing.py +2 -2
  62. udata/settings.py +7 -0
  63. udata/tasks.py +1 -0
  64. udata/templates/mail/message.html +5 -31
  65. udata/tests/__init__.py +27 -2
  66. udata/tests/api/__init__.py +108 -21
  67. udata/tests/api/test_activities_api.py +36 -0
  68. udata/tests/api/test_auth_api.py +121 -95
  69. udata/tests/api/test_base_api.py +7 -4
  70. udata/tests/api/test_datasets_api.py +44 -19
  71. udata/tests/api/test_organizations_api.py +192 -197
  72. udata/tests/api/test_reports_api.py +157 -0
  73. udata/tests/api/test_reuses_api.py +147 -147
  74. udata/tests/api/test_security_api.py +12 -12
  75. udata/tests/api/test_swagger.py +4 -4
  76. udata/tests/api/test_tags_api.py +8 -8
  77. udata/tests/api/test_user_api.py +1 -1
  78. udata/tests/apiv2/test_swagger.py +4 -4
  79. udata/tests/cli/test_cli_base.py +8 -9
  80. udata/tests/dataset/test_dataset_commands.py +4 -4
  81. udata/tests/dataset/test_dataset_model.py +66 -26
  82. udata/tests/dataset/test_dataset_rdf.py +99 -5
  83. udata/tests/frontend/test_auth.py +24 -1
  84. udata/tests/frontend/test_csv.py +0 -3
  85. udata/tests/helpers.py +25 -27
  86. udata/tests/organization/test_notifications.py +67 -2
  87. udata/tests/plugin.py +6 -261
  88. udata/tests/site/test_site_csv_exports.py +22 -10
  89. udata/tests/test_activity.py +9 -9
  90. udata/tests/test_dcat_commands.py +2 -2
  91. udata/tests/test_discussions.py +5 -5
  92. udata/tests/test_migrations.py +21 -21
  93. udata/tests/test_notifications.py +15 -57
  94. udata/tests/test_notifications_task.py +43 -0
  95. udata/tests/test_owned.py +81 -1
  96. udata/tests/test_storages.py +25 -19
  97. udata/tests/test_topics.py +77 -61
  98. udata/tests/test_uris.py +33 -0
  99. udata/tests/workers/test_jobs_commands.py +23 -23
  100. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  101. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  102. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  103. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  104. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  105. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  106. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  107. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  108. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  109. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  110. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  111. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  112. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  113. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  114. udata/translations/udata.pot +215 -106
  115. udata/uris.py +0 -2
  116. udata-14.4.1.dev7.dist-info/METADATA +109 -0
  117. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +121 -123
  118. udata/core/post/forms.py +0 -30
  119. udata/flask_mongoengine/json.py +0 -38
  120. udata/templates/mail/base.html +0 -105
  121. udata/templates/mail/base.txt +0 -6
  122. udata/templates/mail/button.html +0 -3
  123. udata/templates/mail/layouts/1-column.html +0 -19
  124. udata/templates/mail/layouts/2-columns.html +0 -20
  125. udata/templates/mail/layouts/center-panel.html +0 -16
  126. udata-14.0.0.dist-info/METADATA +0 -132
  127. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
  128. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +0 -0
  129. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
  130. {udata-14.0.0.dist-info → udata-14.4.1.dev7.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()
@@ -1834,6 +1847,18 @@ class DatasetResourceAPITest(APITestCase):
1834
1847
  f"Resource ids must match existing ones in dataset, ie: {set(str(r.id) for r in self.dataset.resources)}",
1835
1848
  )
1836
1849
 
1850
+ def test_invalid_reorder_dict_without_id(self):
1851
+ """It should return 400 when dict in resources list has no 'id' key"""
1852
+ self.dataset.resources = ResourceFactory.build_batch(3)
1853
+ self.dataset.save()
1854
+
1855
+ # Dict without 'id' key should fail gracefully, not raise KeyError
1856
+ wrong_order_dict_without_id = [{"title": "foo"}, {"title": "bar"}, {"title": "baz"}]
1857
+ response = self.put(
1858
+ url_for("api.resources", dataset=self.dataset), wrong_order_dict_without_id
1859
+ )
1860
+ self.assertStatus(response, 400)
1861
+
1837
1862
  def test_update_local(self):
1838
1863
  resource = ResourceFactory()
1839
1864
  self.dataset.resources.append(resource)
@@ -2506,13 +2531,13 @@ class ResourcesTypesAPITest(APITestCase):
2506
2531
 
2507
2532
 
2508
2533
  class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2509
- def test_dataset_schemas_api_list(self, api, rmock, app):
2534
+ def test_dataset_schemas_api_list(self, rmock, app):
2510
2535
  # Can't use @pytest.mark.options otherwise a request will be
2511
2536
  # made before setting up rmock at module load, resulting in a 404
2512
2537
  app.config["SCHEMA_CATALOG_URL"] = "https://example.com/schemas"
2513
2538
 
2514
2539
  rmock.get("https://example.com/schemas", json=ResourceSchemaMockData.get_mock_data())
2515
- response = api.get(url_for("api.schemas"))
2540
+ response = self.get(url_for("api.schemas"))
2516
2541
 
2517
2542
  assert200(response)
2518
2543
  assert (
@@ -2520,27 +2545,27 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2520
2545
  )
2521
2546
 
2522
2547
  @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"))
2548
+ def test_dataset_schemas_api_list_no_catalog_url(self):
2549
+ response = self.get(url_for("api.schemas"))
2525
2550
 
2526
2551
  assert200(response)
2527
2552
  assert response.json == []
2528
2553
 
2529
2554
  @pytest.mark.options(SCHEMA_CATALOG_URL="https://example.com/notfound")
2530
- def test_dataset_schemas_api_list_not_found(self, api, rmock):
2555
+ def test_dataset_schemas_api_list_not_found(self, rmock):
2531
2556
  rmock.get("https://example.com/notfound", status_code=404)
2532
- response = api.get(url_for("api.schemas"))
2557
+ response = self.get(url_for("api.schemas"))
2533
2558
  assert404(response)
2534
2559
 
2535
2560
  @pytest.mark.options(SCHEMA_CATALOG_URL="https://example.com/schemas")
2536
- def test_dataset_schemas_api_list_error_no_cache(self, api, rmock):
2561
+ def test_dataset_schemas_api_list_error_no_cache(self, rmock):
2537
2562
  rmock.get("https://example.com/schemas", status_code=500)
2538
2563
 
2539
- response = api.get(url_for("api.schemas"))
2564
+ response = self.get(url_for("api.schemas"))
2540
2565
  assert response.status_code == 503
2541
2566
 
2542
2567
  @pytest.mark.options(SCHEMA_CATALOG_URL="https://example.com/schemas")
2543
- def test_dataset_schemas_api_list_error_w_cache(self, api, rmock, mocker):
2568
+ def test_dataset_schemas_api_list_error_w_cache(self, rmock, mocker):
2544
2569
  cache_mock_set = mocker.patch.object(cache, "set")
2545
2570
  mocker.patch.object(
2546
2571
  cache, "get", return_value=ResourceSchemaMockData.get_mock_data()["schemas"]
@@ -2548,7 +2573,7 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2548
2573
 
2549
2574
  # Fill cache
2550
2575
  rmock.get("https://example.com/schemas", json=ResourceSchemaMockData.get_mock_data())
2551
- response = api.get(url_for("api.schemas"))
2576
+ response = self.get(url_for("api.schemas"))
2552
2577
  assert200(response)
2553
2578
  assert (
2554
2579
  response.json == ResourceSchemaMockData.get_expected_assignable_schemas_from_mock_data()
@@ -2559,7 +2584,7 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2559
2584
  rmock.get("https://example.com/schemas", status_code=500)
2560
2585
 
2561
2586
  # Long term cache is used
2562
- response = api.get(url_for("api.schemas"))
2587
+ response = self.get(url_for("api.schemas"))
2563
2588
  assert200(response)
2564
2589
  assert (
2565
2590
  response.json == ResourceSchemaMockData.get_expected_assignable_schemas_from_mock_data()
@@ -2567,7 +2592,7 @@ class DatasetSchemasAPITest(PytestOnlyAPITestCase):
2567
2592
 
2568
2593
 
2569
2594
  class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2570
- def test_dataset_with_harvest_metadata(self, api):
2595
+ def test_dataset_with_harvest_metadata(self):
2571
2596
  date = datetime(2022, 2, 22, tzinfo=pytz.UTC)
2572
2597
  harvest_metadata = HarvestDatasetMetadata(
2573
2598
  backend="DCAT",
@@ -2585,7 +2610,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2585
2610
  )
2586
2611
  dataset = DatasetFactory(harvest=harvest_metadata)
2587
2612
 
2588
- response = api.get(url_for("api.dataset", dataset=dataset))
2613
+ response = self.get(url_for("api.dataset", dataset=dataset))
2589
2614
  assert200(response)
2590
2615
  assert response.json["harvest"] == {
2591
2616
  "backend": "DCAT",
@@ -2602,7 +2627,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2602
2627
  "archived": "not-on-remote",
2603
2628
  }
2604
2629
 
2605
- def test_dataset_with_resource_harvest_metadata(self, api):
2630
+ def test_dataset_with_resource_harvest_metadata(self):
2606
2631
  date = datetime(2022, 2, 22, tzinfo=pytz.UTC)
2607
2632
 
2608
2633
  harvest_metadata = HarvestResourceMetadata(
@@ -2612,7 +2637,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2612
2637
  )
2613
2638
  dataset = DatasetFactory(resources=[ResourceFactory(harvest=harvest_metadata)])
2614
2639
 
2615
- response = api.get(url_for("api.dataset", dataset=dataset))
2640
+ response = self.get(url_for("api.dataset", dataset=dataset))
2616
2641
  assert200(response)
2617
2642
  assert response.json["resources"][0]["harvest"] == {
2618
2643
  "issued_at": date.isoformat(),
@@ -2620,7 +2645,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2620
2645
  "uri": "http://domain.gouv.fr/dataset/uri",
2621
2646
  }
2622
2647
 
2623
- def test_dataset_with_harvest_computed_dates(self, api):
2648
+ def test_dataset_with_harvest_computed_dates(self):
2624
2649
  # issued_date takes precedence over internal creation date and harvest created_at on dataset
2625
2650
  issued_date = datetime(2022, 2, 22, tzinfo=pytz.UTC)
2626
2651
  creation_date = datetime(2022, 2, 23, tzinfo=pytz.UTC)
@@ -2631,7 +2656,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2631
2656
  modified_at=modification_date,
2632
2657
  )
2633
2658
  dataset = DatasetFactory(harvest=harvest_metadata)
2634
- response = api.get(url_for("api.dataset", dataset=dataset))
2659
+ response = self.get(url_for("api.dataset", dataset=dataset))
2635
2660
  assert200(response)
2636
2661
  assert response.json["created_at"] == issued_date.isoformat()
2637
2662
  assert response.json["last_modified"] == modification_date.isoformat()
@@ -2643,7 +2668,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2643
2668
  modified_at=modification_date,
2644
2669
  )
2645
2670
  dataset = DatasetFactory(harvest=harvest_metadata)
2646
- response = api.get(url_for("api.dataset", dataset=dataset))
2671
+ response = self.get(url_for("api.dataset", dataset=dataset))
2647
2672
  assert200(response)
2648
2673
  assert response.json["created_at"] == creation_date.isoformat()
2649
2674
  assert response.json["last_modified"] == modification_date.isoformat()
@@ -2654,7 +2679,7 @@ class HarvestMetadataAPITest(PytestOnlyAPITestCase):
2654
2679
  modified_at=modification_date,
2655
2680
  )
2656
2681
  dataset = DatasetFactory(resources=[ResourceFactory(harvest=resource_harvest_metadata)])
2657
- response = api.get(url_for("api.dataset", dataset=dataset))
2682
+ response = self.get(url_for("api.dataset", dataset=dataset))
2658
2683
  assert200(response)
2659
2684
  assert response.json["resources"][0]["created_at"] == issued_date.isoformat()
2660
2685
  assert response.json["resources"][0]["last_modified"] == modification_date.isoformat()