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
|
@@ -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
|
@@ -175,6 +175,18 @@ class UserAPITest(APITestCase):
|
|
|
175
175
|
|
|
176
176
|
self.assertEqual(len(response.json["data"]), 2)
|
|
177
177
|
|
|
178
|
+
def test_user_api_full_text_search_email(self):
|
|
179
|
+
"""It should find users based on last name"""
|
|
180
|
+
self.login(AdminFactory())
|
|
181
|
+
|
|
182
|
+
UserFactory(email="john@example.org")
|
|
183
|
+
UserFactory(email="jane@example.org")
|
|
184
|
+
|
|
185
|
+
response = self.get(url_for("api.users", q="jane"))
|
|
186
|
+
self.assert200(response)
|
|
187
|
+
|
|
188
|
+
self.assertEqual(len(response.json["data"]), 1)
|
|
189
|
+
|
|
178
190
|
def test_user_api_full_text_search_unicode(self):
|
|
179
191
|
"""It should find user with special characters"""
|
|
180
192
|
self.login(AdminFactory())
|
|
@@ -370,7 +382,7 @@ class UserAPITest(APITestCase):
|
|
|
370
382
|
response = self.delete(url_for("api.user", user=user_to_delete))
|
|
371
383
|
self.assertEqual(list(storages.avatars.list_files()), [])
|
|
372
384
|
self.assert204(response)
|
|
373
|
-
self.
|
|
385
|
+
self.assertEqual(len(mails), 1)
|
|
374
386
|
|
|
375
387
|
user_to_delete.reload()
|
|
376
388
|
response = self.delete(url_for("api.user", user=user_to_delete))
|
|
@@ -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/apiv2/test_topics.py
CHANGED
|
@@ -23,7 +23,7 @@ from udata.core.user.factories import UserFactory
|
|
|
23
23
|
from udata.i18n import _
|
|
24
24
|
from udata.tests.api import APITestCase
|
|
25
25
|
from udata.tests.api.test_datasets_api import SAMPLE_GEOM
|
|
26
|
-
from udata.tests.
|
|
26
|
+
from udata.tests.helpers import create_geozones_fixtures
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class TopicsListAPITest(APITestCase):
|
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")
|
|
@@ -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):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from flask import current_app, url_for
|
|
2
2
|
from flask_security.utils import hash_data
|
|
3
3
|
|
|
4
|
-
from udata.core.user.factories import AdminFactory
|
|
4
|
+
from udata.core.user.factories import AdminFactory, UserFactory
|
|
5
5
|
from udata.tests.api import APITestCase
|
|
6
6
|
|
|
7
7
|
|
|
@@ -22,3 +22,26 @@ class AuthTest(APITestCase):
|
|
|
22
22
|
|
|
23
23
|
user.reload()
|
|
24
24
|
assert user.email == new_email
|
|
25
|
+
|
|
26
|
+
def test_change_mail_already_taken(self):
|
|
27
|
+
"""Should not allow changing email to one already taken by another user"""
|
|
28
|
+
user = self.login(AdminFactory())
|
|
29
|
+
original_email = user.email
|
|
30
|
+
|
|
31
|
+
# Create another user with the target email
|
|
32
|
+
existing_user = UserFactory(email="taken@example.com")
|
|
33
|
+
new_email = existing_user.email
|
|
34
|
+
|
|
35
|
+
security = current_app.extensions["security"]
|
|
36
|
+
|
|
37
|
+
data = [str(user.fs_uniquifier), hash_data(user.email), new_email]
|
|
38
|
+
token = security.confirm_serializer.dumps(data)
|
|
39
|
+
confirmation_link = url_for("security.confirm_change_email", token=token)
|
|
40
|
+
|
|
41
|
+
resp = self.get(confirmation_link)
|
|
42
|
+
assert resp.status_code == 302
|
|
43
|
+
assert "change_email_already_taken" in resp.location
|
|
44
|
+
|
|
45
|
+
# Email should not have changed
|
|
46
|
+
user.reload()
|
|
47
|
+
assert user.email == original_email
|
udata/tests/frontend/test_csv.py
CHANGED
|
@@ -269,7 +269,6 @@ class CsvTest(APITestCase):
|
|
|
269
269
|
|
|
270
270
|
self.assert200(response)
|
|
271
271
|
self.assertEqual(response.mimetype, "text/csv")
|
|
272
|
-
self.assertEqual(response.charset, "utf-8")
|
|
273
272
|
|
|
274
273
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
275
274
|
reader = csv.get_reader(csvfile)
|
|
@@ -327,7 +326,6 @@ class CsvTest(APITestCase):
|
|
|
327
326
|
|
|
328
327
|
self.assert200(response)
|
|
329
328
|
self.assertEqual(response.mimetype, "text/csv")
|
|
330
|
-
self.assertEqual(response.charset, "utf-8")
|
|
331
329
|
|
|
332
330
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
333
331
|
reader = csv.get_reader(csvfile)
|
|
@@ -349,7 +347,6 @@ class CsvTest(APITestCase):
|
|
|
349
347
|
|
|
350
348
|
self.assert200(response)
|
|
351
349
|
self.assertEqual(response.mimetype, "text/csv")
|
|
352
|
-
self.assertEqual(response.charset, "utf-8")
|
|
353
350
|
|
|
354
351
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
355
352
|
reader = csv.get_reader(csvfile)
|