udata 14.0.0__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_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 +6 -3
- 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/models.py +1 -1
- 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 +17 -5
- udata/core/discussions/models.py +1 -0
- 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/spatial/forms.py +2 -2
- udata/core/user/models.py +5 -1
- 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/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/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 +57 -10
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +1 -4
- udata/mail.py +5 -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 +45 -6
- udata/routing.py +2 -2
- udata/settings.py +7 -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 +44 -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_swagger.py +4 -4
- 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/frontend/test_auth.py +24 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +25 -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_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- 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.4.1.dev7.dist-info/METADATA +109 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +121 -123
- 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.4.1.dev7.dist-info}/WHEEL +0 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +0 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
udata/tests/__init__.py
CHANGED
|
@@ -5,7 +5,6 @@ from werkzeug import Response
|
|
|
5
5
|
|
|
6
6
|
from udata import settings
|
|
7
7
|
from udata.app import UDataApp, create_app
|
|
8
|
-
from udata.tests.plugin import TestClient
|
|
9
8
|
|
|
10
9
|
from . import helpers
|
|
11
10
|
|
|
@@ -33,7 +32,6 @@ class TestCaseMixin:
|
|
|
33
32
|
def _app(self, request):
|
|
34
33
|
test_settings = self.get_settings(request)
|
|
35
34
|
self.app = create_app(settings.Defaults, override=test_settings)
|
|
36
|
-
self.app.test_client_class = TestClient
|
|
37
35
|
return self.app
|
|
38
36
|
|
|
39
37
|
def assertEqualDates(self, datetime1, datetime2, limit=1): # Seconds.
|
|
@@ -47,6 +45,33 @@ class TestCaseMixin:
|
|
|
47
45
|
stream2 = list(response2.iter_encoded())
|
|
48
46
|
assert stream1 == stream2
|
|
49
47
|
|
|
48
|
+
def cli(self, *args, **kwargs):
|
|
49
|
+
"""
|
|
50
|
+
Execute a CLI command.
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
self.cli("command", "arg1", "arg2")
|
|
54
|
+
self.cli("command arg1 arg2") # Auto-split on spaces
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
*args: Command and arguments (can be a single string with spaces or multiple args)
|
|
58
|
+
**kwargs: Additional arguments for the CLI runner (e.g., expect_error=True)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The CLI result object
|
|
62
|
+
"""
|
|
63
|
+
import shlex
|
|
64
|
+
|
|
65
|
+
from udata.commands import cli as cli_cmd
|
|
66
|
+
|
|
67
|
+
if len(args) == 1 and " " in args[0]:
|
|
68
|
+
args = shlex.split(args[0])
|
|
69
|
+
|
|
70
|
+
result = self.app.test_cli_runner().invoke(cli_cmd, args, **kwargs)
|
|
71
|
+
if result.exit_code != 0 and kwargs.get("expect_error") is not True:
|
|
72
|
+
helpers.assert_command_ok(result)
|
|
73
|
+
return result
|
|
74
|
+
|
|
50
75
|
|
|
51
76
|
class TestCase(TestCaseMixin, unittest.TestCase):
|
|
52
77
|
pass
|
udata/tests/api/__init__.py
CHANGED
|
@@ -2,7 +2,10 @@ from contextlib import contextmanager
|
|
|
2
2
|
from urllib.parse import urlparse
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
+
from flask import json
|
|
6
|
+
from flask_security.utils import login_user, logout_user, set_request_attr
|
|
5
7
|
|
|
8
|
+
from udata.core.user.factories import UserFactory
|
|
6
9
|
from udata.mongo import db
|
|
7
10
|
from udata.mongo.document import get_all_models
|
|
8
11
|
from udata.tests import PytestOnlyTestCase, TestCase, helpers
|
|
@@ -11,12 +14,15 @@ from udata.tests import PytestOnlyTestCase, TestCase, helpers
|
|
|
11
14
|
@pytest.mark.usefixtures("instance_path")
|
|
12
15
|
class APITestCaseMixin:
|
|
13
16
|
"""
|
|
14
|
-
|
|
17
|
+
API Test Case Mixin with integrated API client functionality.
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
This mixin provides API testing methods with automatic JSON handling.
|
|
20
|
+
The `get`, `post`, `put`, `patch`, `delete`, and `options` methods
|
|
21
|
+
default to JSON content-type and automatic serialization.
|
|
17
22
|
"""
|
|
18
23
|
|
|
19
24
|
user = None
|
|
25
|
+
_user = None # For API key authentication context
|
|
20
26
|
|
|
21
27
|
@pytest.fixture(autouse=True)
|
|
22
28
|
def load_api_routes(self, app):
|
|
@@ -26,45 +32,126 @@ class APITestCaseMixin:
|
|
|
26
32
|
frontend.init_app(app)
|
|
27
33
|
|
|
28
34
|
@pytest.fixture(autouse=True)
|
|
29
|
-
def
|
|
35
|
+
def inject_client(self, app):
|
|
30
36
|
"""
|
|
31
|
-
Inject
|
|
37
|
+
Inject test client for Flask testing.
|
|
32
38
|
"""
|
|
33
|
-
self.
|
|
34
|
-
|
|
35
|
-
@pytest.fixture(autouse=True)
|
|
36
|
-
def inject_client(self, client):
|
|
37
|
-
"""
|
|
38
|
-
Inject test client for compatibility with Flask-Testing.
|
|
39
|
-
"""
|
|
40
|
-
self.client = client
|
|
39
|
+
self.client = app.test_client()
|
|
41
40
|
|
|
42
41
|
@contextmanager
|
|
43
42
|
def api_user(self, user=None):
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
"""
|
|
44
|
+
Context manager for API key authentication.
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
with self.api_user(user) as user:
|
|
48
|
+
response = self.get(url)
|
|
49
|
+
"""
|
|
50
|
+
self._user = user or UserFactory()
|
|
51
|
+
if not self._user.apikey:
|
|
52
|
+
self._user.generate_api_key()
|
|
53
|
+
self._user.save()
|
|
54
|
+
yield self._user
|
|
55
|
+
self._user = None
|
|
46
56
|
|
|
47
57
|
def login(self, user=None):
|
|
48
|
-
|
|
58
|
+
"""Login a user via session authentication."""
|
|
59
|
+
self.user = user or UserFactory()
|
|
60
|
+
|
|
61
|
+
login_user(self.user)
|
|
62
|
+
set_request_attr("fs_authn_via", "session")
|
|
63
|
+
|
|
49
64
|
return self.user
|
|
50
65
|
|
|
66
|
+
def logout(self):
|
|
67
|
+
"""Logout the current user."""
|
|
68
|
+
logout_user()
|
|
69
|
+
self.user = None
|
|
70
|
+
|
|
71
|
+
def perform(self, verb, url, **kwargs):
|
|
72
|
+
"""
|
|
73
|
+
Perform an HTTP request with JSON handling.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
verb: HTTP verb (get, post, put, patch, delete, options)
|
|
77
|
+
url: URL to request
|
|
78
|
+
**kwargs: Additional arguments for the request
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Flask test response
|
|
82
|
+
"""
|
|
83
|
+
headers = kwargs.pop("headers", {})
|
|
84
|
+
|
|
85
|
+
# Only set Content-Type for methods that have a body
|
|
86
|
+
if verb in ("post", "put", "patch", "delete"):
|
|
87
|
+
headers["Content-Type"] = "application/json"
|
|
88
|
+
|
|
89
|
+
data = kwargs.get("data")
|
|
90
|
+
if data is not None:
|
|
91
|
+
data = json.dumps(data)
|
|
92
|
+
headers["Content-Length"] = len(data)
|
|
93
|
+
kwargs["data"] = data
|
|
94
|
+
|
|
95
|
+
if self._user:
|
|
96
|
+
headers["X-API-KEY"] = kwargs.get("X-API-KEY", self._user.apikey)
|
|
97
|
+
|
|
98
|
+
kwargs["headers"] = headers
|
|
99
|
+
method = getattr(self.client, verb)
|
|
100
|
+
return method(url, **kwargs)
|
|
101
|
+
|
|
51
102
|
def get(self, url, *args, **kwargs):
|
|
52
|
-
|
|
103
|
+
"""Perform a GET request with JSON handling."""
|
|
104
|
+
return self.perform("get", url, *args, **kwargs)
|
|
53
105
|
|
|
54
106
|
def post(self, url, data=None, json=True, *args, **kwargs):
|
|
55
|
-
|
|
107
|
+
"""
|
|
108
|
+
Perform a POST request.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
url: URL to request
|
|
112
|
+
data: Data to send (will be JSON-encoded if json=True)
|
|
113
|
+
json: If True, send as JSON (default: True)
|
|
114
|
+
*args, **kwargs: Additional arguments
|
|
115
|
+
"""
|
|
116
|
+
if not json:
|
|
117
|
+
return self.client.post(url, data=data or {}, *args, **kwargs)
|
|
118
|
+
return self.perform("post", url, data=data or {}, *args, **kwargs)
|
|
56
119
|
|
|
57
120
|
def put(self, url, data=None, json=True, *args, **kwargs):
|
|
58
|
-
|
|
121
|
+
"""
|
|
122
|
+
Perform a PUT request.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
url: URL to request
|
|
126
|
+
data: Data to send (will be JSON-encoded if json=True)
|
|
127
|
+
json: If True, send as JSON (default: True)
|
|
128
|
+
*args, **kwargs: Additional arguments
|
|
129
|
+
"""
|
|
130
|
+
if not json:
|
|
131
|
+
return self.client.put(url, data=data or {}, *args, **kwargs)
|
|
132
|
+
return self.perform("put", url, data=data or {}, *args, **kwargs)
|
|
59
133
|
|
|
60
134
|
def patch(self, url, data=None, json=True, *args, **kwargs):
|
|
61
|
-
|
|
135
|
+
"""
|
|
136
|
+
Perform a PATCH request.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
url: URL to request
|
|
140
|
+
data: Data to send (will be JSON-encoded if json=True)
|
|
141
|
+
json: If True, send as JSON (default: True)
|
|
142
|
+
*args, **kwargs: Additional arguments
|
|
143
|
+
"""
|
|
144
|
+
if not json:
|
|
145
|
+
return self.client.patch(url, data=data or {}, *args, **kwargs)
|
|
146
|
+
return self.perform("patch", url, data=data or {}, *args, **kwargs)
|
|
62
147
|
|
|
63
148
|
def delete(self, url, data=None, *args, **kwargs):
|
|
64
|
-
|
|
149
|
+
"""Perform a DELETE request with JSON handling."""
|
|
150
|
+
return self.perform("delete", url, data=data or {}, *args, **kwargs)
|
|
65
151
|
|
|
66
152
|
def options(self, url, data=None, *args, **kwargs):
|
|
67
|
-
|
|
153
|
+
"""Perform an OPTIONS request with JSON handling."""
|
|
154
|
+
return self.perform("options", url, data=data or {}, *args, **kwargs)
|
|
68
155
|
|
|
69
156
|
def assertStatus(self, response, status_code, message=None):
|
|
70
157
|
__tracebackhide__ = True
|
|
@@ -4,6 +4,7 @@ from werkzeug.test import TestResponse
|
|
|
4
4
|
from udata.core.activity.models import Activity
|
|
5
5
|
from udata.core.dataset.factories import DatasetFactory
|
|
6
6
|
from udata.core.dataset.models import Dataset
|
|
7
|
+
from udata.core.organization.factories import OrganizationFactory
|
|
7
8
|
from udata.core.reuse.factories import ReuseFactory
|
|
8
9
|
from udata.core.reuse.models import Reuse
|
|
9
10
|
from udata.core.topic.factories import TopicFactory
|
|
@@ -111,3 +112,38 @@ class ActivityAPITest(APITestCase):
|
|
|
111
112
|
assert activity_data["related_to_id"] == str(topic.id)
|
|
112
113
|
assert activity_data["related_to_kind"] == "Topic"
|
|
113
114
|
assert activity_data["related_to_url"] == topic.self_api_url()
|
|
115
|
+
|
|
116
|
+
def test_activity_api_list_with_private_visible_to_owner(self) -> None:
|
|
117
|
+
"""Owner should see activities about their own private objects."""
|
|
118
|
+
owner = UserFactory()
|
|
119
|
+
dataset = DatasetFactory(private=True, owner=owner)
|
|
120
|
+
FakeDatasetActivity.objects.create(actor=UserFactory(), related_to=dataset)
|
|
121
|
+
|
|
122
|
+
# Anonymous user won't see it
|
|
123
|
+
response = self.get(url_for("api.activity"))
|
|
124
|
+
assert200(response)
|
|
125
|
+
assert len(response.json["data"]) == 0
|
|
126
|
+
|
|
127
|
+
# Owner should see their own private dataset activity
|
|
128
|
+
self.login(owner)
|
|
129
|
+
response = self.get(url_for("api.activity"))
|
|
130
|
+
assert200(response)
|
|
131
|
+
assert len(response.json["data"]) == 1
|
|
132
|
+
|
|
133
|
+
def test_activity_api_list_with_private_visible_to_org_member(self) -> None:
|
|
134
|
+
"""Organization members should see activities about their org's private objects."""
|
|
135
|
+
member = UserFactory()
|
|
136
|
+
org = OrganizationFactory(admins=[member])
|
|
137
|
+
dataset = DatasetFactory(private=True, organization=org)
|
|
138
|
+
FakeDatasetActivity.objects.create(actor=UserFactory(), related_to=dataset)
|
|
139
|
+
|
|
140
|
+
# Anonymous user won't see it
|
|
141
|
+
response = self.get(url_for("api.activity"))
|
|
142
|
+
assert200(response)
|
|
143
|
+
assert len(response.json["data"]) == 0
|
|
144
|
+
|
|
145
|
+
# Org member should see the private dataset activity
|
|
146
|
+
self.login(member)
|
|
147
|
+
response = self.get(url_for("api.activity"))
|
|
148
|
+
assert200(response)
|
|
149
|
+
assert len(response.json["data"]) == 1
|