udata 14.0.0__py3-none-any.whl → 14.5.1.dev6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api/__init__.py +2 -0
- udata/api_fields.py +35 -4
- udata/app.py +18 -20
- udata/auth/__init__.py +29 -6
- udata/auth/forms.py +2 -2
- udata/auth/views.py +13 -6
- udata/commands/dcat.py +1 -1
- udata/commands/serve.py +3 -11
- udata/commands/tests/test_fixtures.py +9 -9
- udata/core/access_type/api.py +1 -1
- udata/core/access_type/constants.py +12 -8
- udata/core/activity/api.py +5 -6
- udata/core/badges/tests/test_commands.py +6 -6
- udata/core/csv.py +5 -0
- udata/core/dataservices/api.py +8 -1
- udata/core/dataservices/apiv2.py +2 -5
- udata/core/dataservices/models.py +5 -2
- udata/core/dataservices/rdf.py +2 -1
- udata/core/dataservices/tasks.py +13 -2
- udata/core/dataset/api.py +10 -0
- udata/core/dataset/models.py +6 -6
- udata/core/dataset/permissions.py +31 -0
- udata/core/dataset/rdf.py +8 -2
- udata/core/dataset/tasks.py +23 -7
- udata/core/discussions/api.py +15 -1
- udata/core/discussions/models.py +6 -0
- udata/core/legal/__init__.py +0 -0
- udata/core/legal/mails.py +128 -0
- udata/core/organization/api.py +16 -5
- udata/core/organization/apiv2.py +2 -3
- udata/core/organization/mails.py +1 -1
- udata/core/organization/models.py +15 -2
- udata/core/organization/notifications.py +84 -0
- udata/core/organization/permissions.py +1 -1
- udata/core/organization/tasks.py +3 -0
- udata/core/pages/tests/test_api.py +32 -0
- udata/core/post/api.py +24 -69
- udata/core/post/models.py +84 -16
- udata/core/post/tests/test_api.py +24 -1
- udata/core/reports/api.py +18 -0
- udata/core/reports/models.py +42 -2
- udata/core/reuse/api.py +8 -0
- udata/core/reuse/apiv2.py +2 -5
- udata/core/reuse/models.py +1 -1
- udata/core/reuse/tasks.py +7 -0
- udata/core/spatial/forms.py +2 -2
- udata/core/topic/models.py +8 -2
- udata/core/user/api.py +10 -3
- udata/core/user/models.py +12 -2
- udata/features/notifications/api.py +7 -18
- udata/features/notifications/models.py +56 -0
- udata/features/notifications/tasks.py +25 -0
- udata/flask_mongoengine/engine.py +0 -4
- udata/flask_mongoengine/pagination.py +1 -1
- udata/frontend/markdown.py +2 -1
- udata/harvest/actions.py +21 -1
- udata/harvest/api.py +25 -8
- udata/harvest/backends/base.py +27 -1
- udata/harvest/backends/ckan/harvesters.py +11 -2
- udata/harvest/backends/dcat.py +4 -1
- udata/harvest/commands.py +33 -0
- udata/harvest/filters.py +17 -6
- udata/harvest/models.py +16 -0
- udata/harvest/permissions.py +27 -0
- udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
- udata/harvest/tests/test_actions.py +58 -5
- udata/harvest/tests/test_api.py +276 -122
- udata/harvest/tests/test_base_backend.py +86 -1
- udata/harvest/tests/test_dcat_backend.py +81 -10
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +1 -4
- udata/mail.py +19 -1
- udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
- udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
- udata/mongo/slug_fields.py +1 -1
- udata/rdf.py +58 -10
- udata/routing.py +2 -2
- udata/settings.py +11 -0
- udata/tasks.py +1 -0
- udata/templates/mail/message.html +5 -31
- udata/tests/__init__.py +27 -2
- udata/tests/api/__init__.py +108 -21
- udata/tests/api/test_activities_api.py +36 -0
- udata/tests/api/test_auth_api.py +121 -95
- udata/tests/api/test_base_api.py +7 -4
- udata/tests/api/test_datasets_api.py +50 -19
- udata/tests/api/test_organizations_api.py +192 -197
- udata/tests/api/test_reports_api.py +157 -0
- udata/tests/api/test_reuses_api.py +147 -147
- udata/tests/api/test_security_api.py +12 -12
- udata/tests/api/test_swagger.py +4 -4
- udata/tests/api/test_tags_api.py +8 -8
- udata/tests/api/test_user_api.py +1 -1
- udata/tests/apiv2/test_search.py +30 -0
- udata/tests/apiv2/test_swagger.py +4 -4
- udata/tests/cli/test_cli_base.py +8 -9
- udata/tests/dataservice/test_dataservice_tasks.py +29 -0
- udata/tests/dataset/test_dataset_commands.py +4 -4
- udata/tests/dataset/test_dataset_model.py +66 -26
- udata/tests/dataset/test_dataset_rdf.py +99 -5
- udata/tests/dataset/test_dataset_tasks.py +25 -0
- udata/tests/frontend/test_auth.py +58 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +31 -27
- udata/tests/organization/test_notifications.py +67 -2
- udata/tests/plugin.py +6 -261
- udata/tests/search/test_search_integration.py +33 -0
- udata/tests/site/test_site_csv_exports.py +22 -10
- udata/tests/test_activity.py +9 -9
- udata/tests/test_api_fields.py +10 -0
- udata/tests/test_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- udata/tests/test_legal_mails.py +359 -0
- udata/tests/test_migrations.py +21 -21
- udata/tests/test_notifications.py +15 -57
- udata/tests/test_notifications_task.py +43 -0
- udata/tests/test_owned.py +81 -1
- udata/tests/test_storages.py +25 -19
- udata/tests/test_topics.py +77 -61
- udata/tests/test_uris.py +33 -0
- udata/tests/workers/test_jobs_commands.py +23 -23
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +187 -108
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +187 -108
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +187 -108
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +188 -109
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +187 -108
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +187 -108
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +187 -108
- udata/translations/udata.pot +215 -106
- udata/uris.py +0 -2
- udata-14.5.1.dev6.dist-info/METADATA +109 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/RECORD +143 -140
- udata/core/post/forms.py +0 -30
- udata/flask_mongoengine/json.py +0 -38
- udata/templates/mail/base.html +0 -105
- udata/templates/mail/base.txt +0 -6
- udata/templates/mail/button.html +0 -3
- udata/templates/mail/layouts/1-column.html +0 -19
- udata/templates/mail/layouts/2-columns.html +0 -20
- udata/templates/mail/layouts/center-panel.html +0 -16
- udata-14.0.0.dist-info/METADATA +0 -132
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/WHEEL +0 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/entry_points.txt +0 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/licenses/LICENSE +0 -0
- {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/top_level.txt +0 -0
|
@@ -12,12 +12,12 @@ from udata.tests.helpers import capture_mails
|
|
|
12
12
|
|
|
13
13
|
class SecurityAPITest(PytestOnlyAPITestCase):
|
|
14
14
|
@pytest.mark.options(CAPTCHETAT_BASE_URL=None)
|
|
15
|
-
def test_register(self
|
|
15
|
+
def test_register(self):
|
|
16
16
|
# We cannot test for mail sending since they are sent with Flask
|
|
17
17
|
# directly and not with our system but if the sending is working
|
|
18
18
|
# we test the rendering of the mail.
|
|
19
19
|
|
|
20
|
-
response =
|
|
20
|
+
response = self.post(
|
|
21
21
|
url_for("security.register"),
|
|
22
22
|
{
|
|
23
23
|
"first_name": "Jane",
|
|
@@ -32,13 +32,13 @@ class SecurityAPITest(PytestOnlyAPITestCase):
|
|
|
32
32
|
self.assertStatus(response, 200)
|
|
33
33
|
|
|
34
34
|
@pytest.mark.options(CAPTCHETAT_BASE_URL=None, SECURITY_RETURN_GENERIC_RESPONSES=True)
|
|
35
|
-
def test_register_existing(self
|
|
35
|
+
def test_register_existing(self):
|
|
36
36
|
# We cannot test for mail sending since they are sent with Flask
|
|
37
37
|
# directly and not with our system but if the sending is working
|
|
38
38
|
# we test the rendering of the mail.
|
|
39
39
|
|
|
40
40
|
UserFactory(email="jane@example.org", confirmed_at=datetime.now())
|
|
41
|
-
response =
|
|
41
|
+
response = self.post(
|
|
42
42
|
url_for("security.register"),
|
|
43
43
|
{
|
|
44
44
|
"first_name": "Jane",
|
|
@@ -53,20 +53,20 @@ class SecurityAPITest(PytestOnlyAPITestCase):
|
|
|
53
53
|
self.assertStatus(response, 200)
|
|
54
54
|
|
|
55
55
|
@pytest.mark.options(CAPTCHETAT_BASE_URL=None)
|
|
56
|
-
def test_ask_for_reset(self
|
|
56
|
+
def test_ask_for_reset(self):
|
|
57
57
|
# We cannot test for mail sending since they are sent with Flask
|
|
58
58
|
# directly and not with our system but if the sending is working
|
|
59
59
|
# we test the rendering of the mail.
|
|
60
60
|
|
|
61
61
|
UserFactory(email="jane@example.org", confirmed_at=datetime.now())
|
|
62
62
|
|
|
63
|
-
response =
|
|
63
|
+
response = self.post(
|
|
64
64
|
url_for("security.forgot_password"), {"email": "jane@example.org", "submit": True}
|
|
65
65
|
)
|
|
66
66
|
self.assertStatus(response, 200)
|
|
67
67
|
|
|
68
68
|
@pytest.mark.options(CAPTCHETAT_BASE_URL=None)
|
|
69
|
-
def test_change_notice_mail(self
|
|
69
|
+
def test_change_notice_mail(self):
|
|
70
70
|
# We cannot test for mail sending since they are sent with Flask
|
|
71
71
|
# directly and not with our system but if the sending is working
|
|
72
72
|
# we test the rendering of the mail.
|
|
@@ -76,7 +76,7 @@ class SecurityAPITest(PytestOnlyAPITestCase):
|
|
|
76
76
|
)
|
|
77
77
|
self.login(user)
|
|
78
78
|
|
|
79
|
-
response =
|
|
79
|
+
response = self.post(
|
|
80
80
|
url_for("security.change_password"),
|
|
81
81
|
{
|
|
82
82
|
"password": "password",
|
|
@@ -88,12 +88,12 @@ class SecurityAPITest(PytestOnlyAPITestCase):
|
|
|
88
88
|
self.assertStatus(response, 200)
|
|
89
89
|
|
|
90
90
|
@pytest.mark.options(CAPTCHETAT_BASE_URL=None)
|
|
91
|
-
def test_change_email_confirmation(self
|
|
91
|
+
def test_change_email_confirmation(self):
|
|
92
92
|
user = UserFactory(email="jane@example.org", confirmed_at=datetime.now())
|
|
93
93
|
self.login(user)
|
|
94
94
|
|
|
95
95
|
with capture_mails() as mails:
|
|
96
|
-
response =
|
|
96
|
+
response = self.post(
|
|
97
97
|
url_for("security.change_email"),
|
|
98
98
|
{
|
|
99
99
|
"new_email": "jane2@example.org",
|
|
@@ -109,11 +109,11 @@ class SecurityAPITest(PytestOnlyAPITestCase):
|
|
|
109
109
|
assert mails[0].subject == _("Confirm your email address")
|
|
110
110
|
|
|
111
111
|
@pytest.mark.options(CAPTCHETAT_BASE_URL=None, SECURITY_RETURN_GENERIC_RESPONSES=True)
|
|
112
|
-
def test_reset_password(self
|
|
112
|
+
def test_reset_password(self):
|
|
113
113
|
user = UserFactory(email="jane@example.org", confirmed_at=datetime.now())
|
|
114
114
|
token = generate_reset_password_token(user)
|
|
115
115
|
|
|
116
|
-
response =
|
|
116
|
+
response = self.post(
|
|
117
117
|
url_for("security.reset_password", token=token),
|
|
118
118
|
{
|
|
119
119
|
"password": "Password123",
|
udata/tests/api/test_swagger.py
CHANGED
|
@@ -8,16 +8,16 @@ from udata.tests.helpers import assert200
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SwaggerBlueprintTest(PytestOnlyAPITestCase):
|
|
11
|
-
def test_swagger_resource_type(self
|
|
12
|
-
response =
|
|
11
|
+
def test_swagger_resource_type(self):
|
|
12
|
+
response = self.get(url_for("api.specs"))
|
|
13
13
|
assert200(response)
|
|
14
14
|
swagger = json.loads(response.data)
|
|
15
15
|
expected = swagger["paths"]["/datasets/{dataset}/resources/"]
|
|
16
16
|
expected = expected["put"]["responses"]["200"]["schema"]["type"]
|
|
17
17
|
assert expected == "array"
|
|
18
18
|
|
|
19
|
-
def test_swagger_specs_validate(self
|
|
20
|
-
response =
|
|
19
|
+
def test_swagger_specs_validate(self):
|
|
20
|
+
response = self.get(url_for("api.specs"))
|
|
21
21
|
try:
|
|
22
22
|
schemas.validate(response.json)
|
|
23
23
|
except schemas.SchemaValidationError as e:
|
udata/tests/api/test_tags_api.py
CHANGED
|
@@ -9,7 +9,7 @@ from udata.utils import faker
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TagsAPITest(PytestOnlyAPITestCase):
|
|
12
|
-
def test_suggest_tags_api(self
|
|
12
|
+
def test_suggest_tags_api(self):
|
|
13
13
|
"""It should suggest tags"""
|
|
14
14
|
for i in range(3):
|
|
15
15
|
tags = [faker.tag(), faker.tag(), "test", "test-{0}".format(i)]
|
|
@@ -18,7 +18,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
|
|
|
18
18
|
|
|
19
19
|
count_tags()
|
|
20
20
|
|
|
21
|
-
response =
|
|
21
|
+
response = self.get(url_for("api.suggest_tags", q="tes", size=5))
|
|
22
22
|
assert200(response)
|
|
23
23
|
|
|
24
24
|
assert len(response.json) <= 5
|
|
@@ -29,7 +29,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
|
|
|
29
29
|
assert "text" in suggestion
|
|
30
30
|
assert "tes" in suggestion["text"]
|
|
31
31
|
|
|
32
|
-
def test_suggest_tags_api_with_unicode(self
|
|
32
|
+
def test_suggest_tags_api_with_unicode(self):
|
|
33
33
|
"""It should suggest tags"""
|
|
34
34
|
for i in range(3):
|
|
35
35
|
tags = [faker.tag(), faker.tag(), "testé", "testé-{0}".format(i)]
|
|
@@ -38,7 +38,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
|
|
|
38
38
|
|
|
39
39
|
count_tags()
|
|
40
40
|
|
|
41
|
-
response =
|
|
41
|
+
response = self.get(url_for("api.suggest_tags", q="testé", size=5))
|
|
42
42
|
assert200(response)
|
|
43
43
|
|
|
44
44
|
assert len(response.json) <= 5
|
|
@@ -49,7 +49,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
|
|
|
49
49
|
assert "text" in suggestion
|
|
50
50
|
assert "teste" in suggestion["text"]
|
|
51
51
|
|
|
52
|
-
def test_suggest_tags_api_no_match(self
|
|
52
|
+
def test_suggest_tags_api_no_match(self):
|
|
53
53
|
"""It should not provide tag suggestion if no match"""
|
|
54
54
|
for i in range(3):
|
|
55
55
|
tags = ["aaaa", "aaaa-{0}".format(i)]
|
|
@@ -58,12 +58,12 @@ class TagsAPITest(PytestOnlyAPITestCase):
|
|
|
58
58
|
|
|
59
59
|
count_tags()
|
|
60
60
|
|
|
61
|
-
response =
|
|
61
|
+
response = self.get(url_for("api.suggest_tags", q="bbbb", size=5))
|
|
62
62
|
assert200(response)
|
|
63
63
|
assert len(response.json) == 0
|
|
64
64
|
|
|
65
|
-
def test_suggest_tags_api_empty(self
|
|
65
|
+
def test_suggest_tags_api_empty(self):
|
|
66
66
|
"""It should not provide tag suggestion if no data"""
|
|
67
|
-
response =
|
|
67
|
+
response = self.get(url_for("api.suggest_tags", q="bbbb", size=5))
|
|
68
68
|
assert200(response)
|
|
69
69
|
assert len(response.json) == 0
|
udata/tests/api/test_user_api.py
CHANGED
|
@@ -382,7 +382,7 @@ class UserAPITest(APITestCase):
|
|
|
382
382
|
response = self.delete(url_for("api.user", user=user_to_delete))
|
|
383
383
|
self.assertEqual(list(storages.avatars.list_files()), [])
|
|
384
384
|
self.assert204(response)
|
|
385
|
-
self.
|
|
385
|
+
self.assertEqual(len(mails), 1)
|
|
386
386
|
|
|
387
387
|
user_to_delete.reload()
|
|
388
388
|
response = self.delete(url_for("api.user", user=user_to_delete))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from udata.core.dataservices.factories import DataserviceFactory
|
|
2
|
+
from udata.core.organization.factories import OrganizationFactory
|
|
3
|
+
from udata.core.reuse.factories import ReuseFactory
|
|
4
|
+
from udata.tests.api import APITestCase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SearchAPIV2Test(APITestCase):
|
|
8
|
+
def test_dataservice_search_with_model_query_param(self):
|
|
9
|
+
"""Searching dataservices with 'model' as query param should not crash.
|
|
10
|
+
|
|
11
|
+
Regression test for: TypeError: query() got multiple values for argument 'model'
|
|
12
|
+
"""
|
|
13
|
+
DataserviceFactory.create_batch(3)
|
|
14
|
+
|
|
15
|
+
response = self.get("/api/2/dataservices/search/?model=malicious")
|
|
16
|
+
self.assert200(response)
|
|
17
|
+
|
|
18
|
+
def test_reuse_search_with_model_query_param(self):
|
|
19
|
+
"""Searching reuses with 'model' as query param should not crash."""
|
|
20
|
+
ReuseFactory.create_batch(3)
|
|
21
|
+
|
|
22
|
+
response = self.get("/api/2/reuses/search/?model=malicious")
|
|
23
|
+
self.assert200(response)
|
|
24
|
+
|
|
25
|
+
def test_organization_search_with_model_query_param(self):
|
|
26
|
+
"""Searching organizations with 'model' as query param should not crash."""
|
|
27
|
+
OrganizationFactory.create_batch(3)
|
|
28
|
+
|
|
29
|
+
response = self.get("/api/2/organizations/search/?model=malicious")
|
|
30
|
+
self.assert200(response)
|
|
@@ -8,16 +8,16 @@ from udata.tests.helpers import assert200
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SwaggerBlueprintTest(PytestOnlyAPITestCase):
|
|
11
|
-
def test_swagger_resource_type(self
|
|
12
|
-
response =
|
|
11
|
+
def test_swagger_resource_type(self):
|
|
12
|
+
response = self.get(url_for("apiv2.specs"))
|
|
13
13
|
assert200(response)
|
|
14
14
|
swagger = json.loads(response.data)
|
|
15
15
|
expected = swagger["paths"]["/datasets/{dataset}/resources/"]
|
|
16
16
|
expected = expected["get"]["responses"]["200"]["schema"]["$ref"]
|
|
17
17
|
assert expected == "#/definitions/ResourcePage"
|
|
18
18
|
|
|
19
|
-
def test_swagger_specs_validate(self
|
|
20
|
-
response =
|
|
19
|
+
def test_swagger_specs_validate(self):
|
|
20
|
+
response = self.get(url_for("apiv2.specs"))
|
|
21
21
|
try:
|
|
22
22
|
schemas.validate(response.json)
|
|
23
23
|
except schemas.SchemaValidationError as e:
|
udata/tests/cli/test_cli_base.py
CHANGED
|
@@ -2,17 +2,16 @@ from udata.tests import PytestOnlyTestCase
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class CliBaseTest(PytestOnlyTestCase):
|
|
5
|
-
def test_cli_help(self
|
|
5
|
+
def test_cli_help(self):
|
|
6
6
|
"""Should display help without errors"""
|
|
7
|
-
cli()
|
|
8
|
-
cli("
|
|
9
|
-
cli("
|
|
10
|
-
cli("--help")
|
|
7
|
+
self.cli("-?")
|
|
8
|
+
self.cli("-h")
|
|
9
|
+
self.cli("--help")
|
|
11
10
|
|
|
12
|
-
def test_cli_log_and_printing(self
|
|
11
|
+
def test_cli_log_and_printing(self):
|
|
13
12
|
"""Should properly log and print"""
|
|
14
|
-
cli("test log")
|
|
13
|
+
self.cli("test log")
|
|
15
14
|
|
|
16
|
-
def test_cli_version(self
|
|
15
|
+
def test_cli_version(self):
|
|
17
16
|
"""Should display version without errors"""
|
|
18
|
-
cli("--version")
|
|
17
|
+
self.cli("--version")
|
|
@@ -43,3 +43,32 @@ class DataserviceTasksTest(PytestOnlyDBTestCase):
|
|
|
43
43
|
assert Discussion.objects.filter(id=discussion.id).count() == 0
|
|
44
44
|
assert Follow.objects.filter(id=follower.id).count() == 0
|
|
45
45
|
assert HarvestJob.objects.filter(items__dataservice=dataservices[0].id).count() == 0
|
|
46
|
+
|
|
47
|
+
def test_purge_dataservices_cleans_all_harvest_items_references(self):
|
|
48
|
+
"""Test that purging dataservices cleans all HarvestItem references in a job.
|
|
49
|
+
|
|
50
|
+
The same dataservice can appear multiple times in a job's items (e.g. if the
|
|
51
|
+
harvest source has duplicates). The $ operator only updates the first match,
|
|
52
|
+
so we need to use $[] with array_filters to update all matches.
|
|
53
|
+
"""
|
|
54
|
+
dataservice_to_delete = Dataservice.objects.create(
|
|
55
|
+
title="delete me", base_api_url="https://example.com/api", deleted_at="2016-01-01"
|
|
56
|
+
)
|
|
57
|
+
dataservice_keep = Dataservice.objects.create(
|
|
58
|
+
title="keep me", base_api_url="https://example.com/api"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
job = HarvestJobFactory(
|
|
62
|
+
items=[
|
|
63
|
+
HarvestItem(dataservice=dataservice_to_delete, remote_id="1"),
|
|
64
|
+
HarvestItem(dataservice=dataservice_keep, remote_id="2"),
|
|
65
|
+
HarvestItem(dataservice=dataservice_to_delete, remote_id="3"),
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
tasks.purge_dataservices()
|
|
70
|
+
|
|
71
|
+
job.reload()
|
|
72
|
+
assert job.items[0].dataservice is None
|
|
73
|
+
assert job.items[1].dataservice == dataservice_keep
|
|
74
|
+
assert job.items[2].dataservice is None
|
|
@@ -5,22 +5,22 @@ from udata.tests.api import PytestOnlyDBTestCase
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class DatasetCommandTest(PytestOnlyDBTestCase):
|
|
8
|
-
def test_dataset_archive_one(self
|
|
8
|
+
def test_dataset_archive_one(self):
|
|
9
9
|
dataset = DatasetFactory()
|
|
10
10
|
|
|
11
|
-
cli("dataset", "archive-one", str(dataset.id))
|
|
11
|
+
self.cli("dataset", "archive-one", str(dataset.id))
|
|
12
12
|
|
|
13
13
|
dataset.reload()
|
|
14
14
|
assert dataset.archived is not None
|
|
15
15
|
|
|
16
|
-
def test_dataset_archive(self
|
|
16
|
+
def test_dataset_archive(self):
|
|
17
17
|
datasets = [DatasetFactory() for _ in range(2)]
|
|
18
18
|
|
|
19
19
|
with NamedTemporaryFile(mode="w", encoding="utf8") as temp:
|
|
20
20
|
temp.write("\n".join((str(d.id) for d in datasets)))
|
|
21
21
|
temp.flush()
|
|
22
22
|
|
|
23
|
-
cli("dataset", "archive", temp.name)
|
|
23
|
+
self.cli("dataset", "archive", temp.name)
|
|
24
24
|
|
|
25
25
|
for dataset in datasets:
|
|
26
26
|
dataset.reload()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
1
|
+
from datetime import date, datetime, timedelta, timezone
|
|
2
2
|
from uuid import uuid4
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
@@ -206,6 +206,18 @@ class DatasetModelTest(PytestOnlyDBTestCase):
|
|
|
206
206
|
assert dataset.quality["update_fulfilled_in_time"] is False
|
|
207
207
|
assert dataset.quality["score"] == Dataset.normalize_score(1)
|
|
208
208
|
|
|
209
|
+
def test_quality_frequency_update_with_harvest_timezone_aware(self):
|
|
210
|
+
"""Test that update_fulfilled_in_time works with timezone-aware harvest dates."""
|
|
211
|
+
dataset = DatasetFactory(
|
|
212
|
+
description="",
|
|
213
|
+
frequency=UpdateFrequency.DAILY,
|
|
214
|
+
harvest=HarvestDatasetMetadata(
|
|
215
|
+
modified_at=datetime.now(timezone.utc) - timedelta(hours=1),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
assert dataset.quality["update_frequency"] is True
|
|
219
|
+
assert dataset.quality["update_fulfilled_in_time"] is True
|
|
220
|
+
|
|
209
221
|
def test_quality_description_length(self):
|
|
210
222
|
dataset = DatasetFactory(
|
|
211
223
|
description="a" * (current_app.config.get("QUALITY_DESCRIPTION_LENGTH") - 1)
|
|
@@ -326,9 +338,11 @@ class DatasetModelTest(PytestOnlyDBTestCase):
|
|
|
326
338
|
|
|
327
339
|
assert dataset_without_resources.resources_len == 0
|
|
328
340
|
|
|
329
|
-
def test_dataset_activities(self,
|
|
341
|
+
def test_dataset_activities(self, app, mocker):
|
|
330
342
|
# A user must be authenticated for activities to be emitted
|
|
331
|
-
|
|
343
|
+
from flask_login import login_user
|
|
344
|
+
|
|
345
|
+
user = UserFactory()
|
|
332
346
|
|
|
333
347
|
mock_created = mocker.patch.object(UserCreatedDataset, "emit")
|
|
334
348
|
mock_updated = mocker.patch.object(UserUpdatedDataset, "emit")
|
|
@@ -337,35 +351,38 @@ class DatasetModelTest(PytestOnlyDBTestCase):
|
|
|
337
351
|
mock_resouce_updated = mocker.patch.object(UserUpdatedResource, "emit")
|
|
338
352
|
mock_resouce_removed = mocker.patch.object(UserRemovedResourceFromDataset, "emit")
|
|
339
353
|
|
|
340
|
-
with
|
|
341
|
-
|
|
342
|
-
mock_created.assert_called()
|
|
354
|
+
with app.test_request_context():
|
|
355
|
+
login_user(user)
|
|
343
356
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
mock_updated.assert_called()
|
|
357
|
+
with assert_emit(Dataset.on_create):
|
|
358
|
+
dataset = DatasetFactory(owner=user)
|
|
359
|
+
mock_created.assert_called()
|
|
348
360
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
361
|
+
with assert_emit(Dataset.on_update):
|
|
362
|
+
dataset.title = "new title"
|
|
363
|
+
dataset.save()
|
|
364
|
+
mock_updated.assert_called()
|
|
352
365
|
|
|
353
|
-
|
|
366
|
+
with assert_emit(Dataset.on_resource_added):
|
|
367
|
+
dataset.add_resource(ResourceFactory())
|
|
368
|
+
mock_resource_added.assert_called()
|
|
354
369
|
|
|
355
|
-
|
|
356
|
-
resource = dataset.resources[0]
|
|
357
|
-
resource.description = "New description"
|
|
358
|
-
dataset.update_resource(resource)
|
|
359
|
-
mock_resouce_updated.assert_called()
|
|
370
|
+
dataset.reload()
|
|
360
371
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
372
|
+
with assert_emit(Dataset.on_resource_updated):
|
|
373
|
+
resource = dataset.resources[0]
|
|
374
|
+
resource.description = "New description"
|
|
375
|
+
dataset.update_resource(resource)
|
|
376
|
+
mock_resouce_updated.assert_called()
|
|
364
377
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
378
|
+
with assert_emit(Dataset.on_resource_removed):
|
|
379
|
+
dataset.remove_resource(dataset.resources[-1])
|
|
380
|
+
mock_resouce_removed.assert_called()
|
|
381
|
+
|
|
382
|
+
with assert_emit(Dataset.on_delete):
|
|
383
|
+
dataset.deleted = datetime.utcnow()
|
|
384
|
+
dataset.save()
|
|
385
|
+
mock_deleted.assert_called()
|
|
369
386
|
|
|
370
387
|
def test_dataset_metrics(self):
|
|
371
388
|
# We need to init metrics module
|
|
@@ -867,3 +884,26 @@ class HarvestMetadataTest(PytestOnlyDBTestCase):
|
|
|
867
884
|
resource.validate()
|
|
868
885
|
|
|
869
886
|
assert resource.last_modified == resource.extras["analysis:last-modified-at"]
|
|
887
|
+
|
|
888
|
+
def test_quality_cached_next_update_with_date_last_update(self):
|
|
889
|
+
"""Test that quality_cached with date (not datetime) last_update can be saved to MongoDB.
|
|
890
|
+
|
|
891
|
+
This reproduces a production bug where last_update could be a datetime.date instead
|
|
892
|
+
of datetime.datetime, causing next_update to also be a datetime.date, which BSON
|
|
893
|
+
cannot encode (it only supports datetime.datetime).
|
|
894
|
+
|
|
895
|
+
See error: bson.errors.InvalidDocument: cannot encode object: datetime.date(...), of type: <class 'datetime.date'>
|
|
896
|
+
"""
|
|
897
|
+
dataset: Dataset = DatasetFactory(
|
|
898
|
+
frequency=UpdateFrequency.QUARTERLY,
|
|
899
|
+
)
|
|
900
|
+
# Set harvest metadata with modified_at as a date instead of datetime
|
|
901
|
+
# This simulates data coming from harvesting where dates might not be properly typed
|
|
902
|
+
dataset.harvest = HarvestDatasetMetadata(
|
|
903
|
+
created_at=datetime(2019, 1, 1),
|
|
904
|
+
modified_at=date(2019, 6, 7), # Using date instead of datetime
|
|
905
|
+
)
|
|
906
|
+
# Also set last_update as date to fully simulate the production scenario
|
|
907
|
+
dataset.last_update = date(2019, 6, 7)
|
|
908
|
+
|
|
909
|
+
dataset.save()
|
|
@@ -643,10 +643,10 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
|
|
|
643
643
|
|
|
644
644
|
assert len(dataset.contact_points) == 1
|
|
645
645
|
assert dataset.contact_points[0].role == "contact"
|
|
646
|
-
assert dataset.contact_points[0].name == "foo"
|
|
646
|
+
assert dataset.contact_points[0].name == "foo (bar)"
|
|
647
647
|
assert dataset.contact_points[0].email == "foo@example.com"
|
|
648
648
|
|
|
649
|
-
def
|
|
649
|
+
def test_contact_point_organization_member_foaf_both_mails(self):
|
|
650
650
|
g = Graph()
|
|
651
651
|
node = URIRef("https://test.org/dataset")
|
|
652
652
|
g.set((node, RDF.type, DCAT.Dataset))
|
|
@@ -673,10 +673,10 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
|
|
|
673
673
|
|
|
674
674
|
assert len(dataset.contact_points) == 1
|
|
675
675
|
assert dataset.contact_points[0].role == "creator"
|
|
676
|
-
assert dataset.contact_points[0].name == "foo"
|
|
676
|
+
assert dataset.contact_points[0].name == "foo (bar)"
|
|
677
677
|
assert dataset.contact_points[0].email == "foo@example.com"
|
|
678
678
|
|
|
679
|
-
def
|
|
679
|
+
def test_contact_point_organization_member_foaf_no_org_mail(self):
|
|
680
680
|
g = Graph()
|
|
681
681
|
node = URIRef("https://test.org/dataset")
|
|
682
682
|
g.set((node, RDF.type, DCAT.Dataset))
|
|
@@ -703,9 +703,39 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
|
|
|
703
703
|
|
|
704
704
|
assert len(dataset.contact_points) == 1
|
|
705
705
|
assert dataset.contact_points[0].role == "creator"
|
|
706
|
-
assert dataset.contact_points[0].name == "foo"
|
|
706
|
+
assert dataset.contact_points[0].name == "foo (bar)"
|
|
707
707
|
assert dataset.contact_points[0].email == "foo@example.com"
|
|
708
708
|
|
|
709
|
+
def test_contact_point_organization_member_foaf_no_agent_mail(self):
|
|
710
|
+
g = Graph()
|
|
711
|
+
node = URIRef("https://test.org/dataset")
|
|
712
|
+
g.set((node, RDF.type, DCAT.Dataset))
|
|
713
|
+
g.set((node, DCT.identifier, Literal(faker.uuid4())))
|
|
714
|
+
g.set((node, DCT.title, Literal(faker.sentence())))
|
|
715
|
+
|
|
716
|
+
org = BNode()
|
|
717
|
+
g.add((org, RDF.type, FOAF.Organization))
|
|
718
|
+
g.add((org, FOAF.name, Literal("bar")))
|
|
719
|
+
g.add((org, FOAF.mbox, Literal("bar@example.com")))
|
|
720
|
+
contact = BNode()
|
|
721
|
+
g.add((contact, RDF.type, FOAF.Person))
|
|
722
|
+
g.add((contact, FOAF.name, Literal("foo")))
|
|
723
|
+
# no agent email
|
|
724
|
+
g.add((contact, ORG.memberOf, org))
|
|
725
|
+
g.add((node, DCT.creator, contact))
|
|
726
|
+
|
|
727
|
+
# Dataset needs an owner/organization for contact_points_from_rdf() to work
|
|
728
|
+
d = DatasetFactory.build()
|
|
729
|
+
d.organization = OrganizationFactory(name="organization")
|
|
730
|
+
|
|
731
|
+
dataset = dataset_from_rdf(g, d)
|
|
732
|
+
dataset.validate()
|
|
733
|
+
|
|
734
|
+
assert len(dataset.contact_points) == 1
|
|
735
|
+
assert dataset.contact_points[0].role == "creator"
|
|
736
|
+
assert dataset.contact_points[0].name == "foo (bar)"
|
|
737
|
+
assert dataset.contact_points[0].email == "bar@example.com"
|
|
738
|
+
|
|
709
739
|
def test_theme_and_tags(self):
|
|
710
740
|
node = BNode()
|
|
711
741
|
g = Graph()
|
|
@@ -1364,6 +1394,70 @@ class DatasetRdfViewsTest(PytestOnlyAPITestCase):
|
|
|
1364
1394
|
assert200(response)
|
|
1365
1395
|
assert response.content_type == mime
|
|
1366
1396
|
|
|
1397
|
+
@pytest.mark.parametrize(
|
|
1398
|
+
"fmt,mime",
|
|
1399
|
+
[
|
|
1400
|
+
("n3", "text/n3"),
|
|
1401
|
+
("nt", "application/n-triples"),
|
|
1402
|
+
("ttl", "application/x-turtle"),
|
|
1403
|
+
("xml", "application/rdf+xml"),
|
|
1404
|
+
("rdf", "application/rdf+xml"),
|
|
1405
|
+
("owl", "application/rdf+xml"),
|
|
1406
|
+
("trig", "application/trig"),
|
|
1407
|
+
],
|
|
1408
|
+
)
|
|
1409
|
+
def test_dont_fail_with_invalid_uri(self, client, fmt, mime):
|
|
1410
|
+
"""Invalid URIs (with spaces or curly brackets) shouldn't make rdf export fail in any format"""
|
|
1411
|
+
invalid_uri_with_quote = 'https://test.org/dataset_with"quote"'
|
|
1412
|
+
invalid_uri_with_curly_bracket = 'http://opendata-sig.saintdenis.re/datasets/identifiant.kml?outSR={"latestWkid":2975,"wkid":2975}'
|
|
1413
|
+
invalid_uri_with_space = "https://catalogue.opendata-ligair.fr/geonetwork/srv/60678572-36e5-4e78-9af3-48f726670dfd fr-modelisation-sirane-vacarm_no2"
|
|
1414
|
+
dataset = DatasetFactory(
|
|
1415
|
+
resources=[
|
|
1416
|
+
ResourceFactory(url=invalid_uri_with_quote),
|
|
1417
|
+
ResourceFactory(url=invalid_uri_with_curly_bracket),
|
|
1418
|
+
],
|
|
1419
|
+
harvest=HarvestDatasetMetadata(uri=invalid_uri_with_space),
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
url = url_for("api.dataset_rdf_format", dataset=dataset, _format=fmt)
|
|
1423
|
+
response = client.get(url, headers={"Accept": mime})
|
|
1424
|
+
assert200(response)
|
|
1425
|
+
|
|
1426
|
+
@pytest.mark.parametrize(
|
|
1427
|
+
"fmt,mime",
|
|
1428
|
+
[
|
|
1429
|
+
("n3", "text/n3"),
|
|
1430
|
+
("nt", "application/n-triples"),
|
|
1431
|
+
("ttl", "application/x-turtle"),
|
|
1432
|
+
("trig", "application/trig"),
|
|
1433
|
+
],
|
|
1434
|
+
)
|
|
1435
|
+
def test_invalid_uri_escape_in_n3_turtle_format(self, client, fmt, mime):
|
|
1436
|
+
"""Invalid URIs (with spaces or curly brackets) should be escaped in N3/turtle formats"""
|
|
1437
|
+
invalid_uri_with_quote = 'https://test.org/dataset_with"quote"'
|
|
1438
|
+
invalid_uri_with_curly_bracket = 'http://opendata-sig.saintdenis.re/datasets/identifiant.kml?outSR={"latestWkid":2975,"wkid":2975}'
|
|
1439
|
+
invalid_uri_with_space = "https://catalogue.opendata-ligair.fr/geonetwork/srv/60678572-36e5-4e78-9af3-48f726670dfd fr-modelisation-sirane-vacarm_no2"
|
|
1440
|
+
dataset = DatasetFactory(
|
|
1441
|
+
resources=[
|
|
1442
|
+
ResourceFactory(url=invalid_uri_with_quote),
|
|
1443
|
+
ResourceFactory(url=invalid_uri_with_curly_bracket),
|
|
1444
|
+
],
|
|
1445
|
+
harvest=HarvestDatasetMetadata(uri=invalid_uri_with_space),
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
url = url_for("api.dataset_rdf_format", dataset=dataset, _format=fmt)
|
|
1449
|
+
response = client.get(url, headers={"Accept": mime})
|
|
1450
|
+
assert200(response)
|
|
1451
|
+
assert "https://test.org/dataset_with%22quote%22" in response.text
|
|
1452
|
+
assert (
|
|
1453
|
+
"http://opendata-sig.saintdenis.re/datasets/identifiant.kml?outSR=%7B%22latestWkid%22:2975,%22wkid%22:2975%7D"
|
|
1454
|
+
in response.text
|
|
1455
|
+
)
|
|
1456
|
+
assert (
|
|
1457
|
+
"https://catalogue.opendata-ligair.fr/geonetwork/srv/60678572-36e5-4e78-9af3-48f726670dfd%20fr-modelisation-sirane-vacarm_no2"
|
|
1458
|
+
in response.text
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1367
1461
|
|
|
1368
1462
|
class DatasetFromRdfUtilsTest(PytestOnlyTestCase):
|
|
1369
1463
|
def test_licenses_from_rdf(self):
|
|
@@ -60,6 +60,31 @@ class DatasetTasksTest(PytestOnlyDBTestCase):
|
|
|
60
60
|
assert HarvestJob.objects.filter(items__dataset=datasets[0].id).count() == 0
|
|
61
61
|
assert Dataservice.objects.filter(datasets=datasets[0].id).count() == 0
|
|
62
62
|
|
|
63
|
+
def test_purge_datasets_cleans_all_harvest_items_references(self):
|
|
64
|
+
"""Test that purging datasets cleans all HarvestItem references in a job.
|
|
65
|
+
|
|
66
|
+
The same dataset can appear multiple times in a job's items (e.g. if the
|
|
67
|
+
harvest source has duplicates). The $ operator only updates the first match,
|
|
68
|
+
so we need to use $[] with array_filters to update all matches.
|
|
69
|
+
"""
|
|
70
|
+
dataset_to_delete = Dataset.objects.create(title="delete me", deleted="2016-01-01")
|
|
71
|
+
dataset_keep = Dataset.objects.create(title="keep me")
|
|
72
|
+
|
|
73
|
+
job = HarvestJobFactory(
|
|
74
|
+
items=[
|
|
75
|
+
HarvestItem(dataset=dataset_to_delete, remote_id="1"),
|
|
76
|
+
HarvestItem(dataset=dataset_keep, remote_id="2"),
|
|
77
|
+
HarvestItem(dataset=dataset_to_delete, remote_id="3"),
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
tasks.purge_datasets()
|
|
82
|
+
|
|
83
|
+
job.reload()
|
|
84
|
+
assert job.items[0].dataset is None
|
|
85
|
+
assert job.items[1].dataset == dataset_keep
|
|
86
|
+
assert job.items[2].dataset is None
|
|
87
|
+
|
|
63
88
|
def test_purge_datasets_community(self):
|
|
64
89
|
dataset = Dataset.objects.create(title="delete me", deleted="2016-01-01")
|
|
65
90
|
community_resource1 = CommunityResourceFactory()
|