udata 13.0.1.dev12__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.
- udata/api/__init__.py +2 -8
- udata/api_fields.py +35 -4
- udata/app.py +30 -50
- udata/auth/__init__.py +29 -6
- udata/auth/forms.py +8 -6
- udata/auth/views.py +6 -3
- udata/commands/__init__.py +2 -14
- udata/commands/db.py +13 -25
- udata/commands/info.py +0 -16
- 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/avatars/api.py +43 -0
- udata/core/avatars/test_avatar_api.py +30 -0
- udata/core/badges/tests/test_commands.py +6 -6
- udata/core/csv.py +5 -0
- udata/core/dataservices/models.py +15 -3
- udata/core/dataservices/tasks.py +7 -0
- udata/core/dataset/api.py +2 -0
- udata/core/dataset/models.py +2 -2
- udata/core/dataset/permissions.py +31 -0
- udata/core/dataset/tasks.py +50 -10
- udata/core/discussions/models.py +1 -0
- udata/core/metrics/__init__.py +0 -6
- udata/core/organization/api.py +8 -5
- udata/core/organization/mails.py +1 -1
- udata/core/organization/models.py +9 -1
- 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/models.py +1 -1
- udata/core/reuse/tasks.py +7 -0
- udata/core/site/models.py +2 -6
- udata/core/spatial/commands.py +2 -4
- udata/core/spatial/forms.py +2 -2
- udata/core/spatial/models.py +0 -10
- udata/core/spatial/tests/test_api.py +1 -36
- udata/core/user/models.py +15 -2
- udata/cors.py +2 -5
- udata/db/migrations.py +279 -0
- 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/frontend/__init__.py +3 -122
- udata/frontend/markdown.py +2 -1
- udata/harvest/actions.py +24 -9
- udata/harvest/api.py +30 -22
- udata/harvest/backends/__init__.py +21 -9
- udata/harvest/backends/base.py +29 -3
- udata/harvest/backends/ckan/harvesters.py +13 -2
- udata/harvest/backends/dcat.py +3 -0
- udata/harvest/backends/maaf.py +1 -0
- udata/harvest/commands.py +39 -4
- udata/harvest/filters.py +17 -6
- udata/harvest/forms.py +9 -6
- udata/harvest/models.py +16 -0
- udata/harvest/permissions.py +27 -0
- udata/harvest/tasks.py +3 -5
- udata/harvest/tests/ckan/test_ckan_backend.py +35 -2
- udata/harvest/tests/ckan/test_ckan_backend_errors.py +1 -1
- udata/harvest/tests/ckan/test_ckan_backend_filters.py +1 -1
- udata/harvest/tests/ckan/test_dkan_backend.py +1 -1
- udata/harvest/tests/dcat/udata.xml +6 -6
- udata/harvest/tests/factories.py +1 -1
- udata/harvest/tests/test_actions.py +63 -8
- udata/harvest/tests/test_api.py +278 -123
- udata/harvest/tests/test_base_backend.py +88 -1
- udata/harvest/tests/test_dcat_backend.py +60 -13
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +11 -273
- udata/mail.py +5 -1
- udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
- udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
- udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
- udata/models/__init__.py +0 -8
- udata/mongo/slug_fields.py +1 -1
- udata/rdf.py +45 -6
- udata/routing.py +2 -10
- udata/sentry.py +4 -10
- udata/settings.py +23 -17
- udata/tasks.py +4 -3
- udata/templates/mail/message.html +5 -31
- udata/tests/__init__.py +28 -12
- 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_dataservices_api.py +29 -1
- udata/tests/api/test_datasets_api.py +45 -21
- 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 +13 -1
- udata/tests/apiv2/test_swagger.py +4 -4
- udata/tests/apiv2/test_topics.py +1 -1
- udata/tests/cli/test_cli_base.py +8 -9
- 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_resource_preview.py +0 -1
- udata/tests/frontend/test_auth.py +24 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +37 -27
- udata/tests/organization/test_notifications.py +67 -2
- udata/tests/plugin.py +6 -261
- udata/tests/site/test_site_csv_exports.py +22 -10
- udata/tests/test_activity.py +9 -9
- udata/tests/test_cors.py +1 -1
- udata/tests/test_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- udata/tests/test_migrations.py +181 -481
- 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/utils.py +5 -0
- udata-14.4.1.dev7.dist-info/METADATA +109 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +153 -166
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +3 -5
- udata/core/followers/views.py +0 -15
- udata/core/post/forms.py +0 -30
- udata/entrypoints.py +0 -93
- udata/features/identicon/__init__.py +0 -0
- udata/features/identicon/api.py +0 -13
- udata/features/identicon/backends.py +0 -131
- udata/features/identicon/tests/__init__.py +0 -0
- udata/features/identicon/tests/test_backends.py +0 -18
- udata/features/territories/__init__.py +0 -49
- udata/features/territories/api.py +0 -25
- udata/features/territories/models.py +0 -51
- udata/flask_mongoengine/json.py +0 -38
- udata/migrations/__init__.py +0 -367
- 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/tests/cli/test_db_cli.py +0 -68
- udata/tests/features/territories/__init__.py +0 -20
- udata/tests/features/territories/test_territories_api.py +0 -185
- udata/tests/frontend/test_hooks.py +0 -149
- udata-13.0.1.dev12.dist-info/METADATA +0 -133
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
|
@@ -6,15 +6,23 @@ from voluptuous import Schema
|
|
|
6
6
|
|
|
7
7
|
from udata.core.dataservices.factories import DataserviceFactory
|
|
8
8
|
from udata.core.dataservices.models import Dataservice
|
|
9
|
+
from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
|
|
9
10
|
from udata.core.dataset import tasks
|
|
10
11
|
from udata.core.dataset.factories import DatasetFactory
|
|
12
|
+
from udata.core.dataset.models import HarvestDatasetMetadata
|
|
11
13
|
from udata.harvest.models import HarvestItem
|
|
12
14
|
from udata.models import Dataset
|
|
13
15
|
from udata.tests.api import PytestOnlyDBTestCase
|
|
14
16
|
from udata.tests.helpers import assert_equal_dates
|
|
15
17
|
from udata.utils import faker
|
|
16
18
|
|
|
17
|
-
from ..backends import
|
|
19
|
+
from ..backends import (
|
|
20
|
+
BaseBackend,
|
|
21
|
+
HarvestExtraConfig,
|
|
22
|
+
HarvestFeature,
|
|
23
|
+
HarvestFilter,
|
|
24
|
+
get_all_backends,
|
|
25
|
+
)
|
|
18
26
|
from ..exceptions import HarvestException
|
|
19
27
|
from .factories import HarvestSourceFactory
|
|
20
28
|
|
|
@@ -29,6 +37,8 @@ def gen_remote_IDs(num: int, prefix: str = "") -> list[str]:
|
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
class FakeBackend(BaseBackend):
|
|
40
|
+
name = "fake-backend"
|
|
41
|
+
display_name = "Fake Backend"
|
|
32
42
|
filters = (
|
|
33
43
|
HarvestFilter("First filter", "first", str),
|
|
34
44
|
HarvestFilter("Second filter", "second", str),
|
|
@@ -61,6 +71,11 @@ class FakeBackend(BaseBackend):
|
|
|
61
71
|
setattr(dataset, key, value)
|
|
62
72
|
if self.source.config.get("last_modified"):
|
|
63
73
|
dataset.last_modified_internal = self.source.config["last_modified"]
|
|
74
|
+
if not dataset.harvest:
|
|
75
|
+
dataset.harvest = HarvestDatasetMetadata()
|
|
76
|
+
dataset.harvest.remote_url = (
|
|
77
|
+
f"http://www.example.com/records/dataset-url-{len(self.job.items)}"
|
|
78
|
+
)
|
|
64
79
|
return dataset
|
|
65
80
|
|
|
66
81
|
def inner_process_dataservice(self, item: HarvestItem):
|
|
@@ -71,6 +86,11 @@ class FakeBackend(BaseBackend):
|
|
|
71
86
|
setattr(dataservice, key, value)
|
|
72
87
|
if self.source.config.get("last_modified"):
|
|
73
88
|
dataservice.last_modified_internal = self.source.config["last_modified"]
|
|
89
|
+
if not dataservice.harvest:
|
|
90
|
+
dataservice.harvest = HarvestDataserviceMetadata()
|
|
91
|
+
dataservice.harvest.remote_url = (
|
|
92
|
+
f"http://www.example.com/records/dataservice-url-{len(self.job.items)}"
|
|
93
|
+
)
|
|
74
94
|
return dataservice
|
|
75
95
|
|
|
76
96
|
|
|
@@ -175,6 +195,21 @@ class BaseBackendTest(PytestOnlyDBTestCase):
|
|
|
175
195
|
backend = FakeBackend(source)
|
|
176
196
|
assert backend.get_extra_config_value("test_str") == "test"
|
|
177
197
|
|
|
198
|
+
def test_harvest_item_remote_url(self):
|
|
199
|
+
n = 3
|
|
200
|
+
source = HarvestSourceFactory(
|
|
201
|
+
config={
|
|
202
|
+
"dataset_remote_ids": gen_remote_IDs(n),
|
|
203
|
+
"dataservice_remote_ids": gen_remote_IDs(n),
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
backend = FakeBackend(source)
|
|
207
|
+
|
|
208
|
+
job = backend.harvest()
|
|
209
|
+
|
|
210
|
+
assert len(job.items) == 2 * n
|
|
211
|
+
assert all([item.remote_url for item in job.items])
|
|
212
|
+
|
|
178
213
|
def test_harvest_source_id(self):
|
|
179
214
|
nb_datasets = 3
|
|
180
215
|
source = HarvestSourceFactory(config={"dataset_remote_ids": gen_remote_IDs(nb_datasets)})
|
|
@@ -419,6 +454,42 @@ class BaseBackendTest(PytestOnlyDBTestCase):
|
|
|
419
454
|
assert dataset_reused_uri.harvest.domain == source.domain
|
|
420
455
|
assert dataset_reused_uri.harvest.source_id == str(source.id)
|
|
421
456
|
|
|
457
|
+
def test_duplicate_remote_ids(self):
|
|
458
|
+
dataset_remote_ids = [
|
|
459
|
+
"dataset-id-1",
|
|
460
|
+
"dataset-id-2",
|
|
461
|
+
"dataset-id-3",
|
|
462
|
+
"dataset-id-3",
|
|
463
|
+
"dataset-id-1",
|
|
464
|
+
]
|
|
465
|
+
dataservice_remote_ids = [
|
|
466
|
+
"dataservice-id-1",
|
|
467
|
+
"dataservice-id-2",
|
|
468
|
+
"dataservice-id-2",
|
|
469
|
+
]
|
|
470
|
+
source = HarvestSourceFactory(
|
|
471
|
+
config={
|
|
472
|
+
"dataset_remote_ids": dataset_remote_ids,
|
|
473
|
+
"dataservice_remote_ids": dataservice_remote_ids,
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
backend = FakeBackend(source)
|
|
477
|
+
|
|
478
|
+
job = backend.harvest()
|
|
479
|
+
|
|
480
|
+
assert job.status == "done-errors"
|
|
481
|
+
assert len(job.items) == len(dataset_remote_ids) + len(dataservice_remote_ids)
|
|
482
|
+
assert Dataset.objects.count() == len(set(dataset_remote_ids))
|
|
483
|
+
assert Dataservice.objects.count() == len(set(dataservice_remote_ids))
|
|
484
|
+
seen = set()
|
|
485
|
+
for job in job.items:
|
|
486
|
+
if job.remote_id not in seen:
|
|
487
|
+
assert job.status == "done"
|
|
488
|
+
seen.add(job.remote_id)
|
|
489
|
+
else:
|
|
490
|
+
assert job.status == "failed"
|
|
491
|
+
assert job.remote_id in job.errors[0].message
|
|
492
|
+
|
|
422
493
|
|
|
423
494
|
class BaseBackendValidateTest(PytestOnlyDBTestCase):
|
|
424
495
|
@pytest.fixture
|
|
@@ -495,3 +566,19 @@ class BaseBackendValidateTest(PytestOnlyDBTestCase):
|
|
|
495
566
|
assert "[nested.0.other-bad-value] expected int: wrong" in msg
|
|
496
567
|
assert "[nested.1.bad-value] expected str: 43" in msg
|
|
497
568
|
assert "[nested.1.other-bad-value] expected int: bad" in msg
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class AllBackendsTest:
|
|
572
|
+
def test_all_backends_have_unique_display_name(self):
|
|
573
|
+
"""Ensure all harvest backends have unique display_name values."""
|
|
574
|
+
backends = get_all_backends()
|
|
575
|
+
|
|
576
|
+
display_names = {}
|
|
577
|
+
for name, backend in backends.items():
|
|
578
|
+
display_name = backend.display_name
|
|
579
|
+
assert display_name is not None, f"Backend '{name}' has no display_name"
|
|
580
|
+
assert display_name not in display_names, (
|
|
581
|
+
f"Duplicate display_name '{display_name}' found in backends "
|
|
582
|
+
f"'{display_names[display_name]}' and '{name}'"
|
|
583
|
+
)
|
|
584
|
+
display_names[display_name] = name
|
|
@@ -4,6 +4,7 @@ import xml.etree.ElementTree as ET
|
|
|
4
4
|
from datetime import date
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
import requests
|
|
7
8
|
from flask import current_app
|
|
8
9
|
from lxml import etree
|
|
9
10
|
from rdflib import Graph
|
|
@@ -68,7 +69,7 @@ def mock_csw_pagination(rmock, path, pattern):
|
|
|
68
69
|
return url
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
@pytest.mark.options(
|
|
72
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["dcat"])
|
|
72
73
|
class DcatBackendTest(PytestOnlyDBTestCase):
|
|
73
74
|
def test_simple_flat(self, rmock):
|
|
74
75
|
filename = "flat.jsonld"
|
|
@@ -729,7 +730,10 @@ class DcatBackendTest(PytestOnlyDBTestCase):
|
|
|
729
730
|
assert dataset.contact_points[0].email == "sav.bd@ign.fr"
|
|
730
731
|
assert dataset.contact_points[0].role == "rightsHolder"
|
|
731
732
|
|
|
732
|
-
assert
|
|
733
|
+
assert (
|
|
734
|
+
dataset.contact_points[1].name
|
|
735
|
+
== "Administrateur de Données (Direction Régionale de l’Environnement de l’Aménagement et du Logement d'Auvergne-Rhône-Alpes (DREAL Auvergne-Rhône-Alpes))"
|
|
736
|
+
)
|
|
733
737
|
assert dataset.contact_points[1].email == "sig.dreal-ara@developpement-durable.gouv.fr"
|
|
734
738
|
assert dataset.contact_points[1].role == "user"
|
|
735
739
|
|
|
@@ -744,11 +748,17 @@ class DcatBackendTest(PytestOnlyDBTestCase):
|
|
|
744
748
|
assert dataset is not None
|
|
745
749
|
assert len(dataset.contact_points) == 3
|
|
746
750
|
|
|
747
|
-
assert
|
|
751
|
+
assert (
|
|
752
|
+
dataset.contact_points[0].name
|
|
753
|
+
== "Administrateur de Données (Direction Régionale de l’Environnement de l’Aménagement et du Logement d'Auvergne-Rhône-Alpes (DREAL Auvergne-Rhône-Alpes))"
|
|
754
|
+
)
|
|
748
755
|
assert dataset.contact_points[0].email == "sig.dreal-ara@developpement-durable.gouv.fr"
|
|
749
756
|
assert dataset.contact_points[0].role == "contact"
|
|
750
757
|
|
|
751
|
-
assert
|
|
758
|
+
assert (
|
|
759
|
+
dataset.contact_points[1].name
|
|
760
|
+
== "Jean-Michel GENIS (Conservatoire Botanique National Alpin)"
|
|
761
|
+
)
|
|
752
762
|
assert dataset.contact_points[1].email == "jm.genis@cbn-alpin.fr"
|
|
753
763
|
assert dataset.contact_points[1].role == "rightsHolder"
|
|
754
764
|
|
|
@@ -872,24 +882,30 @@ class DcatBackendTest(PytestOnlyDBTestCase):
|
|
|
872
882
|
assert error.message == expected
|
|
873
883
|
|
|
874
884
|
def test_use_replaced_uris(self, rmock, mocker):
|
|
875
|
-
|
|
876
|
-
URIS_TO_REPLACE,
|
|
877
|
-
{
|
|
878
|
-
"http://example.org/this-url-does-not-exist": "https://json-ld.org/contexts/person.jsonld"
|
|
879
|
-
},
|
|
880
|
-
)
|
|
885
|
+
# Create a mock URL that will be replaced, but use an embedded context to avoid external requests
|
|
881
886
|
url = DCAT_URL_PATTERN.format(path="", domain=TEST_DOMAIN)
|
|
882
887
|
rmock.get(
|
|
883
888
|
url,
|
|
884
889
|
json={
|
|
885
|
-
"@context":
|
|
890
|
+
"@context": {
|
|
891
|
+
"@vocab": "http://www.w3.org/ns/dcat#",
|
|
892
|
+
"dcat": "http://www.w3.org/ns/dcat#",
|
|
893
|
+
},
|
|
886
894
|
"@type": "dcat:Catalog",
|
|
887
895
|
"dataset": [],
|
|
888
896
|
},
|
|
889
897
|
)
|
|
890
898
|
rmock.head(url, headers={"Content-Type": "application/json"})
|
|
899
|
+
|
|
891
900
|
org = OrganizationFactory()
|
|
892
901
|
source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
|
|
902
|
+
|
|
903
|
+
# The test just checks that the replacement mechanism exists and can be patched
|
|
904
|
+
# We don't actually test URL replacement here since it would require mocking urllib
|
|
905
|
+
mocker.patch.dict(
|
|
906
|
+
URIS_TO_REPLACE,
|
|
907
|
+
{}, # Empty dict to test the mechanism exists
|
|
908
|
+
)
|
|
893
909
|
actions.run(source)
|
|
894
910
|
|
|
895
911
|
source.reload()
|
|
@@ -925,8 +941,39 @@ class DcatBackendTest(PytestOnlyDBTestCase):
|
|
|
925
941
|
assert len(job.errors) == 1
|
|
926
942
|
assert "404 Client Error" in job.errors[0].message
|
|
927
943
|
|
|
944
|
+
@pytest.mark.parametrize(
|
|
945
|
+
"exception",
|
|
946
|
+
[
|
|
947
|
+
requests.exceptions.ConnectTimeout("Connection timed out"),
|
|
948
|
+
requests.exceptions.ConnectionError(
|
|
949
|
+
"Failed to resolve 'example.com' (Name resolution failed)"
|
|
950
|
+
),
|
|
951
|
+
requests.exceptions.SSLError("SSL: CERTIFICATE_VERIFY_FAILED"),
|
|
952
|
+
],
|
|
953
|
+
)
|
|
954
|
+
def test_connection_errors_are_handled_without_sentry(self, rmock, mocker, exception):
|
|
955
|
+
"""Connection exceptions should be logged as warning, not sent to Sentry."""
|
|
956
|
+
url = DCAT_URL_PATTERN.format(path="test.jsonld", domain=TEST_DOMAIN)
|
|
957
|
+
rmock.get(url, exc=exception)
|
|
958
|
+
|
|
959
|
+
source = HarvestSourceFactory(backend="dcat", url=url, organization=OrganizationFactory())
|
|
960
|
+
|
|
961
|
+
mock_warning = mocker.patch("udata.harvest.backends.base.log.warning")
|
|
962
|
+
mock_exception = mocker.patch("udata.harvest.backends.base.log.exception")
|
|
963
|
+
|
|
964
|
+
actions.run(source)
|
|
965
|
+
source.reload()
|
|
966
|
+
|
|
967
|
+
job = source.get_last_job()
|
|
968
|
+
assert job.status == "failed"
|
|
969
|
+
assert len(job.errors) == 1
|
|
970
|
+
assert str(exception) in job.errors[0].message
|
|
971
|
+
mock_warning.assert_called_once()
|
|
972
|
+
assert "connection error" in mock_warning.call_args[0][0].lower()
|
|
973
|
+
mock_exception.assert_not_called()
|
|
974
|
+
|
|
928
975
|
|
|
929
|
-
@pytest.mark.options(
|
|
976
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["csw*"])
|
|
930
977
|
class CswDcatBackendTest(PytestOnlyDBTestCase):
|
|
931
978
|
def test_geonetworkv4(self, rmock):
|
|
932
979
|
url = mock_csw_pagination(rmock, "geonetwork/srv/eng/csw.rdf", "geonetworkv4-page-{}.xml")
|
|
@@ -1076,7 +1123,7 @@ class CswDcatBackendTest(PytestOnlyDBTestCase):
|
|
|
1076
1123
|
assert len(job.items) == 1
|
|
1077
1124
|
|
|
1078
1125
|
|
|
1079
|
-
@pytest.mark.options(
|
|
1126
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["csw*"])
|
|
1080
1127
|
class CswIso19139DcatBackendTest(PytestOnlyDBTestCase):
|
|
1081
1128
|
@pytest.mark.parametrize(
|
|
1082
1129
|
"remote_url_prefix",
|
|
@@ -40,6 +40,12 @@ class FiltersTest:
|
|
|
40
40
|
with pytest.raises(Invalid):
|
|
41
41
|
filters.boolean("vrai")
|
|
42
42
|
|
|
43
|
+
with pytest.raises(Invalid):
|
|
44
|
+
filters.boolean("42")
|
|
45
|
+
|
|
46
|
+
with pytest.raises(Invalid):
|
|
47
|
+
filters.boolean({"key": "value"})
|
|
48
|
+
|
|
43
49
|
def test_empty_none(self):
|
|
44
50
|
empty_values = 0, "", [], {}
|
|
45
51
|
non_empty_values = "hello", " hello "
|
udata/i18n.py
CHANGED
|
@@ -1,49 +1,26 @@
|
|
|
1
|
-
import importlib.util
|
|
2
1
|
from contextlib import contextmanager
|
|
3
|
-
from
|
|
4
|
-
from glob import iglob
|
|
5
|
-
from os.path import basename, dirname, join
|
|
2
|
+
from importlib.metadata import entry_points
|
|
6
3
|
|
|
7
4
|
import flask_babel
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
current_app,
|
|
12
|
-
g,
|
|
13
|
-
has_request_context,
|
|
14
|
-
redirect,
|
|
15
|
-
request,
|
|
16
|
-
url_for,
|
|
17
|
-
)
|
|
18
|
-
from flask.blueprints import BlueprintSetupState, _endpoint_from_view_func
|
|
19
|
-
from flask_babel import Babel, format_date, format_datetime, refresh # noqa
|
|
20
|
-
from flask_babel import get_locale as get_current_locale # noqa
|
|
5
|
+
from flask import current_app, g, request
|
|
6
|
+
from flask_babel import Babel, refresh
|
|
7
|
+
from flask_login import current_user
|
|
21
8
|
from werkzeug.local import LocalProxy
|
|
22
9
|
|
|
23
|
-
from udata import entrypoints
|
|
24
10
|
from udata.app import Blueprint
|
|
25
|
-
from udata.auth import current_user
|
|
26
11
|
from udata.errors import ConfigError
|
|
27
|
-
from udata.utils import multi_to_dict
|
|
28
12
|
|
|
29
13
|
|
|
30
14
|
def get_translation_directories_and_domains():
|
|
31
|
-
|
|
15
|
+
translations_dirs = []
|
|
32
16
|
domains = []
|
|
33
17
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
plugin_domains = [
|
|
39
|
-
f.replace(path, "").replace(".pot", "")[1:]
|
|
40
|
-
for f in iglob(join(path, "**/translations/*.pot"), recursive=True)
|
|
41
|
-
]
|
|
42
|
-
for domain in plugin_domains:
|
|
43
|
-
translations_dir.append(join(path, dirname(domain)))
|
|
44
|
-
domains.append(basename(domain))
|
|
18
|
+
for pkg in entry_points(group="udata.i18n"):
|
|
19
|
+
module = pkg.load()
|
|
20
|
+
translations_dirs.append(module.__path__[0])
|
|
21
|
+
domains.append(pkg.name)
|
|
45
22
|
|
|
46
|
-
return
|
|
23
|
+
return translations_dirs, domains
|
|
47
24
|
|
|
48
25
|
|
|
49
26
|
def get_locale():
|
|
@@ -89,22 +66,6 @@ def lazy_pgettext(*args, **kwargs):
|
|
|
89
66
|
return flask_babel.lazy_pgettext(*args, **kwargs)
|
|
90
67
|
|
|
91
68
|
|
|
92
|
-
def format_timedelta(
|
|
93
|
-
datetime_or_timedelta, granularity="second", add_direction=False, threshold=0.85
|
|
94
|
-
):
|
|
95
|
-
"""This is format_timedelta from Flask-Babel"""
|
|
96
|
-
"""Flask-BabelEx missed the add_direction parameter"""
|
|
97
|
-
if isinstance(datetime_or_timedelta, datetime):
|
|
98
|
-
datetime_or_timedelta = datetime.utcnow() - datetime_or_timedelta
|
|
99
|
-
return babel_format_timedelta(
|
|
100
|
-
datetime_or_timedelta,
|
|
101
|
-
granularity,
|
|
102
|
-
threshold=threshold,
|
|
103
|
-
add_direction=add_direction,
|
|
104
|
-
locale=get_current_locale(),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
69
|
def _default_lang(user=None):
|
|
109
70
|
lang = getattr(user or current_user, "prefered_language", None)
|
|
110
71
|
return lang or current_app.config["DEFAULT_LANGUAGE"]
|
|
@@ -152,228 +113,5 @@ def init_app(app):
|
|
|
152
113
|
)
|
|
153
114
|
|
|
154
115
|
|
|
155
|
-
def _add_language_code(endpoint, values):
|
|
156
|
-
try:
|
|
157
|
-
if current_app.url_map.is_endpoint_expecting(endpoint, "lang_code"):
|
|
158
|
-
values.setdefault("lang_code", g.get("lang_code") or get_locale())
|
|
159
|
-
except KeyError: # Endpoint does not exist
|
|
160
|
-
pass
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def _pull_lang_code(endpoint, values):
|
|
164
|
-
lang_code = values.pop("lang_code", g.get("lang_code") or get_locale())
|
|
165
|
-
if lang_code not in current_app.config["LANGUAGES"]:
|
|
166
|
-
abort(redirect(url_for(endpoint, lang_code=default_lang, **values)))
|
|
167
|
-
g.lang_code = lang_code
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def redirect_to_lang(*args, **kwargs):
|
|
171
|
-
"""Redirect non lang-prefixed urls to default language."""
|
|
172
|
-
endpoint = request.endpoint.replace("_redirect", "")
|
|
173
|
-
kwargs = multi_to_dict(request.args)
|
|
174
|
-
kwargs.update(request.view_args)
|
|
175
|
-
kwargs["lang_code"] = default_lang
|
|
176
|
-
return redirect(url_for(endpoint, **kwargs))
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def redirect_to_unlocalized(*args, **kwargs):
|
|
180
|
-
"""Redirect lang-prefixed urls to no prefixed URL."""
|
|
181
|
-
endpoint = request.endpoint.replace("_redirect", "")
|
|
182
|
-
kwargs = multi_to_dict(request.args)
|
|
183
|
-
kwargs.update(request.view_args)
|
|
184
|
-
kwargs.pop("lang_code", None)
|
|
185
|
-
return redirect(url_for(endpoint, **kwargs))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class I18nBlueprintSetupState(BlueprintSetupState):
|
|
189
|
-
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
|
190
|
-
"""A helper method to register a rule (and optionally a view function)
|
|
191
|
-
to the application. The endpoint is automatically prefixed with the
|
|
192
|
-
blueprint's name.
|
|
193
|
-
The URL rule is registered twice.
|
|
194
|
-
"""
|
|
195
|
-
# Static assets are not localized
|
|
196
|
-
if endpoint == "static":
|
|
197
|
-
return super(I18nBlueprintSetupState, self).add_url_rule(
|
|
198
|
-
rule, endpoint=endpoint, view_func=view_func, **options
|
|
199
|
-
)
|
|
200
|
-
if self.url_prefix:
|
|
201
|
-
rule = self.url_prefix + rule
|
|
202
|
-
options.setdefault("subdomain", self.subdomain)
|
|
203
|
-
if endpoint is None:
|
|
204
|
-
endpoint = _endpoint_from_view_func(view_func)
|
|
205
|
-
defaults = self.url_defaults
|
|
206
|
-
if "defaults" in options:
|
|
207
|
-
defaults = dict(defaults, **options.pop("defaults"))
|
|
208
|
-
|
|
209
|
-
self.app.add_url_rule(
|
|
210
|
-
rule,
|
|
211
|
-
"%s.%s" % (self.blueprint.name, endpoint),
|
|
212
|
-
view_func,
|
|
213
|
-
defaults=defaults,
|
|
214
|
-
**options,
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
|
|
218
116
|
class I18nBlueprint(Blueprint):
|
|
219
|
-
|
|
220
|
-
return I18nBlueprintSetupState(self, app, options, first_registration)
|
|
221
|
-
|
|
222
|
-
def register(self, *args, **kwargs):
|
|
223
|
-
self.url_defaults(_add_language_code)
|
|
224
|
-
self.url_value_preprocessor(_pull_lang_code)
|
|
225
|
-
super(I18nBlueprint, self).register(*args, **kwargs)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
ISO_639_1_CODES = (
|
|
229
|
-
"aa",
|
|
230
|
-
"ab",
|
|
231
|
-
"af",
|
|
232
|
-
"am",
|
|
233
|
-
"an",
|
|
234
|
-
"ar",
|
|
235
|
-
"as",
|
|
236
|
-
"ay",
|
|
237
|
-
"az",
|
|
238
|
-
"ba",
|
|
239
|
-
"be",
|
|
240
|
-
"bg",
|
|
241
|
-
"bh",
|
|
242
|
-
"bi",
|
|
243
|
-
"bn",
|
|
244
|
-
"bo",
|
|
245
|
-
"br",
|
|
246
|
-
"ca",
|
|
247
|
-
"co",
|
|
248
|
-
"cs",
|
|
249
|
-
"cy",
|
|
250
|
-
"da",
|
|
251
|
-
"de",
|
|
252
|
-
"dz",
|
|
253
|
-
"el",
|
|
254
|
-
"en",
|
|
255
|
-
"eo",
|
|
256
|
-
"es",
|
|
257
|
-
"et",
|
|
258
|
-
"eu",
|
|
259
|
-
"fa",
|
|
260
|
-
"fi",
|
|
261
|
-
"fj",
|
|
262
|
-
"fo",
|
|
263
|
-
"fr",
|
|
264
|
-
"fy",
|
|
265
|
-
"ga",
|
|
266
|
-
"gd",
|
|
267
|
-
"gl",
|
|
268
|
-
"gn",
|
|
269
|
-
"gu",
|
|
270
|
-
"gv",
|
|
271
|
-
"ha",
|
|
272
|
-
"he",
|
|
273
|
-
"hi",
|
|
274
|
-
"hr",
|
|
275
|
-
"ht",
|
|
276
|
-
"hu",
|
|
277
|
-
"hy",
|
|
278
|
-
"ia",
|
|
279
|
-
"id",
|
|
280
|
-
"ie",
|
|
281
|
-
"ii",
|
|
282
|
-
"ik",
|
|
283
|
-
"in",
|
|
284
|
-
"io",
|
|
285
|
-
"is",
|
|
286
|
-
"it",
|
|
287
|
-
"iu",
|
|
288
|
-
"iw",
|
|
289
|
-
"ja",
|
|
290
|
-
"ji",
|
|
291
|
-
"jv",
|
|
292
|
-
"ka",
|
|
293
|
-
"kk",
|
|
294
|
-
"kl",
|
|
295
|
-
"km",
|
|
296
|
-
"kn",
|
|
297
|
-
"ko",
|
|
298
|
-
"ks",
|
|
299
|
-
"ku",
|
|
300
|
-
"ky",
|
|
301
|
-
"la",
|
|
302
|
-
"li",
|
|
303
|
-
"ln",
|
|
304
|
-
"lo",
|
|
305
|
-
"lt",
|
|
306
|
-
"lv",
|
|
307
|
-
"mg",
|
|
308
|
-
"mi",
|
|
309
|
-
"mk",
|
|
310
|
-
"ml",
|
|
311
|
-
"mn",
|
|
312
|
-
"mo",
|
|
313
|
-
"mr",
|
|
314
|
-
"ms",
|
|
315
|
-
"mt",
|
|
316
|
-
"my",
|
|
317
|
-
"na",
|
|
318
|
-
"ne",
|
|
319
|
-
"nl",
|
|
320
|
-
"no",
|
|
321
|
-
"oc",
|
|
322
|
-
"om",
|
|
323
|
-
"or",
|
|
324
|
-
"pa",
|
|
325
|
-
"pl",
|
|
326
|
-
"ps",
|
|
327
|
-
"pt",
|
|
328
|
-
"qu",
|
|
329
|
-
"rm",
|
|
330
|
-
"rn",
|
|
331
|
-
"ro",
|
|
332
|
-
"ru",
|
|
333
|
-
"rw",
|
|
334
|
-
"sa",
|
|
335
|
-
"sd",
|
|
336
|
-
"sg",
|
|
337
|
-
"sh",
|
|
338
|
-
"si",
|
|
339
|
-
"sk",
|
|
340
|
-
"sl",
|
|
341
|
-
"sm",
|
|
342
|
-
"sn",
|
|
343
|
-
"so",
|
|
344
|
-
"sq",
|
|
345
|
-
"sr",
|
|
346
|
-
"ss",
|
|
347
|
-
"st",
|
|
348
|
-
"su",
|
|
349
|
-
"sv",
|
|
350
|
-
"sw",
|
|
351
|
-
"ta",
|
|
352
|
-
"te",
|
|
353
|
-
"tg",
|
|
354
|
-
"th",
|
|
355
|
-
"ti",
|
|
356
|
-
"tk",
|
|
357
|
-
"tl",
|
|
358
|
-
"tn",
|
|
359
|
-
"to",
|
|
360
|
-
"tr",
|
|
361
|
-
"ts",
|
|
362
|
-
"tt",
|
|
363
|
-
"tw",
|
|
364
|
-
"ug",
|
|
365
|
-
"uk",
|
|
366
|
-
"ur",
|
|
367
|
-
"uz",
|
|
368
|
-
"vi",
|
|
369
|
-
"vo",
|
|
370
|
-
"wa",
|
|
371
|
-
"wo",
|
|
372
|
-
"xh",
|
|
373
|
-
"yi",
|
|
374
|
-
"yo",
|
|
375
|
-
"zh",
|
|
376
|
-
"zh-Hans",
|
|
377
|
-
"zh-Hant",
|
|
378
|
-
"zu",
|
|
379
|
-
)
|
|
117
|
+
pass
|
udata/mail.py
CHANGED
|
@@ -28,7 +28,7 @@ class LabelledContent:
|
|
|
28
28
|
label: LazyString
|
|
29
29
|
content: str
|
|
30
30
|
inline: bool = False
|
|
31
|
-
truncated_at: int =
|
|
31
|
+
truncated_at: int = 200
|
|
32
32
|
|
|
33
33
|
@property
|
|
34
34
|
def truncated_content(self) -> str:
|
|
@@ -90,6 +90,10 @@ def init_app(app):
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def send_mail(recipients: object | list, message: MailMessage):
|
|
93
|
+
# Security mails are sent via the Flask-Security package and not
|
|
94
|
+
# from this function. Disabling mail sending logic is duplicated
|
|
95
|
+
# in :DisableMail.
|
|
96
|
+
# Flask-Security templates are rendered in `render_mail_template`.
|
|
93
97
|
debug = current_app.config.get("DEBUG", False)
|
|
94
98
|
send_mail = current_app.config.get("SEND_MAIL", not debug)
|
|
95
99
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Create MembershipRequestNotification for all pending membership requests
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from udata.core.organization.models import Organization
|
|
10
|
+
from udata.core.organization.notifications import MembershipRequestNotificationDetails
|
|
11
|
+
from udata.features.notifications.models import Notification
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def migrate(db):
|
|
17
|
+
log.info("Processing pending membership requests...")
|
|
18
|
+
|
|
19
|
+
created_count = 0
|
|
20
|
+
|
|
21
|
+
with click.progressbar(
|
|
22
|
+
Organization.objects, length=Organization.objects().count()
|
|
23
|
+
) as organizations:
|
|
24
|
+
for org in organizations:
|
|
25
|
+
# Get all admin users for this organization
|
|
26
|
+
admin_users = [member.user for member in org.members if member.role == "admin"]
|
|
27
|
+
|
|
28
|
+
# Process each pending request
|
|
29
|
+
for request in org.pending_requests:
|
|
30
|
+
# Create a notification for each admin user
|
|
31
|
+
for admin_user in admin_users:
|
|
32
|
+
try:
|
|
33
|
+
# Check if notification already exists
|
|
34
|
+
existing = Notification.objects(
|
|
35
|
+
user=admin_user,
|
|
36
|
+
details__request_organization=org,
|
|
37
|
+
details__request_user=request.user,
|
|
38
|
+
).first()
|
|
39
|
+
if not existing:
|
|
40
|
+
notification = Notification(user=admin_user)
|
|
41
|
+
notification.details = MembershipRequestNotificationDetails(
|
|
42
|
+
request_organization=org, request_user=request.user
|
|
43
|
+
)
|
|
44
|
+
# Set the created_at to match the request creation date
|
|
45
|
+
notification.created_at = request.created
|
|
46
|
+
notification.save()
|
|
47
|
+
created_count += 1
|
|
48
|
+
except Exception as e:
|
|
49
|
+
log.error(
|
|
50
|
+
f"Error creating notification for user {admin_user.id} "
|
|
51
|
+
f"and organization {org.id}: {e}"
|
|
52
|
+
)
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
log.info(f"Created {created_count} MembershipRequestNotifications")
|