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.

Files changed (130) hide show
  1. udata/api_fields.py +35 -4
  2. udata/app.py +18 -20
  3. udata/auth/__init__.py +29 -6
  4. udata/auth/forms.py +2 -2
  5. udata/auth/views.py +6 -3
  6. udata/commands/serve.py +3 -11
  7. udata/commands/tests/test_fixtures.py +9 -9
  8. udata/core/access_type/api.py +1 -1
  9. udata/core/access_type/constants.py +12 -8
  10. udata/core/activity/api.py +5 -6
  11. udata/core/badges/tests/test_commands.py +6 -6
  12. udata/core/csv.py +5 -0
  13. udata/core/dataservices/models.py +1 -1
  14. udata/core/dataservices/tasks.py +7 -0
  15. udata/core/dataset/api.py +2 -0
  16. udata/core/dataset/models.py +2 -2
  17. udata/core/dataset/permissions.py +31 -0
  18. udata/core/dataset/tasks.py +17 -5
  19. udata/core/discussions/models.py +1 -0
  20. udata/core/organization/api.py +8 -5
  21. udata/core/organization/mails.py +1 -1
  22. udata/core/organization/models.py +9 -1
  23. udata/core/organization/notifications.py +84 -0
  24. udata/core/organization/permissions.py +1 -1
  25. udata/core/organization/tasks.py +3 -0
  26. udata/core/pages/tests/test_api.py +32 -0
  27. udata/core/post/api.py +24 -69
  28. udata/core/post/models.py +84 -16
  29. udata/core/post/tests/test_api.py +24 -1
  30. udata/core/reports/api.py +18 -0
  31. udata/core/reports/models.py +42 -2
  32. udata/core/reuse/models.py +1 -1
  33. udata/core/reuse/tasks.py +7 -0
  34. udata/core/spatial/forms.py +2 -2
  35. udata/core/user/models.py +5 -1
  36. udata/features/notifications/api.py +7 -18
  37. udata/features/notifications/models.py +56 -0
  38. udata/features/notifications/tasks.py +25 -0
  39. udata/flask_mongoengine/engine.py +0 -4
  40. udata/frontend/markdown.py +2 -1
  41. udata/harvest/actions.py +21 -1
  42. udata/harvest/api.py +25 -8
  43. udata/harvest/backends/base.py +27 -1
  44. udata/harvest/backends/ckan/harvesters.py +11 -2
  45. udata/harvest/commands.py +33 -0
  46. udata/harvest/filters.py +17 -6
  47. udata/harvest/models.py +16 -0
  48. udata/harvest/permissions.py +27 -0
  49. udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
  50. udata/harvest/tests/test_actions.py +58 -5
  51. udata/harvest/tests/test_api.py +276 -122
  52. udata/harvest/tests/test_base_backend.py +86 -1
  53. udata/harvest/tests/test_dcat_backend.py +57 -10
  54. udata/harvest/tests/test_filters.py +6 -0
  55. udata/i18n.py +1 -4
  56. udata/mail.py +5 -1
  57. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  58. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  59. udata/mongo/slug_fields.py +1 -1
  60. udata/rdf.py +45 -6
  61. udata/routing.py +2 -2
  62. udata/settings.py +7 -0
  63. udata/tasks.py +1 -0
  64. udata/templates/mail/message.html +5 -31
  65. udata/tests/__init__.py +27 -2
  66. udata/tests/api/__init__.py +108 -21
  67. udata/tests/api/test_activities_api.py +36 -0
  68. udata/tests/api/test_auth_api.py +121 -95
  69. udata/tests/api/test_base_api.py +7 -4
  70. udata/tests/api/test_datasets_api.py +44 -19
  71. udata/tests/api/test_organizations_api.py +192 -197
  72. udata/tests/api/test_reports_api.py +157 -0
  73. udata/tests/api/test_reuses_api.py +147 -147
  74. udata/tests/api/test_security_api.py +12 -12
  75. udata/tests/api/test_swagger.py +4 -4
  76. udata/tests/api/test_tags_api.py +8 -8
  77. udata/tests/api/test_user_api.py +1 -1
  78. udata/tests/apiv2/test_swagger.py +4 -4
  79. udata/tests/cli/test_cli_base.py +8 -9
  80. udata/tests/dataset/test_dataset_commands.py +4 -4
  81. udata/tests/dataset/test_dataset_model.py +66 -26
  82. udata/tests/dataset/test_dataset_rdf.py +99 -5
  83. udata/tests/frontend/test_auth.py +24 -1
  84. udata/tests/frontend/test_csv.py +0 -3
  85. udata/tests/helpers.py +25 -27
  86. udata/tests/organization/test_notifications.py +67 -2
  87. udata/tests/plugin.py +6 -261
  88. udata/tests/site/test_site_csv_exports.py +22 -10
  89. udata/tests/test_activity.py +9 -9
  90. udata/tests/test_dcat_commands.py +2 -2
  91. udata/tests/test_discussions.py +5 -5
  92. udata/tests/test_migrations.py +21 -21
  93. udata/tests/test_notifications.py +15 -57
  94. udata/tests/test_notifications_task.py +43 -0
  95. udata/tests/test_owned.py +81 -1
  96. udata/tests/test_storages.py +25 -19
  97. udata/tests/test_topics.py +77 -61
  98. udata/tests/test_uris.py +33 -0
  99. udata/tests/workers/test_jobs_commands.py +23 -23
  100. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  101. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  102. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  103. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  104. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  105. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  106. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  107. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  108. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  109. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  110. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  111. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  112. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  113. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  114. udata/translations/udata.pot +215 -106
  115. udata/uris.py +0 -2
  116. udata-14.4.1.dev7.dist-info/METADATA +109 -0
  117. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +121 -123
  118. udata/core/post/forms.py +0 -30
  119. udata/flask_mongoengine/json.py +0 -38
  120. udata/templates/mail/base.html +0 -105
  121. udata/templates/mail/base.txt +0 -6
  122. udata/templates/mail/button.html +0 -3
  123. udata/templates/mail/layouts/1-column.html +0 -19
  124. udata/templates/mail/layouts/2-columns.html +0 -20
  125. udata/templates/mail/layouts/center-panel.html +0 -16
  126. udata-14.0.0.dist-info/METADATA +0 -132
  127. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
  128. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +0 -0
  129. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
  130. {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
@@ -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
- See explanation about `get`, `post` overrides in :TestClientOverride
17
+ API Test Case Mixin with integrated API client functionality.
15
18
 
16
- (switch from `data` in kwargs to `data` in args to avoid doing `data=data` and default to `json=True`)
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 inject_api(self, api):
35
+ def inject_client(self, app):
30
36
  """
31
- Inject API test client for compatibility with legacy tests.
37
+ Inject test client for Flask testing.
32
38
  """
33
- self.api = api
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
- with self.api.user(user) as user:
45
- yield user
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
- self.user = self.client.login(user)
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
- return self.api.get(url, *args, **kwargs)
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
- return self.api.post(url, data=data, json=json, *args, **kwargs)
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
- return self.api.put(url, data=data, json=json, *args, **kwargs)
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
- return self.api.patch(url, data=data, json=json, *args, **kwargs)
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
- return self.api.delete(url, data=data, *args, **kwargs)
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
- return self.api.options(url, data=data, *args, **kwargs)
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