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
@@ -0,0 +1,27 @@
1
+ from udata.auth import Permission, UserNeed
2
+ from udata.core.dataset.permissions import OwnablePermission
3
+ from udata.core.organization.permissions import OrganizationAdminNeed
4
+
5
+
6
+ class HarvestSourcePermission(OwnablePermission):
7
+ """Permission for basic harvest source operations (preview)
8
+ Allows organization admins, editors, or owner.
9
+ """
10
+
11
+ pass
12
+
13
+
14
+ class HarvestSourceAdminPermission(Permission):
15
+ """Permission for sensitive harvest source operations (edit, delete, run)
16
+ Allows only organization admins or owner (not editors).
17
+ """
18
+
19
+ def __init__(self, source) -> None:
20
+ needs = []
21
+
22
+ if source.organization:
23
+ needs.append(OrganizationAdminNeed(source.organization.id))
24
+ elif source.owner:
25
+ needs.append(UserNeed(source.owner.fs_uniquifier))
26
+
27
+ super(HarvestSourceAdminPermission, self).__init__(*needs)
@@ -200,6 +200,24 @@ def spatial_geom_multipolygon(resource_data):
200
200
  return data, {"multipolygon": multipolygon}
201
201
 
202
202
 
203
+ @pytest.fixture
204
+ def spatial_geom_polygon_as_dict(resource_data):
205
+ """
206
+ Test case where extra["value"] is already a dict in CKAN (e.g., datasud.fr).
207
+ In some CKAN instances, the spatial value is returned as a dict directly
208
+ instead of a JSON string, so json.loads() would fail.
209
+ """
210
+ polygon = faker.polygon()
211
+ data = {
212
+ "name": faker.unique_string(),
213
+ "title": faker.sentence(),
214
+ "notes": faker.paragraph(),
215
+ "resources": [resource_data],
216
+ "extras": [{"key": "spatial", "value": polygon}],
217
+ }
218
+ return data, {"polygon": polygon}
219
+
220
+
203
221
  @pytest.fixture
204
222
  def known_spatial_text_name(resource_data):
205
223
  zone = GeoZoneFactory()
@@ -422,6 +440,21 @@ class CkanBackendTest(PytestOnlyDBTestCase):
422
440
  dataset = dataset_for(result)
423
441
  assert dataset.spatial.geom == multipolygon
424
442
 
443
+ @pytest.mark.ckan_data("spatial_geom_polygon_as_dict")
444
+ def test_geospatial_geom_polygon_as_dict(self, result, kwargs):
445
+ """
446
+ Test that spatial geometry works when the value is already a dict.
447
+ Some CKAN instances (e.g., datasud.fr) return the spatial value as a dict
448
+ directly instead of a JSON string.
449
+ """
450
+ polygon = kwargs["polygon"]
451
+ dataset = dataset_for(result)
452
+
453
+ assert dataset.spatial.geom == {
454
+ "type": "MultiPolygon",
455
+ "coordinates": [polygon["coordinates"]],
456
+ }
457
+
425
458
  @pytest.mark.ckan_data("skipped_no_resources")
426
459
  def test_skip_no_resources(self, source, result):
427
460
  job = source.get_last_job()
@@ -11,8 +11,8 @@ from udata.core.activity.models import new_activity
11
11
  from udata.core.dataservices.factories import DataserviceFactory
12
12
  from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
13
13
  from udata.core.dataset.activities import UserCreatedDataset
14
- from udata.core.dataset.factories import DatasetFactory
15
- from udata.core.dataset.models import HarvestDatasetMetadata
14
+ from udata.core.dataset.factories import DatasetFactory, ResourceFactory
15
+ from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
16
16
  from udata.core.organization.factories import OrganizationFactory
17
17
  from udata.core.user.factories import UserFactory
18
18
  from udata.harvest.backends import get_enabled_backends
