udata 13.0.1.dev12__py3-none-any.whl → 14.0.0__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/app.py +12 -30
- udata/auth/forms.py +6 -4
- udata/commands/__init__.py +2 -14
- udata/commands/db.py +13 -25
- udata/commands/info.py +0 -16
- udata/core/avatars/api.py +43 -0
- udata/core/avatars/test_avatar_api.py +30 -0
- udata/core/dataservices/models.py +14 -2
- udata/core/dataset/tasks.py +36 -8
- udata/core/metrics/__init__.py +0 -6
- udata/core/site/models.py +2 -6
- udata/core/spatial/commands.py +2 -4
- udata/core/spatial/models.py +0 -10
- udata/core/spatial/tests/test_api.py +1 -36
- udata/core/user/models.py +10 -1
- udata/cors.py +2 -5
- udata/db/migrations.py +279 -0
- udata/frontend/__init__.py +3 -122
- udata/harvest/actions.py +3 -8
- udata/harvest/api.py +5 -14
- udata/harvest/backends/__init__.py +21 -9
- udata/harvest/backends/base.py +2 -2
- udata/harvest/backends/ckan/harvesters.py +2 -0
- udata/harvest/backends/dcat.py +3 -0
- udata/harvest/backends/maaf.py +1 -0
- udata/harvest/commands.py +6 -4
- udata/harvest/forms.py +9 -6
- udata/harvest/tasks.py +3 -5
- udata/harvest/tests/ckan/test_ckan_backend.py +2 -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 +5 -3
- udata/harvest/tests/test_api.py +2 -1
- udata/harvest/tests/test_base_backend.py +2 -0
- udata/harvest/tests/test_dcat_backend.py +3 -3
- udata/i18n.py +14 -273
- udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
- udata/models/__init__.py +0 -8
- udata/routing.py +0 -8
- udata/sentry.py +4 -10
- udata/settings.py +16 -17
- udata/tasks.py +3 -3
- udata/tests/__init__.py +1 -10
- udata/tests/api/test_dataservices_api.py +29 -1
- udata/tests/api/test_datasets_api.py +1 -2
- udata/tests/api/test_user_api.py +12 -0
- udata/tests/apiv2/test_topics.py +1 -1
- udata/tests/dataset/test_resource_preview.py +0 -1
- udata/tests/helpers.py +12 -0
- udata/tests/test_cors.py +1 -1
- udata/tests/test_migrations.py +181 -481
- udata/utils.py +5 -0
- {udata-13.0.1.dev12.dist-info → udata-14.0.0.dist-info}/METADATA +1 -2
- {udata-13.0.1.dev12.dist-info → udata-14.0.0.dist-info}/RECORD +62 -73
- {udata-13.0.1.dev12.dist-info → udata-14.0.0.dist-info}/entry_points.txt +3 -5
- udata/core/followers/views.py +0 -15
- 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/migrations/__init__.py +0 -367
- 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 → udata-14.0.0.dist-info}/WHEEL +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.0.0.dist-info}/licenses/LICENSE +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.0.0.dist-info}/top_level.txt +0 -0
udata/harvest/forms.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from udata.forms import Form, fields, validators
|
|
2
|
+
from udata.harvest.backends import get_backend, get_enabled_backends
|
|
2
3
|
from udata.i18n import lazy_gettext as _
|
|
3
4
|
from udata.utils import safe_unicode
|
|
4
5
|
|
|
5
|
-
from .actions import list_backends
|
|
6
6
|
from .models import VALIDATION_REFUSED, VALIDATION_STATES
|
|
7
7
|
|
|
8
8
|
__all__ = "HarvestSourceForm", "HarvestSourceValidationForm"
|
|
@@ -13,9 +13,6 @@ class HarvestConfigField(fields.DictField):
|
|
|
13
13
|
A DictField with extras validations on known configurations
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
def get_backend(self, form):
|
|
17
|
-
return next(b for b in list_backends() if b.name == form.backend.data)
|
|
18
|
-
|
|
19
16
|
def get_filter_specs(self, backend, key):
|
|
20
17
|
candidates = (f for f in backend.filters if f.key == key)
|
|
21
18
|
return next(candidates, None)
|
|
@@ -30,7 +27,10 @@ class HarvestConfigField(fields.DictField):
|
|
|
30
27
|
|
|
31
28
|
def pre_validate(self, form):
|
|
32
29
|
if self.data:
|
|
33
|
-
backend =
|
|
30
|
+
backend = get_backend(form.backend.data)
|
|
31
|
+
if backend is None:
|
|
32
|
+
return # Should have been catch by the enum check for `form.backend`
|
|
33
|
+
|
|
34
34
|
# Validate filters
|
|
35
35
|
for f in self.data.get("filters") or []:
|
|
36
36
|
if not ("key" in f and "value" in f):
|
|
@@ -49,6 +49,7 @@ class HarvestConfigField(fields.DictField):
|
|
|
49
49
|
msg = '"{0}" filter should of type "{1}"'
|
|
50
50
|
msg = msg.format(specs.key, specs.type.__name__)
|
|
51
51
|
raise validators.ValidationError(msg)
|
|
52
|
+
|
|
52
53
|
# Validate extras configs
|
|
53
54
|
for f in self.data.get("extra_configs") or []:
|
|
54
55
|
if not ("key" in f and "value" in f):
|
|
@@ -63,6 +64,7 @@ class HarvestConfigField(fields.DictField):
|
|
|
63
64
|
msg = '"{0}" extra config should be of type "{1}"'
|
|
64
65
|
msg = msg.format(specs.key, specs.type.__name__)
|
|
65
66
|
raise validators.ValidationError(msg)
|
|
67
|
+
|
|
66
68
|
# Validate features
|
|
67
69
|
for key, value in (self.data.get("features") or {}).items():
|
|
68
70
|
if not isinstance(value, bool):
|
|
@@ -81,7 +83,8 @@ class HarvestSourceForm(Form):
|
|
|
81
83
|
)
|
|
82
84
|
url = fields.URLField(_("URL"), [validators.DataRequired()])
|
|
83
85
|
backend = fields.SelectField(
|
|
84
|
-
_("Backend"),
|
|
86
|
+
_("Backend"),
|
|
87
|
+
choices=lambda: [(b.name, b.display_name) for b in get_enabled_backends().values()],
|
|
85
88
|
)
|
|
86
89
|
owner = fields.CurrentUserField()
|
|
87
90
|
organization = fields.PublishAsField(_("Publish as"))
|
udata/harvest/tasks.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from flask import current_app
|
|
2
|
-
|
|
3
1
|
from udata.tasks import get_logger, job, task
|
|
4
2
|
|
|
5
3
|
from . import backends
|
|
@@ -16,7 +14,7 @@ def harvest(self, ident):
|
|
|
16
14
|
if source.deleted or not source.active:
|
|
17
15
|
log.info('Ignoring inactive or deleted source "%s"', source.id)
|
|
18
16
|
return # Ignore deleted and inactive sources
|
|
19
|
-
Backend = backends.
|
|
17
|
+
Backend = backends.get_backend(source.backend)
|
|
20
18
|
backend = Backend(source)
|
|
21
19
|
|
|
22
20
|
backend.harvest()
|
|
@@ -27,7 +25,7 @@ def harvest_job_item(job_id, item_id):
|
|
|
27
25
|
log.info('Harvesting item %s for job "%s"', item_id, job_id)
|
|
28
26
|
|
|
29
27
|
job = HarvestJob.objects.get(pk=job_id)
|
|
30
|
-
Backend = backends.
|
|
28
|
+
Backend = backends.get_backend(job.source.backend)
|
|
31
29
|
backend = Backend(job)
|
|
32
30
|
|
|
33
31
|
item = next(i for i in job.items if i.remote_id == item_id)
|
|
@@ -40,7 +38,7 @@ def harvest_job_item(job_id, item_id):
|
|
|
40
38
|
def harvest_job_finalize(results, job_id):
|
|
41
39
|
log.info('Finalize harvesting for job "%s"', job_id)
|
|
42
40
|
job = HarvestJob.objects.get(pk=job_id)
|
|
43
|
-
Backend = backends.
|
|
41
|
+
Backend = backends.get_backend(job.source.backend)
|
|
44
42
|
backend = Backend(job)
|
|
45
43
|
backend.finalize()
|
|
46
44
|
|
|
@@ -364,7 +364,7 @@ def empty_extras(resource_data):
|
|
|
364
364
|
##############################################################################
|
|
365
365
|
|
|
366
366
|
|
|
367
|
-
@pytest.mark.options(
|
|
367
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
|
|
368
368
|
class CkanBackendTest(PytestOnlyDBTestCase):
|
|
369
369
|
@pytest.mark.ckan_data("minimal")
|
|
370
370
|
def test_minimal_metadata(self, data, result, kwargs):
|
|
@@ -505,7 +505,7 @@ class CkanBackendTest(PytestOnlyDBTestCase):
|
|
|
505
505
|
##############################################################################
|
|
506
506
|
|
|
507
507
|
|
|
508
|
-
@pytest.mark.options(
|
|
508
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
|
|
509
509
|
class CkanBackendEdgeCasesTest(PytestOnlyDBTestCase):
|
|
510
510
|
def test_minimal_ckan_response(self, rmock):
|
|
511
511
|
"""CKAN Harvester should accept the minimum dataset payload"""
|
|
@@ -13,7 +13,7 @@ API_URL = "{}api/3/action/package_list".format(CKAN_URL)
|
|
|
13
13
|
STATUS_CODE = (400, 500)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@pytest.mark.options(
|
|
16
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
|
|
17
17
|
class CkanBackendErrorsTest(PytestOnlyDBTestCase):
|
|
18
18
|
@pytest.mark.parametrize("code", STATUS_CODE)
|
|
19
19
|
def test_html_error(self, rmock, code):
|
|
@@ -8,7 +8,7 @@ from udata.tests.api import PytestOnlyDBTestCase
|
|
|
8
8
|
from udata.utils import faker
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
@pytest.mark.options(
|
|
11
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
|
|
12
12
|
class CkanBackendFilterTest(PytestOnlyDBTestCase):
|
|
13
13
|
def test_include_org_filter(self, ckan, rmock):
|
|
14
14
|
source = HarvestSourceFactory(
|
|
@@ -16,7 +16,7 @@ def data_path(filename):
|
|
|
16
16
|
return os.path.join(os.path.dirname(__file__), "data", filename)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@pytest.mark.options(
|
|
19
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["dkan"])
|
|
20
20
|
class DkanBackendTest(PytestOnlyDBTestCase):
|
|
21
21
|
def test_dkan_french_w_license(self, rmock):
|
|
22
22
|
"""CKAN Harvester should accept the minimum dataset payload"""
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<dct:title>bureau-de-vote-vanves.csv</dct:title>
|
|
38
38
|
<dct:description>Bureaux de vote - Vanves (csv)</dct:description>
|
|
39
39
|
<dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/csv?use_labels=true"/>
|
|
40
|
-
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/
|
|
40
|
+
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/5dd4e0b2-4d96-4f36-b73e-b78ec993703c"/>
|
|
41
41
|
<dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
|
|
42
42
|
<dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
|
|
43
43
|
<dct:rights>License Not Specified</dct:rights>
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
<dct:title>bureau-de-vote-vanves.geojson</dct:title>
|
|
63
63
|
<dct:description>Bureaux de vote - Vanves (geojson)</dct:description>
|
|
64
64
|
<dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/geojson"/>
|
|
65
|
-
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/
|
|
65
|
+
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/d78d1245-6e8e-44d8-88cd-87b1dd66034f"/>
|
|
66
66
|
<dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
|
|
67
67
|
<dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
|
|
68
68
|
<dct:rights>License Not Specified</dct:rights>
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
<dct:title>vfe_public_219200755_20240506.json</dct:title>
|
|
87
87
|
<dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc (json)</dct:description>
|
|
88
88
|
<dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/vfe_public_219200755_20240506/exports/json"/>
|
|
89
|
-
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/
|
|
89
|
+
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/482677b8-379c-45a5-83ef-0361fecb4cc3"/>
|
|
90
90
|
<dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
|
|
91
91
|
<dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
|
|
92
92
|
<dct:rights>License Not Specified</dct:rights>
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
<dct:title>bureau-de-vote-vanves.zip</dct:title>
|
|
100
100
|
<dct:description>Bureaux de vote - Vanves (shp)</dct:description>
|
|
101
101
|
<dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/shp"/>
|
|
102
|
-
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/
|
|
102
|
+
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a"/>
|
|
103
103
|
<dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
|
|
104
104
|
<dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
|
|
105
105
|
<dct:rights>License Not Specified</dct:rights>
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
<dct:title>vfe_public_219200755_20240506.csv</dct:title>
|
|
113
113
|
<dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc (csv)</dct:description>
|
|
114
114
|
<dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/vfe_public_219200755_20240506/exports/csv?use_labels=true"/>
|
|
115
|
-
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/
|
|
115
|
+
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/aab4d337-a617-4c18-a045-291d68787a7d"/>
|
|
116
116
|
<dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
|
|
117
117
|
<dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
|
|
118
118
|
<dct:rights>License Not Specified</dct:rights>
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
<dct:title>bureau-de-vote-vanves.json</dct:title>
|
|
162
162
|
<dct:description>Bureaux de vote - Vanves (json)</dct:description>
|
|
163
163
|
<dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/json"/>
|
|
164
|
-
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/
|
|
164
|
+
<dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/0f80d285-72f8-49f8-a691-1dad6bd2f6db"/>
|
|
165
165
|
<dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
|
|
166
166
|
<dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
|
|
167
167
|
<dct:rights>License Not Specified</dct:rights>
|
udata/harvest/tests/factories.py
CHANGED
|
@@ -79,4 +79,4 @@ class MockBackendsMixin(object):
|
|
|
79
79
|
@pytest.fixture(autouse=True)
|
|
80
80
|
def mock_backend(self, mocker):
|
|
81
81
|
return_value = {"factory": FactoryBackend}
|
|
82
|
-
mocker.patch("udata.harvest.backends.
|
|
82
|
+
mocker.patch("udata.harvest.backends.get_all_backends", return_value=return_value)
|
|
@@ -15,13 +15,14 @@ from udata.core.dataset.factories import DatasetFactory
|
|
|
15
15
|
from udata.core.dataset.models import HarvestDatasetMetadata
|
|
16
16
|
from udata.core.organization.factories import OrganizationFactory
|
|
17
17
|
from udata.core.user.factories import UserFactory
|
|
18
|
+
from udata.harvest.backends import get_enabled_backends
|
|
19
|
+
from udata.harvest.backends.base import BaseBackend
|
|
18
20
|
from udata.models import Dataset, PeriodicTask
|
|
19
21
|
from udata.tests.api import PytestOnlyDBTestCase
|
|
20
22
|
from udata.tests.helpers import assert_emit, assert_equal_dates, assert_not_emit
|
|
21
23
|
from udata.utils import faker
|
|
22
24
|
|
|
23
25
|
from .. import actions, signals
|
|
24
|
-
from ..backends import BaseBackend
|
|
25
26
|
from ..models import (
|
|
26
27
|
VALIDATION_ACCEPTED,
|
|
27
28
|
VALIDATION_PENDING,
|
|
@@ -42,9 +43,10 @@ from .factories import (
|
|
|
42
43
|
log = logging.getLogger(__name__)
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
class HarvestActionsTest(PytestOnlyDBTestCase):
|
|
46
|
+
class HarvestActionsTest(MockBackendsMixin, PytestOnlyDBTestCase):
|
|
46
47
|
def test_list_backends(self):
|
|
47
|
-
|
|
48
|
+
assert len(get_enabled_backends()) > 0
|
|
49
|
+
for backend in get_enabled_backends().values():
|
|
48
50
|
assert issubclass(backend, BaseBackend)
|
|
49
51
|
|
|
50
52
|
def test_list_sources(self):
|
udata/harvest/tests/test_api.py
CHANGED
|
@@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
|
|
|
7
7
|
|
|
8
8
|
from udata.core.organization.factories import OrganizationFactory
|
|
9
9
|
from udata.core.user.factories import AdminFactory, UserFactory
|
|
10
|
+
from udata.harvest.backends import get_enabled_backends
|
|
10
11
|
from udata.models import Member, PeriodicTask
|
|
11
12
|
from udata.tests.api import PytestOnlyAPITestCase
|
|
12
13
|
from udata.tests.helpers import assert200, assert201, assert204, assert400, assert403, assert404
|
|
@@ -31,7 +32,7 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
|
|
|
31
32
|
"""It should fetch the harvest backends list from the API"""
|
|
32
33
|
response = api.get(url_for("api.harvest_backends"))
|
|
33
34
|
assert200(response)
|
|
34
|
-
assert len(response.json) == len(
|
|
35
|
+
assert len(response.json) == len(get_enabled_backends())
|
|
35
36
|
for data in response.json:
|
|
36
37
|
assert "id" in data
|
|
37
38
|
assert "label" in data
|
|
@@ -29,6 +29,8 @@ def gen_remote_IDs(num: int, prefix: str = "") -> list[str]:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class FakeBackend(BaseBackend):
|
|
32
|
+
name = "fake-backend"
|
|
33
|
+
display_name = "Fake Backend"
|
|
32
34
|
filters = (
|
|
33
35
|
HarvestFilter("First filter", "first", str),
|
|
34
36
|
HarvestFilter("Second filter", "second", str),
|
|
@@ -68,7 +68,7 @@ def mock_csw_pagination(rmock, path, pattern):
|
|
|
68
68
|
return url
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
@pytest.mark.options(
|
|
71
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["dcat"])
|
|
72
72
|
class DcatBackendTest(PytestOnlyDBTestCase):
|
|
73
73
|
def test_simple_flat(self, rmock):
|
|
74
74
|
filename = "flat.jsonld"
|
|
@@ -926,7 +926,7 @@ class DcatBackendTest(PytestOnlyDBTestCase):
|
|
|
926
926
|
assert "404 Client Error" in job.errors[0].message
|
|
927
927
|
|
|
928
928
|
|
|
929
|
-
@pytest.mark.options(
|
|
929
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["csw*"])
|
|
930
930
|
class CswDcatBackendTest(PytestOnlyDBTestCase):
|
|
931
931
|
def test_geonetworkv4(self, rmock):
|
|
932
932
|
url = mock_csw_pagination(rmock, "geonetwork/srv/eng/csw.rdf", "geonetworkv4-page-{}.xml")
|
|
@@ -1076,7 +1076,7 @@ class CswDcatBackendTest(PytestOnlyDBTestCase):
|
|
|
1076
1076
|
assert len(job.items) == 1
|
|
1077
1077
|
|
|
1078
1078
|
|
|
1079
|
-
@pytest.mark.options(
|
|
1079
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["csw*"])
|
|
1080
1080
|
class CswIso19139DcatBackendTest(PytestOnlyDBTestCase):
|
|
1081
1081
|
@pytest.mark.parametrize(
|
|
1082
1082
|
"remote_url_prefix",
|
udata/i18n.py
CHANGED
|
@@ -1,49 +1,29 @@
|
|
|
1
|
-
import importlib.util
|
|
2
1
|
from contextlib import contextmanager
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from os.path import basename, dirname, join
|
|
2
|
+
from importlib import resources
|
|
3
|
+
from importlib.metadata import entry_points
|
|
6
4
|
|
|
7
5
|
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
|
|
6
|
+
from flask import current_app, g, request
|
|
7
|
+
from flask_babel import Babel, refresh
|
|
8
|
+
from flask_login import current_user
|
|
21
9
|
from werkzeug.local import LocalProxy
|
|
22
10
|
|
|
23
|
-
from udata import entrypoints
|
|
24
11
|
from udata.app import Blueprint
|
|
25
|
-
from udata.auth import current_user
|
|
26
12
|
from udata.errors import ConfigError
|
|
27
|
-
from udata.utils import multi_to_dict
|
|
28
13
|
|
|
29
14
|
|
|
30
15
|
def get_translation_directories_and_domains():
|
|
31
|
-
|
|
16
|
+
translations_dirs = []
|
|
32
17
|
domains = []
|
|
33
18
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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))
|
|
19
|
+
for pkg in entry_points(group="udata.i18n"):
|
|
20
|
+
module = pkg.load()
|
|
21
|
+
path = resources.files(module)
|
|
22
|
+
# `/ ""` is here to transform MultiplexedPath to a simple str
|
|
23
|
+
translations_dirs.append(str(path / ""))
|
|
24
|
+
domains.append(pkg.name)
|
|
45
25
|
|
|
46
|
-
return
|
|
26
|
+
return translations_dirs, domains
|
|
47
27
|
|
|
48
28
|
|
|
49
29
|
def get_locale():
|
|
@@ -89,22 +69,6 @@ def lazy_pgettext(*args, **kwargs):
|
|
|
89
69
|
return flask_babel.lazy_pgettext(*args, **kwargs)
|
|
90
70
|
|
|
91
71
|
|
|
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
72
|
def _default_lang(user=None):
|
|
109
73
|
lang = getattr(user or current_user, "prefered_language", None)
|
|
110
74
|
return lang or current_app.config["DEFAULT_LANGUAGE"]
|
|
@@ -152,228 +116,5 @@ def init_app(app):
|
|
|
152
116
|
)
|
|
153
117
|
|
|
154
118
|
|
|
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
119
|
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
|
-
)
|
|
120
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Delete Topic index 'name_text'"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from mongoengine.connection import get_db
|
|
6
|
+
from pymongo.errors import OperationFailure
|
|
7
|
+
|
|
8
|
+
from udata.models import User
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def migrate(db):
|
|
14
|
+
log.info("Deleting index…")
|
|
15
|
+
|
|
16
|
+
collection = get_db().user
|
|
17
|
+
|
|
18
|
+
# Remove previous index
|
|
19
|
+
try:
|
|
20
|
+
collection.drop_index("slug_text")
|
|
21
|
+
except OperationFailure:
|
|
22
|
+
log.info("Index `slug_text` does not exist?", exc_info=True)
|
|
23
|
+
|
|
24
|
+
# Force new index creation (before old code in workers recreate it?)
|
|
25
|
+
User.objects.first()
|
udata/models/__init__.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
from mongoengine.errors import ValidationError # noqa
|
|
2
|
-
|
|
3
|
-
from udata import entrypoints # noqa
|
|
4
1
|
from udata.mongo import * # noqa
|
|
5
2
|
|
|
6
3
|
# Load all core models and mixins
|
|
@@ -26,11 +23,6 @@ from udata.core.dataservices.models import * # noqa
|
|
|
26
23
|
from udata.core.pages.models import * # noqa
|
|
27
24
|
|
|
28
25
|
from udata.features.transfer.models import * # noqa
|
|
29
|
-
from udata.features.territories.models import * # noqa
|
|
30
26
|
|
|
31
27
|
# Load HarvestSource model as harvest for catalog
|
|
32
28
|
from udata.harvest.models import HarvestSource as Harvest # noqa
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def init_app(app):
|
|
36
|
-
entrypoints.get_enabled("udata.models", app)
|
udata/routing.py
CHANGED
|
@@ -11,7 +11,6 @@ from udata import models
|
|
|
11
11
|
from udata.core.dataservices.models import Dataservice
|
|
12
12
|
from udata.core.spatial.models import GeoZone
|
|
13
13
|
from udata.harvest.models import HarvestSource
|
|
14
|
-
from udata.i18n import ISO_639_1_CODES
|
|
15
14
|
from udata.mongo import db
|
|
16
15
|
from udata.uris import cdata_url, homepage_url
|
|
17
16
|
|
|
@@ -23,12 +22,6 @@ class LazyRedirect(object):
|
|
|
23
22
|
self.arg = arg
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
class LanguagePrefixConverter(BaseConverter):
|
|
27
|
-
def __init__(self, map):
|
|
28
|
-
super(LanguagePrefixConverter, self).__init__(map)
|
|
29
|
-
self.regex = "(?:%s)" % "|".join(ISO_639_1_CODES)
|
|
30
|
-
|
|
31
|
-
|
|
32
25
|
class ListConverter(BaseConverter):
|
|
33
26
|
def to_python(self, value):
|
|
34
27
|
return value.split(",")
|
|
@@ -241,7 +234,6 @@ def lazy_raise_or_redirect():
|
|
|
241
234
|
|
|
242
235
|
def init_app(app):
|
|
243
236
|
app.before_request(lazy_raise_or_redirect)
|
|
244
|
-
app.url_map.converters["lang"] = LanguagePrefixConverter
|
|
245
237
|
app.url_map.converters["list"] = ListConverter
|
|
246
238
|
app.url_map.converters["pathlist"] = PathListConverter
|
|
247
239
|
app.url_map.converters["uuid"] = UUIDConverter
|