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/tests/helpers.py
CHANGED
|
@@ -4,11 +4,11 @@ from datetime import timedelta
|
|
|
4
4
|
from io import BytesIO
|
|
5
5
|
from urllib.parse import parse_qs, urlparse
|
|
6
6
|
|
|
7
|
-
import mock
|
|
8
7
|
from flask import current_app, json
|
|
9
8
|
from flask_security.babel import FsDomain
|
|
10
9
|
from PIL import Image
|
|
11
10
|
|
|
11
|
+
from udata.core.spatial.factories import GeoZoneFactory
|
|
12
12
|
from udata.mail import mail_sent
|
|
13
13
|
|
|
14
14
|
|
|
@@ -34,51 +34,50 @@ def assert_json_equal(first, second):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
@contextmanager
|
|
37
|
-
def mock_signals(
|
|
37
|
+
def mock_signals(*signals):
|
|
38
38
|
__tracebackhide__ = True
|
|
39
|
-
specs = []
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
callbacks_by_signal = {}
|
|
41
|
+
calls_kwargs_by_signal = {}
|
|
43
42
|
|
|
44
|
-
for
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
for requestSignal in signals:
|
|
44
|
+
# We capture requestSignal with a default argument
|
|
45
|
+
def callback(*args, requestSignal=requestSignal, **kwargs):
|
|
46
|
+
calls_kwargs_by_signal.setdefault(requestSignal, [])
|
|
47
|
+
calls_kwargs_by_signal[requestSignal].append(kwargs)
|
|
48
|
+
|
|
49
|
+
callbacks_by_signal[requestSignal] = callback
|
|
50
|
+
requestSignal.connect(callback, weak=False)
|
|
48
51
|
|
|
49
|
-
yield
|
|
52
|
+
yield calls_kwargs_by_signal
|
|
50
53
|
|
|
51
|
-
for
|
|
52
|
-
|
|
53
|
-
signal_name = getattr(signal, "name", str(signal))
|
|
54
|
-
callback(signal_name, mock_handler)
|
|
54
|
+
for sig in signals:
|
|
55
|
+
sig.disconnect(callbacks_by_signal[sig])
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
@contextmanager
|
|
58
59
|
def assert_emit(*signals, assertions_callback=None):
|
|
59
60
|
__tracebackhide__ = True
|
|
60
|
-
msg = 'Signal "{0}" should have been emitted'
|
|
61
|
-
|
|
62
|
-
def callback(name, handler):
|
|
63
|
-
assert handler.called, msg.format(name)
|
|
64
|
-
if assertions_callback is not None:
|
|
65
|
-
assertions_callback(handler.call_args)
|
|
66
61
|
|
|
67
|
-
with mock_signals(
|
|
62
|
+
with mock_signals(*signals) as calls_kwargs_by_signal:
|
|
68
63
|
yield
|
|
69
64
|
|
|
65
|
+
for signal in signals:
|
|
66
|
+
assert signal in calls_kwargs_by_signal, f'Signal "{signal}" should have been emitted'
|
|
67
|
+
if assertions_callback is not None:
|
|
68
|
+
for kwargs in calls_kwargs_by_signal[signal]:
|
|
69
|
+
assertions_callback(kwargs)
|
|
70
|
+
|
|
70
71
|
|
|
71
72
|
@contextmanager
|
|
72
73
|
def assert_not_emit(*signals):
|
|
73
74
|
__tracebackhide__ = True
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def callback(name, handler):
|
|
77
|
-
assert not handler.called, msg.format(name)
|
|
78
|
-
|
|
79
|
-
with mock_signals(callback, *signals):
|
|
75
|
+
with mock_signals(*signals) as calls_args_by_signal:
|
|
80
76
|
yield
|
|
81
77
|
|
|
78
|
+
for signal in signals:
|
|
79
|
+
assert signal not in calls_args_by_signal, f'Signal "{signal}" should not have been emitted'
|
|
80
|
+
|
|
82
81
|
|
|
83
82
|
@contextmanager
|
|
84
83
|
def capture_mails():
|
|
@@ -221,5 +220,16 @@ def create_test_image():
|
|
|
221
220
|
return file
|
|
222
221
|
|
|
223
222
|
|
|
223
|
+
def create_geozones_fixtures():
|
|
224
|
+
paca = GeoZoneFactory(
|
|
225
|
+
id="fr:region:93", level="fr:region", name="Provence Alpes Côtes dAzur", code="93"
|
|
226
|
+
)
|
|
227
|
+
bdr = GeoZoneFactory(
|
|
228
|
+
id="fr:departement:13", level="fr:departement", name="Bouches-du-Rhône", code="13"
|
|
229
|
+
)
|
|
230
|
+
arles = GeoZoneFactory(id="fr:commune:13004", level="fr:commune", name="Arles", code="13004")
|
|
231
|
+
return paca, bdr, arles
|
|
232
|
+
|
|
233
|
+
|
|
224
234
|
def security_gettext(string):
|
|
225
235
|
return FsDomain(current_app).gettext(string)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from udata.core.organization.factories import OrganizationFactory
|
|
2
|
-
from udata.core.organization.notifications import
|
|
2
|
+
from udata.core.organization.notifications import (
|
|
3
|
+
membership_request_notifications,
|
|
4
|
+
)
|
|
3
5
|
from udata.core.user.factories import UserFactory
|
|
6
|
+
from udata.features.notifications.models import Notification
|
|
4
7
|
from udata.models import Member, MembershipRequest
|
|
5
|
-
from udata.tests.api import PytestOnlyDBTestCase
|
|
8
|
+
from udata.tests.api import DBTestCase, PytestOnlyDBTestCase
|
|
6
9
|
from udata.tests.helpers import assert_equal_dates
|
|
7
10
|
|
|
8
11
|
|
|
@@ -27,3 +30,65 @@ class OrganizationNotificationsTest(PytestOnlyDBTestCase):
|
|
|
27
30
|
assert details["user"]["id"] == applicant.id
|
|
28
31
|
assert details["user"]["fullname"] == applicant.fullname
|
|
29
32
|
assert details["user"]["avatar"] == str(applicant.avatar)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MembershipRequestNotificationTest(DBTestCase):
|
|
36
|
+
def test_notification_created_for_admins_only(self):
|
|
37
|
+
"""Notifications are created for all admin users, not editors"""
|
|
38
|
+
admin1 = UserFactory()
|
|
39
|
+
admin2 = UserFactory()
|
|
40
|
+
editor = UserFactory()
|
|
41
|
+
applicant = UserFactory()
|
|
42
|
+
members = [
|
|
43
|
+
Member(user=editor, role="editor"),
|
|
44
|
+
Member(user=admin1, role="admin"),
|
|
45
|
+
Member(user=admin2, role="admin"),
|
|
46
|
+
]
|
|
47
|
+
org = OrganizationFactory(members=members)
|
|
48
|
+
|
|
49
|
+
request = MembershipRequest(user=applicant, comment="test")
|
|
50
|
+
org.add_membership_request(request)
|
|
51
|
+
|
|
52
|
+
notifications = Notification.objects.all()
|
|
53
|
+
assert len(notifications) == 2
|
|
54
|
+
|
|
55
|
+
admin_users = [notif.user for notif in notifications]
|
|
56
|
+
self.assertIn(admin1, admin_users)
|
|
57
|
+
self.assertIn(admin2, admin_users)
|
|
58
|
+
|
|
59
|
+
for notification in notifications:
|
|
60
|
+
assert notification.details.request_organization == org
|
|
61
|
+
assert notification.details.request_user == applicant
|
|
62
|
+
assert_equal_dates(notification.created_at, request.created)
|
|
63
|
+
|
|
64
|
+
def test_no_duplicate_notifications(self):
|
|
65
|
+
"""Duplicate notifications are not created on subsequent saves"""
|
|
66
|
+
admin = UserFactory()
|
|
67
|
+
applicant = UserFactory()
|
|
68
|
+
org = OrganizationFactory(members=[Member(user=admin, role="admin")])
|
|
69
|
+
|
|
70
|
+
request = MembershipRequest(user=applicant, comment="test")
|
|
71
|
+
org.add_membership_request(request)
|
|
72
|
+
org.add_membership_request(request)
|
|
73
|
+
|
|
74
|
+
assert Notification.objects.count() == 1
|
|
75
|
+
|
|
76
|
+
def test_multiple_requests_create_separate_notifications(self):
|
|
77
|
+
"""Multiple requests from different users create separate notifications"""
|
|
78
|
+
admin = UserFactory()
|
|
79
|
+
applicant1 = UserFactory()
|
|
80
|
+
applicant2 = UserFactory()
|
|
81
|
+
org = OrganizationFactory(members=[Member(user=admin, role="admin")])
|
|
82
|
+
|
|
83
|
+
request1 = MembershipRequest(user=applicant1, comment="test 1")
|
|
84
|
+
org.add_membership_request(request1)
|
|
85
|
+
|
|
86
|
+
request2 = MembershipRequest(user=applicant2, comment="test 2")
|
|
87
|
+
org.add_membership_request(request2)
|
|
88
|
+
|
|
89
|
+
notifications = Notification.objects.all()
|
|
90
|
+
assert len(notifications) == 2
|
|
91
|
+
|
|
92
|
+
request_users = [notif.details.request_user for notif in notifications]
|
|
93
|
+
self.assertIn(applicant1, request_users)
|
|
94
|
+
self.assertIn(applicant2, request_users)
|
udata/tests/plugin.py
CHANGED
|
@@ -1,147 +1,20 @@
|
|
|
1
|
-
import shlex
|
|
2
|
-
from contextlib import contextmanager
|
|
3
|
-
|
|
4
1
|
import pytest
|
|
5
|
-
from flask import current_app, json, template_rendered, url_for
|
|
6
|
-
from flask.testing import FlaskClient
|
|
7
|
-
from flask_principal import Identity, identity_changed
|
|
8
|
-
from lxml import etree
|
|
9
|
-
|
|
10
|
-
from udata.core.user.factories import UserFactory
|
|
11
|
-
|
|
12
|
-
from .helpers import assert200, assert_command_ok
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestClient(FlaskClient):
|
|
16
|
-
"""
|
|
17
|
-
The goal of these `post`, `put` and `delete` functions is to
|
|
18
|
-
switch from `data` in kwargs to `data` in args and be able to
|
|
19
|
-
`client.post(url, data)` without doing `client.post(url, data=data)`
|
|
20
|
-
|
|
21
|
-
Same as in :TestClientOverride
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
def post(self, url, data=None, **kwargs):
|
|
25
|
-
return super(TestClient, self).post(url, data=data, **kwargs)
|
|
26
|
-
|
|
27
|
-
def put(self, url, data=None, **kwargs):
|
|
28
|
-
return super(TestClient, self).put(url, data=data, **kwargs)
|
|
29
|
-
|
|
30
|
-
def delete(self, url, data=None, **kwargs):
|
|
31
|
-
return super(TestClient, self).delete(url, data=data, **kwargs)
|
|
32
|
-
|
|
33
|
-
def login(self, user=None):
|
|
34
|
-
user = user or UserFactory()
|
|
35
|
-
with self.session_transaction() as session:
|
|
36
|
-
# Since flask-security-too 4.0.0, the user.fs_uniquifier is used instead of user.id for auth
|
|
37
|
-
user_id = getattr(user, current_app.login_manager.id_attribute)()
|
|
38
|
-
session["user_id"] = user_id
|
|
39
|
-
session["_fresh"] = True
|
|
40
|
-
session["_id"] = current_app.login_manager._session_identifier_generator()
|
|
41
|
-
current_app.login_manager._update_request_context_with_user(user)
|
|
42
|
-
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
|
|
43
|
-
return user
|
|
44
|
-
|
|
45
|
-
def logout(self):
|
|
46
|
-
with self.session_transaction() as session:
|
|
47
|
-
del session["user_id"]
|
|
48
|
-
del session["_fresh"]
|
|
49
|
-
del session["_id"]
|
|
50
2
|
|
|
51
3
|
|
|
52
4
|
@pytest.fixture
|
|
53
|
-
def
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
return app.test_client()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class ApiClient(object):
|
|
61
|
-
def __init__(self, client):
|
|
62
|
-
self.client = client
|
|
63
|
-
self._user = None
|
|
64
|
-
|
|
65
|
-
def login(self, *args, **kwargs):
|
|
66
|
-
return self.client.login(*args, **kwargs)
|
|
67
|
-
|
|
68
|
-
@contextmanager
|
|
69
|
-
def user(self, user=None):
|
|
70
|
-
self._user = user or UserFactory()
|
|
71
|
-
if not self._user.apikey:
|
|
72
|
-
self._user.generate_api_key()
|
|
73
|
-
self._user.save()
|
|
74
|
-
yield self._user
|
|
75
|
-
|
|
76
|
-
def perform(self, verb, url, **kwargs):
|
|
77
|
-
headers = kwargs.pop("headers", {})
|
|
78
|
-
headers["Content-Type"] = "application/json"
|
|
79
|
-
|
|
80
|
-
data = kwargs.get("data")
|
|
81
|
-
if data is not None:
|
|
82
|
-
data = json.dumps(data)
|
|
83
|
-
headers["Content-Length"] = len(data)
|
|
84
|
-
kwargs["data"] = data
|
|
85
|
-
|
|
86
|
-
if self._user:
|
|
87
|
-
headers["X-API-KEY"] = kwargs.get("X-API-KEY", self._user.apikey)
|
|
88
|
-
|
|
89
|
-
kwargs["headers"] = headers
|
|
90
|
-
method = getattr(self.client, verb)
|
|
91
|
-
return method(url, **kwargs)
|
|
92
|
-
|
|
93
|
-
def get(self, url, *args, **kwargs):
|
|
94
|
-
return self.perform("get", url, *args, **kwargs)
|
|
95
|
-
|
|
96
|
-
def post(self, url, data=None, json=True, *args, **kwargs):
|
|
97
|
-
if not json:
|
|
98
|
-
return self.client.post(url, data or {}, *args, **kwargs)
|
|
99
|
-
return self.perform("post", url, data=data or {}, *args, **kwargs)
|
|
100
|
-
|
|
101
|
-
def put(self, url, data=None, json=True, *args, **kwargs):
|
|
102
|
-
if not json:
|
|
103
|
-
return self.client.put(url, data or {}, *args, **kwargs)
|
|
104
|
-
return self.perform("put", url, data=data or {}, *args, **kwargs)
|
|
105
|
-
|
|
106
|
-
def patch(self, url, data=None, json=True, *args, **kwargs):
|
|
107
|
-
if not json:
|
|
108
|
-
return self.client.patch(url, data or {}, *args, **kwargs)
|
|
109
|
-
return self.perform("patch", url, data=data or {}, *args, **kwargs)
|
|
110
|
-
|
|
111
|
-
def delete(self, url, data=None, *args, **kwargs):
|
|
112
|
-
return self.perform("delete", url, data=data or {}, *args, **kwargs)
|
|
113
|
-
|
|
114
|
-
def options(self, url, data=None, *args, **kwargs):
|
|
115
|
-
return self.perform("options", url, data=data or {}, *args, **kwargs)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
@pytest.fixture
|
|
119
|
-
def api(client):
|
|
120
|
-
api_client = ApiClient(client)
|
|
121
|
-
return api_client
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@pytest.fixture(name="cli")
|
|
125
|
-
def cli_fixture(app):
|
|
126
|
-
def mock_runner(*args, **kwargs):
|
|
127
|
-
from udata.commands import cli
|
|
128
|
-
|
|
129
|
-
if len(args) == 1 and " " in args[0]:
|
|
130
|
-
args = shlex.split(args[0])
|
|
131
|
-
runner = app.test_cli_runner()
|
|
132
|
-
result = runner.invoke(cli, args, catch_exceptions=False)
|
|
133
|
-
if kwargs.get("check", True):
|
|
134
|
-
assert_command_ok(result)
|
|
135
|
-
return result
|
|
5
|
+
def rmock():
|
|
6
|
+
"""A requests-mock fixture"""
|
|
7
|
+
import requests_mock
|
|
136
8
|
|
|
137
|
-
|
|
9
|
+
with requests_mock.Mocker() as m:
|
|
10
|
+
m.ANY = requests_mock.ANY
|
|
11
|
+
yield m
|
|
138
12
|
|
|
139
13
|
|
|
140
14
|
@pytest.fixture
|
|
141
15
|
def instance_path(app, tmpdir):
|
|
142
16
|
"""Use temporary application instance_path"""
|
|
143
17
|
from udata.core import storages
|
|
144
|
-
from udata.core.storages.views import blueprint
|
|
145
18
|
|
|
146
19
|
app.instance_path = str(tmpdir)
|
|
147
20
|
app.config["FS_ROOT"] = str(tmpdir / "fs")
|
|
@@ -152,133 +25,5 @@ def instance_path(app, tmpdir):
|
|
|
152
25
|
app.config.pop(key.format("ROOT"), None)
|
|
153
26
|
|
|
154
27
|
storages.init_app(app)
|
|
155
|
-
app.register_blueprint(blueprint, name="test-storage")
|
|
156
28
|
|
|
157
29
|
return tmpdir
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class ContextVariableDoesNotExist(Exception):
|
|
161
|
-
pass
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
class TemplateRecorder:
|
|
165
|
-
@contextmanager
|
|
166
|
-
def capture(self):
|
|
167
|
-
self.templates = []
|
|
168
|
-
template_rendered.connect(self._add_template)
|
|
169
|
-
yield
|
|
170
|
-
template_rendered.disconnect(self._add_template)
|
|
171
|
-
|
|
172
|
-
def _add_template(self, app, template, context):
|
|
173
|
-
self.templates.append((template, context))
|
|
174
|
-
|
|
175
|
-
def assert_used(self, name):
|
|
176
|
-
"""
|
|
177
|
-
Checks if a given template is used in the request.
|
|
178
|
-
|
|
179
|
-
:param name: template name
|
|
180
|
-
"""
|
|
181
|
-
__tracebackhide__ = True
|
|
182
|
-
|
|
183
|
-
used_templates = []
|
|
184
|
-
|
|
185
|
-
for template, context in self.templates:
|
|
186
|
-
if template.name == name:
|
|
187
|
-
return True
|
|
188
|
-
|
|
189
|
-
used_templates.append(template)
|
|
190
|
-
|
|
191
|
-
msg = "Template %s not used. Templates were used: %s" % (
|
|
192
|
-
name,
|
|
193
|
-
" ".join(repr(used_templates)),
|
|
194
|
-
)
|
|
195
|
-
raise AssertionError(msg)
|
|
196
|
-
|
|
197
|
-
def get_context_variable(self, name):
|
|
198
|
-
"""
|
|
199
|
-
Returns a variable from the context passed to the template.
|
|
200
|
-
|
|
201
|
-
:param name: name of variable
|
|
202
|
-
:raises ContextVariableDoesNotExist: if does not exist.
|
|
203
|
-
"""
|
|
204
|
-
for template, context in self.templates:
|
|
205
|
-
if name in context:
|
|
206
|
-
return context[name]
|
|
207
|
-
raise ContextVariableDoesNotExist()
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
@pytest.fixture
|
|
211
|
-
def templates():
|
|
212
|
-
recorder = TemplateRecorder()
|
|
213
|
-
with recorder.capture():
|
|
214
|
-
yield recorder
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@pytest.fixture
|
|
218
|
-
def httpretty():
|
|
219
|
-
import httpretty
|
|
220
|
-
|
|
221
|
-
httpretty.reset()
|
|
222
|
-
httpretty.enable()
|
|
223
|
-
yield httpretty
|
|
224
|
-
httpretty.disable()
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@pytest.fixture
|
|
228
|
-
def rmock():
|
|
229
|
-
"""A requests-mock fixture"""
|
|
230
|
-
import requests_mock
|
|
231
|
-
|
|
232
|
-
with requests_mock.Mocker() as m:
|
|
233
|
-
m.ANY = requests_mock.ANY
|
|
234
|
-
yield m
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class SitemapClient:
|
|
238
|
-
# Needed for lxml XPath not supporting default namespace
|
|
239
|
-
NAMESPACES = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
|
|
240
|
-
MISMATCH = 'URL "{0}" {1} mismatch: expected "{2}" found "{3}"'
|
|
241
|
-
|
|
242
|
-
def __init__(self, client):
|
|
243
|
-
self.client = client
|
|
244
|
-
self._sitemap = None
|
|
245
|
-
|
|
246
|
-
def fetch(self, secure=False):
|
|
247
|
-
base_url = "{0}://local.test".format("https" if secure else "http")
|
|
248
|
-
response = self.client.get("sitemap.xml", base_url=base_url)
|
|
249
|
-
assert200(response)
|
|
250
|
-
self._sitemap = etree.fromstring(response.data)
|
|
251
|
-
return self._sitemap
|
|
252
|
-
|
|
253
|
-
def xpath(self, query):
|
|
254
|
-
return self._sitemap.xpath(query, namespaces=self.NAMESPACES)
|
|
255
|
-
|
|
256
|
-
def get_by_url(self, endpoint, **kwargs):
|
|
257
|
-
url = url_for(endpoint, _external=True, **kwargs)
|
|
258
|
-
query = 's:url[s:loc="{url}"]'.format(url=url)
|
|
259
|
-
result = self.xpath(query)
|
|
260
|
-
return result[0] if result else None
|
|
261
|
-
|
|
262
|
-
def assert_url(self, url, priority, changefreq):
|
|
263
|
-
"""
|
|
264
|
-
Check than a URL is present in the sitemap
|
|
265
|
-
with given `priority` and `changefreq`
|
|
266
|
-
"""
|
|
267
|
-
__tracebackhide__ = True
|
|
268
|
-
r = url.xpath("s:priority", namespaces=self.NAMESPACES)
|
|
269
|
-
assert len(r) == 1, 'URL "{0}" should have one priority'.format(url)
|
|
270
|
-
found = r[0].text
|
|
271
|
-
msg = self.MISMATCH.format(url, "priority", priority, found)
|
|
272
|
-
assert found == str(priority), msg
|
|
273
|
-
|
|
274
|
-
r = url.xpath("s:changefreq", namespaces=self.NAMESPACES)
|
|
275
|
-
assert len(r) == 1, 'URL "{0}" should have one changefreq'.format(url)
|
|
276
|
-
found = r[0].text
|
|
277
|
-
msg = self.MISMATCH.format(url, "changefreq", changefreq, found)
|
|
278
|
-
assert found == changefreq, msg
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@pytest.fixture
|
|
282
|
-
def sitemap(client):
|
|
283
|
-
sitemap_client = SitemapClient(client)
|
|
284
|
-
return sitemap_client
|
|
@@ -7,6 +7,7 @@ from flask import url_for
|
|
|
7
7
|
from udata.core import csv
|
|
8
8
|
from udata.core.dataservices.factories import DataserviceFactory
|
|
9
9
|
from udata.core.dataset import tasks as dataset_tasks
|
|
10
|
+
from udata.core.dataset.constants import SPD
|
|
10
11
|
from udata.core.dataset.factories import DatasetFactory, ResourceFactory
|
|
11
12
|
from udata.core.organization.factories import OrganizationFactory
|
|
12
13
|
from udata.core.reuse.factories import ReuseFactory
|
|
@@ -25,7 +26,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
25
26
|
|
|
26
27
|
self.assert200(response)
|
|
27
28
|
self.assertEqual(response.mimetype, "text/csv")
|
|
28
|
-
self.assertEqual(response.charset, "utf-8")
|
|
29
29
|
|
|
30
30
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
31
31
|
reader = csv.get_reader(csvfile)
|
|
@@ -73,7 +73,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
73
73
|
|
|
74
74
|
self.assert200(response)
|
|
75
75
|
self.assertEqual(response.mimetype, "text/csv")
|
|
76
|
-
self.assertEqual(response.charset, "utf-8")
|
|
77
76
|
|
|
78
77
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
79
78
|
reader = csv.get_reader(csvfile)
|
|
@@ -99,6 +98,27 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
99
98
|
self.assertNotIn(str(dataset.id), ids)
|
|
100
99
|
self.assertNotIn(str(hidden_dataset.id), ids)
|
|
101
100
|
|
|
101
|
+
def test_datasets_csv_with_badge_filter(self):
|
|
102
|
+
self.app.config["EXPORT_CSV_MODELS"] = []
|
|
103
|
+
dataset_with_badge = DatasetFactory(resources=[ResourceFactory()])
|
|
104
|
+
dataset_with_badge.add_badge(SPD)
|
|
105
|
+
dataset_without_badge = DatasetFactory(resources=[ResourceFactory()])
|
|
106
|
+
|
|
107
|
+
response = self.get(url_for("api.site_datasets_csv", badge=SPD))
|
|
108
|
+
|
|
109
|
+
self.assert200(response)
|
|
110
|
+
|
|
111
|
+
csvfile = StringIO(response.data.decode("utf8"))
|
|
112
|
+
reader = csv.get_reader(csvfile)
|
|
113
|
+
next(reader) # skip header
|
|
114
|
+
|
|
115
|
+
rows = list(reader)
|
|
116
|
+
ids = [row[0] for row in rows]
|
|
117
|
+
|
|
118
|
+
self.assertEqual(len(rows), 1)
|
|
119
|
+
self.assertIn(str(dataset_with_badge.id), ids)
|
|
120
|
+
self.assertNotIn(str(dataset_without_badge.id), ids)
|
|
121
|
+
|
|
102
122
|
def test_resources_csv(self):
|
|
103
123
|
self.app.config["EXPORT_CSV_MODELS"] = []
|
|
104
124
|
datasets = [
|
|
@@ -110,7 +130,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
110
130
|
|
|
111
131
|
self.assert200(response)
|
|
112
132
|
self.assertEqual(response.mimetype, "text/csv")
|
|
113
|
-
self.assertEqual(response.charset, "utf-8")
|
|
114
133
|
|
|
115
134
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
116
135
|
reader = csv.get_reader(csvfile)
|
|
@@ -164,7 +183,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
164
183
|
|
|
165
184
|
self.assert200(response)
|
|
166
185
|
self.assertEqual(response.mimetype, "text/csv")
|
|
167
|
-
self.assertEqual(response.charset, "utf-8")
|
|
168
186
|
|
|
169
187
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
170
188
|
reader = csv.get_reader(csvfile)
|
|
@@ -200,7 +218,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
200
218
|
|
|
201
219
|
self.assert200(response)
|
|
202
220
|
self.assertEqual(response.mimetype, "text/csv")
|
|
203
|
-
self.assertEqual(response.charset, "utf-8")
|
|
204
221
|
|
|
205
222
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
206
223
|
reader = csv.get_reader(csvfile)
|
|
@@ -245,7 +262,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
245
262
|
|
|
246
263
|
self.assert200(response)
|
|
247
264
|
self.assertEqual(response.mimetype, "text/csv")
|
|
248
|
-
self.assertEqual(response.charset, "utf-8")
|
|
249
265
|
|
|
250
266
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
251
267
|
reader = csv.get_reader(csvfile)
|
|
@@ -293,7 +309,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
293
309
|
|
|
294
310
|
self.assert200(response)
|
|
295
311
|
self.assertEqual(response.mimetype, "text/csv")
|
|
296
|
-
self.assertEqual(response.charset, "utf-8")
|
|
297
312
|
|
|
298
313
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
299
314
|
reader = csv.get_reader(csvfile)
|
|
@@ -332,7 +347,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
332
347
|
|
|
333
348
|
self.assert200(response)
|
|
334
349
|
self.assertEqual(response.mimetype, "text/csv")
|
|
335
|
-
self.assertEqual(response.charset, "utf-8")
|
|
336
350
|
|
|
337
351
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
338
352
|
reader = csv.get_reader(csvfile)
|
|
@@ -379,7 +393,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
379
393
|
|
|
380
394
|
self.assert200(response)
|
|
381
395
|
self.assertEqual(response.mimetype, "text/csv")
|
|
382
|
-
self.assertEqual(response.charset, "utf-8")
|
|
383
396
|
|
|
384
397
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
385
398
|
reader = csv.get_reader(csvfile)
|
|
@@ -424,7 +437,6 @@ class SiteCsvExportsTest(APITestCase):
|
|
|
424
437
|
|
|
425
438
|
self.assert200(response)
|
|
426
439
|
self.assertEqual(response.mimetype, "text/csv")
|
|
427
|
-
self.assertEqual(response.charset, "utf-8")
|
|
428
440
|
|
|
429
441
|
csvfile = StringIO(response.data.decode("utf8"))
|
|
430
442
|
reader = csv.get_reader(csvfile)
|
udata/tests/test_activity.py
CHANGED
|
@@ -211,9 +211,9 @@ class AuditableTest(APITestCase):
|
|
|
211
211
|
not_auditable="original",
|
|
212
212
|
)
|
|
213
213
|
|
|
214
|
-
def check_signal_update(
|
|
214
|
+
def check_signal_update(kwargs):
|
|
215
215
|
self.assertEqual(
|
|
216
|
-
|
|
216
|
+
kwargs["changed_fields"],
|
|
217
217
|
[
|
|
218
218
|
"name",
|
|
219
219
|
"tags",
|
|
@@ -224,13 +224,13 @@ class AuditableTest(APITestCase):
|
|
|
224
224
|
"embedded_list.1.name",
|
|
225
225
|
],
|
|
226
226
|
)
|
|
227
|
-
self.assertEqual(
|
|
228
|
-
self.assertEqual(
|
|
229
|
-
self.assertEqual(
|
|
230
|
-
self.assertEqual(
|
|
231
|
-
self.assertEqual(
|
|
232
|
-
self.assertEqual(
|
|
233
|
-
self.assertEqual(
|
|
227
|
+
self.assertEqual(kwargs["previous"]["name"], "fake")
|
|
228
|
+
self.assertEqual(kwargs["previous"]["tags"], ["some", "tags"])
|
|
229
|
+
self.assertEqual(kwargs["previous"]["some_date"], date(2020, 1, 1))
|
|
230
|
+
self.assertEqual(kwargs["previous"]["daterange_embedded.start"], date(2020, 1, 1))
|
|
231
|
+
self.assertEqual(kwargs["previous"]["daterange_embedded.end"], date(2020, 12, 31))
|
|
232
|
+
self.assertEqual(kwargs["previous"]["some_list"], ["some", "list"])
|
|
233
|
+
self.assertEqual(kwargs["previous"]["embedded_list.1.name"], "fake_embedded_1")
|
|
234
234
|
|
|
235
235
|
with assert_emit(FakeAuditableSubject.on_update, assertions_callback=check_signal_update):
|
|
236
236
|
fake.name = "different"
|
udata/tests/test_cors.py
CHANGED
|
@@ -27,7 +27,7 @@ class CorsTest(APITestCase):
|
|
|
27
27
|
assert "Access-Control-Allow-Origin" in response.headers
|
|
28
28
|
|
|
29
29
|
# Resource permalink
|
|
30
|
-
response = self.get(f"/
|
|
30
|
+
response = self.get(f"/datasets/r/{dataset.resources[0].id}", headers=cors_headers)
|
|
31
31
|
assert_status(response, 404) # The route is defined in udata-front
|
|
32
32
|
assert "Access-Control-Allow-Origin" in response.headers
|
|
33
33
|
|
|
@@ -2,7 +2,7 @@ from udata.tests.api import PytestOnlyDBTestCase
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class ParseUrlCommandTest(PytestOnlyDBTestCase):
|
|
5
|
-
def test_parse_url(self,
|
|
5
|
+
def test_parse_url(self, requests_mock, caplog) -> None:
|
|
6
6
|
logs = []
|
|
7
7
|
|
|
8
8
|
def mock_echo(message: str) -> None:
|
|
@@ -15,7 +15,7 @@ class ParseUrlCommandTest(PytestOnlyDBTestCase):
|
|
|
15
15
|
requests_mock.get(mock_url, text=test_rdf_file.read())
|
|
16
16
|
requests_mock.head(mock_url, text="sig.oreme.rdf")
|
|
17
17
|
dataset_id = "0437a976-cff1-4fa6-807a-c23006df2f8f"
|
|
18
|
-
result = cli(
|
|
18
|
+
result = self.cli(
|
|
19
19
|
"dcat",
|
|
20
20
|
"parse-url",
|
|
21
21
|
mock_url,
|
udata/tests/test_discussions.py
CHANGED
|
@@ -142,11 +142,11 @@ class DiscussionsTest(APITestCase):
|
|
|
142
142
|
with assert_not_emit(on_new_discussion):
|
|
143
143
|
discussion_id = None
|
|
144
144
|
|
|
145
|
-
def check_signal(
|
|
145
|
+
def check_signal(kwargs):
|
|
146
146
|
self.assertIsNotNone(discussion_id)
|
|
147
147
|
self.assertIn(
|
|
148
|
-
f"https://data.gouv.fr/datasets/{dataset.slug}/discussions
|
|
149
|
-
|
|
148
|
+
f"https://data.gouv.fr/datasets/{dataset.slug}/discussions?discussion_id={discussion_id}",
|
|
149
|
+
kwargs["message"],
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
|
|
@@ -620,8 +620,8 @@ class DiscussionsTest(APITestCase):
|
|
|
620
620
|
self.login()
|
|
621
621
|
with assert_not_emit(on_new_discussion_comment):
|
|
622
622
|
|
|
623
|
-
def check_signal(
|
|
624
|
-
self.assertIn(discussion.url_for(),
|
|
623
|
+
def check_signal(kwargs):
|
|
624
|
+
self.assertIn(discussion.url_for(), kwargs["message"])
|
|
625
625
|
|
|
626
626
|
with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
|
|
627
627
|
response = self.post(
|