@@ -271,7 +271,16 @@ class HarvestActionsTest(MockBackendsMixin, PytestOnlyDBTestCase):
271
271
  assert periodic_task.crontab.day_of_month == "*"
272
272
  assert periodic_task.crontab.month_of_year == "*"
273
273
  assert periodic_task.enabled
274
- assert periodic_task.name == "Harvest {0}".format(source.name)
274
+ assert periodic_task.name == f"Harvest {source.name} ({source.id})"
275
+
276
+ def test_double_schedule_with_same_name(self):
277
+ source_1 = HarvestSourceFactory(name="A")
278
+ source_2 = HarvestSourceFactory(name="A")
279
+
280
+ actions.schedule(source_1, hour=0)
281
+ actions.schedule(source_2, hour=0)
282
+
283
+ assert len(PeriodicTask.objects) == 2
275
284
 
276
285
  def test_schedule_from_cron(self):
277
286
  source = HarvestSourceFactory()
@@ -288,7 +297,7 @@ class HarvestActionsTest(MockBackendsMixin, PytestOnlyDBTestCase):
288
297
  assert periodic_task.crontab.month_of_year == "3"
289
298
  assert periodic_task.crontab.day_of_week == "sunday"
290
299
  assert periodic_task.enabled
291
- assert periodic_task.name == "Harvest {0}".format(source.name)
300
+ assert periodic_task.name == f"Harvest {source.name} ({source.id})"
292
301
 
293
302
  def test_reschedule(self):
294
303
  source = HarvestSourceFactory()
@@ -308,7 +317,7 @@ class HarvestActionsTest(MockBackendsMixin, PytestOnlyDBTestCase):
308
317
  assert periodic_task.crontab.day_of_month == "*"
309
318
  assert periodic_task.crontab.month_of_year == "*"
310
319
  assert periodic_task.enabled
311
- assert periodic_task.name == "Harvest {0}".format(source.name)
320
+ assert periodic_task.name == f"Harvest {source.name} ({source.id})"
312
321
 
313
322
  def test_unschedule(self):
314
323
  periodic_task = PeriodicTask.objects.create(
@@ -451,6 +460,50 @@ class HarvestActionsTest(MockBackendsMixin, PytestOnlyDBTestCase):
451
460
  assert result.success == len(datasets)
452
461
  assert result.errors == 1
453
462
 
463
+ def test_detach(self):
464
+ dataset = DatasetFactory(
465
+ harvest=HarvestDatasetMetadata(
466
+ source_id="source id", domain="test.org", remote_id="id"
467
+ ),
468
+ resources=[
469
+ ResourceFactory(
470
+ harvest=HarvestResourceMetadata(issued_at=datetime.now(), uri="test.org")
471
+ )
472
+ ],
473
+ )
474
+
475
+ actions.detach(dataset)
476
+
477
+ dataset.reload()
478
+ assert dataset.harvest is None
479
+ for resource in dataset.resources:
480
+ assert resource.harvest is None
481
+
482
+ def test_detach_all(self):
483
+ source = HarvestSourceFactory()
484
+ datasets = [
485
+ DatasetFactory(
486
+ harvest=HarvestDatasetMetadata(
487
+ source_id=str(source.id), domain="test.org", remote_id=str(i)
488
+ ),
489
+ resources=[
490
+ ResourceFactory(
491
+ harvest=HarvestResourceMetadata(issued_at=datetime.now(), uri="test.org")
492
+ )
493
+ ],
494
+ )
495
+ for i in range(3)
496
+ ]
497
+
498
+ result = actions.detach_all_from_source(source)
499
+
500
+ assert result == len(datasets)
501
+ for dataset in datasets:
502
+ dataset.reload()
503
+ assert dataset.harvest is None
504
+ for resource in dataset.resources:
505
+ assert resource.harvest is None
506
+
454
507
 
455
508
  class ExecutionTestMixin(MockBackendsMixin, PytestOnlyDBTestCase):
456
509
  def action(self, *args, **kwargs):