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.
- udata/api/__init__.py +2 -0
- udata/api_fields.py +35 -4
- udata/app.py +18 -20
- udata/auth/__init__.py +29 -6
- udata/auth/forms.py +2 -2
- udata/auth/views.py +13 -6
- udata/commands/dcat.py +1 -1
- udata/commands/serve.py +3 -11
- udata/commands/tests/test_fixtures.py +9 -9
- udata/core/access_type/api.py +1 -1
- udata/core/access_type/constants.py +12 -8
- udata/core/activity/api.py +5 -6
- udata/core/badges/tests/test_commands.py +6 -6
- udata/core/csv.py +5 -0
- udata/core/dataservices/api.py +8 -1
- udata/core/dataservices/apiv2.py +2 -5
- udata/core/dataservices/models.py +5 -2
- udata/core/dataservices/rdf.py +2 -1
- udata/core/dataservices/tasks.py +13 -2
- udata/core/dataset/api.py +10 -0
- udata/core/dataset/models.py +6 -6
- udata/core/dataset/permissions.py +31 -0
- udata/core/dataset/rdf.py +8 -2
- udata/core/dataset/tasks.py +23 -7
- udata/core/discussions/api.py +15 -1
- udata/core/discussions/models.py +6 -0
- udata/core/legal/__init__.py +0 -0
- udata/core/legal/mails.py +128 -0
- udata/core/organization/api.py +16 -5
- udata/core/organization/apiv2.py +2 -3
- udata/core/organization/mails.py +1 -1
- udata/core/organization/models.py +15 -2
- udata/core/organization/notifications.py +84 -0
- udata/core/organization/permissions.py +1 -1
- udata/core/organization/tasks.py +3 -0
- udata/core/pages/tests/test_api.py +32 -0
- udata/core/post/api.py +24 -69
- udata/core/post/models.py +84 -16
- udata/core/post/tests/test_api.py +24 -1
- udata/core/reports/api.py +18 -0
- udata/core/reports/models.py +42 -2
- udata/core/reuse/api.py +8 -0
- udata/core/reuse/apiv2.py +2 -5
- udata/core/reuse/models.py +1 -1
- udata/core/reuse/tasks.py +7 -0
- udata/core/spatial/forms.py +2 -2
- udata/core/topic/models.py +8 -2
- udata/core/user/api.py +10 -3
- udata/core/user/models.py +12 -2
- udata/features/notifications/api.py +7 -18
- udata/features/notifications/models.py +56 -0
- udata/features/notifications/tasks.py +25 -0
- udata/flask_mongoengine/engine.py +0 -4
- udata/flask_mongoengine/pagination.py +1 -1
- udata/frontend/markdown.py +2 -1
- udata/harvest/actions.py +21 -1
- udata/harvest/api.py +25 -8
- udata/harvest/backends/base.py +27 -1
- udata/harvest/backends/ckan/harvesters.py +11 -2
- udata/harvest/backends/dcat.py +4 -1
- udata/harvest/commands.py +33 -0
- udata/harvest/filters.py +17 -6
- udata/harvest/models.py +16 -0
- udata/harvest/permissions.py +27 -0
- udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
- udata/harvest/tests/test_actions.py +58 -5
- udata/harvest/tests/test_api.py +276 -122
- udata/harvest/tests/test_base_backend.py +86 -1
- udata/harvest/tests/test_dcat_backend.py +81 -10
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +1 -4
- udata/mail.py +19 -1
- udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
- udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
- udata/mongo/slug_fields.py +1 -1
- udata/rdf.py +58 -10
- udata/routing.py +2 -2
- udata/settings.py +11 -0
- udata/tasks.py +1 -0
- udata/templates/mail/message.html +5 -31
- udata/tests/__init__.py +27 -2
- udata/tests/api/__init__.py +108 -21
- udata/tests/api/test_activities_api.py +36 -0
- udata/tests/api/test_auth_api.py +121 -95
- udata/tests/api/test_base_api.py +7 -4
- udata/tests/api/test_datasets_api.py +50 -19
- udata/tests/api/test_organizations_api.py +192 -197
- udata/tests/api/test_reports_api.py +157 -0
- udata/tests/api/test_reuses_api.py +147 -147
- udata/tests/api/test_security_api.py +12 -12
- udata/tests/api/test_swagger.py +4 -4
- udata/tests/api/test_tags_api.py +8 -8
- udata/tests/api/test_user_api.py +1 -1
- udata/tests/apiv2/test_search.py +30 -0
- udata/tests/apiv2/test_swagger.py +4 -4
- udata/tests/cli/test_cli_base.py +8 -9
- udata/tests/dataservice/test_dataservice_tasks.py +29 -0
- udata/tests/dataset/test_dataset_commands.py +4 -4
- udata/tests/dataset/test_dataset_model.py +66 -26
- udata/tests/dataset/test_dataset_rdf.py +99 -5
- udata/tests/dataset/test_dataset_tasks.py +25 -0
- udata/tests/frontend/test_auth.py +58 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +31 -27
- udata/tests/organization/test_notifications.py +67 -2
- udata/tests/plugin.py +6 -261
- udata/tests/search/test_search_integration.py +33 -0
- udata/tests/site/test_site_csv_exports.py +22 -10
- udata/tests/test_activity.py +9 -9
- udata/tests/test_api_fields.py +10 -0
- udata/tests/test_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- udata/tests/test_legal_mails.py +359 -0
- udata/tests/test_migrations.py +21 -21
- udata/tests/test_notifications.py +15 -57
- udata/tests/test_notifications_task.py +43 -0
- udata/tests/test_owned.py +81 -1
- udata/tests/test_storages.py +25 -19
- udata/tests/test_topics.py +77 -61
- udata/tests/test_uris.py +33 -0
- udata/tests/workers/test_jobs_commands.py +23 -23
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +187 -108
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +187 -108
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +187 -108
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +188 -109
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +187 -108
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +187 -108
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +187 -108
- udata/translations/udata.pot +215 -106
- udata/uris.py +0 -2
- udata-14.5.1.dev6.dist-info/METADATA +109 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/RECORD +143 -140
- udata/core/post/forms.py +0 -30
- udata/flask_mongoengine/json.py +0 -38
- udata/templates/mail/base.html +0 -105
- udata/templates/mail/base.txt +0 -6
- udata/templates/mail/button.html +0 -3
- udata/templates/mail/layouts/1-column.html +0 -19
- udata/templates/mail/layouts/2-columns.html +0 -20
- udata/templates/mail/layouts/center-panel.html +0 -16
- udata-14.0.0.dist-info/METADATA +0 -132
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/WHEEL +0 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/entry_points.txt +0 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/licenses/LICENSE +0 -0
- {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 {
|
|
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 {
|
|
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 {
|
|
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):
|