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
udata/harvest/models.py CHANGED
@@ -66,6 +66,7 @@ class HarvestLog(db.EmbeddedDocument):
66
66
 
67
67
  class HarvestItem(db.EmbeddedDocument):
68
68
  remote_id = db.StringField()
69
+ remote_url = db.StringField()
69
70
  dataset = db.ReferenceField(Dataset)
70
71
  dataservice = db.ReferenceField(Dataservice)
71
72
  status = db.StringField(
@@ -172,6 +173,21 @@ class HarvestSource(Owned, db.Document):
172
173
  def __str__(self):
173
174
  return self.name or ""
174
175
 
176
+ @property
177
+ def permissions(self):
178
+ from udata.auth import admin_permission
179
+
180
+ from .permissions import HarvestSourceAdminPermission, HarvestSourcePermission
181
+
182
+ return {
183
+ "edit": HarvestSourceAdminPermission(self),
184
+ "delete": HarvestSourceAdminPermission(self),
185
+ "run": HarvestSourceAdminPermission(self),
186
+ "preview": HarvestSourcePermission(self),
187
+ "validate": admin_permission,
188
+ "schedule": admin_permission,
189
+ }
190
+
175
191
 
176
192
  class HarvestJob(db.Document):
177
193
  """Keep track of harvestings"""
@@ -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):