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
udata/harvest/filters.py
CHANGED
|
@@ -3,6 +3,9 @@ from voluptuous import Invalid
|
|
|
3
3
|
|
|
4
4
|
from udata import tags, uris
|
|
5
5
|
|
|
6
|
+
TRUTHY_STRINGS = ("on", "t", "true", "y", "yes", "1")
|
|
7
|
+
FALSY_STRINGS = ("f", "false", "n", "no", "off", "0")
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
def boolean(value):
|
|
8
11
|
"""
|
|
@@ -15,17 +18,25 @@ def boolean(value):
|
|
|
15
18
|
if value is None or isinstance(value, bool):
|
|
16
19
|
return value
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
return bool(
|
|
20
|
-
|
|
21
|
+
if isinstance(value, int):
|
|
22
|
+
return bool(value)
|
|
23
|
+
|
|
24
|
+
if isinstance(value, str):
|
|
21
25
|
lower_value = value.strip().lower()
|
|
26
|
+
|
|
22
27
|
if not lower_value:
|
|
23
28
|
return None
|
|
24
|
-
if lower_value in
|
|
29
|
+
if lower_value in FALSY_STRINGS:
|
|
25
30
|
return False
|
|
26
|
-
if lower_value in
|
|
31
|
+
if lower_value in TRUTHY_STRINGS:
|
|
27
32
|
return True
|
|
28
|
-
raise Invalid(
|
|
33
|
+
raise Invalid(
|
|
34
|
+
f"Unable to parse string '{value}' as boolean. Supported values are {','.join(TRUTHY_STRINGS)} for `True` and {','.join(FALSY_STRINGS)} for `False`."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
raise Invalid(
|
|
38
|
+
f"Cannot convert value {value} of type {type(value)} to boolean. Supported types are `bool`, `int` and `str`"
|
|
39
|
+
)
|
|
29
40
|
|
|
30
41
|
|
|
31
42
|
def to_date(value):
|
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/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)
|
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
|
|
|
@@ -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()
|
|
@@ -364,7 +382,7 @@ def empty_extras(resource_data):
|
|
|
364
382
|
##############################################################################
|
|
365
383
|
|
|
366
384
|
|
|
367
|
-
@pytest.mark.options(
|
|
385
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
|
|
368
386
|
class CkanBackendTest(PytestOnlyDBTestCase):
|
|
369
387
|
@pytest.mark.ckan_data("minimal")
|
|
370
388
|
def test_minimal_metadata(self, data, result, kwargs):
|
|
@@ -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()
|
|
@@ -505,7 +538,7 @@ class CkanBackendTest(PytestOnlyDBTestCase):
|
|
|
505
538
|
##############################################################################
|
|
506
539
|
|
|
507
540
|
|
|
508
|
-
@pytest.mark.options(
|
|
541
|
+
@pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
|
|
509
542
|
class CkanBackendEdgeCasesTest(PytestOnlyDBTestCase):
|
|
510
543
|
def test_minimal_ckan_response(self, rmock):
|
|
511
544
|
"""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)
|
|
@@ -11,17 +11,18 @@ 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
|
+
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):
|
|
@@ -269,7 +271,16 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
|
|
|
269
271
|
assert periodic_task.crontab.day_of_month == "*"
|
|
270
272
|
assert periodic_task.crontab.month_of_year == "*"
|
|
271
273
|
assert periodic_task.enabled
|
|
272
|
-
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
|
|
273
284
|
|
|
274
285
|
def test_schedule_from_cron(self):
|
|
275
286
|
source = HarvestSourceFactory()
|
|
@@ -286,7 +297,7 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
|
|
|
286
297
|
assert periodic_task.crontab.month_of_year == "3"
|
|
287
298
|
assert periodic_task.crontab.day_of_week == "sunday"
|
|
288
299
|
assert periodic_task.enabled
|
|
289
|
-
assert periodic_task.name == "Harvest {
|
|
300
|
+
assert periodic_task.name == f"Harvest {source.name} ({source.id})"
|
|
290
301
|
|
|
291
302
|
def test_reschedule(self):
|
|
292
303
|
source = HarvestSourceFactory()
|
|
@@ -306,7 +317,7 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
|
|
|
306
317
|
assert periodic_task.crontab.day_of_month == "*"
|
|
307
318
|
assert periodic_task.crontab.month_of_year == "*"
|
|
308
319
|
assert periodic_task.enabled
|
|
309
|
-
assert periodic_task.name == "Harvest {
|
|
320
|
+
assert periodic_task.name == f"Harvest {source.name} ({source.id})"
|
|
310
321
|
|
|
311
322
|
def test_unschedule(self):
|
|
312
323
|
periodic_task = PeriodicTask.objects.create(
|
|
@@ -449,6 +460,50 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
|
|
|
449
460
|
assert result.success == len(datasets)
|
|
450
461
|
assert result.errors == 1
|
|
451
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
|
+
|
|
452
507
|
|
|
453
508
|
class ExecutionTestMixin(MockBackendsMixin, PytestOnlyDBTestCase):
|
|
454
509
|
def action(self, *args, **kwargs):
|