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.

Files changed (177) hide show
  1. udata/api/__init__.py +2 -8
  2. udata/api_fields.py +35 -4
  3. udata/app.py +30 -50
  4. udata/auth/__init__.py +29 -6
  5. udata/auth/forms.py +8 -6
  6. udata/auth/views.py +6 -3
  7. udata/commands/__init__.py +2 -14
  8. udata/commands/db.py +13 -25
  9. udata/commands/info.py +0 -16
  10. udata/commands/serve.py +3 -11
  11. udata/commands/tests/test_fixtures.py +9 -9
  12. udata/core/access_type/api.py +1 -1
  13. udata/core/access_type/constants.py +12 -8
  14. udata/core/activity/api.py +5 -6
  15. udata/core/avatars/api.py +43 -0
  16. udata/core/avatars/test_avatar_api.py +30 -0
  17. udata/core/badges/tests/test_commands.py +6 -6
  18. udata/core/csv.py +5 -0
  19. udata/core/dataservices/models.py +15 -3
  20. udata/core/dataservices/tasks.py +7 -0
  21. udata/core/dataset/api.py +2 -0
  22. udata/core/dataset/models.py +2 -2
  23. udata/core/dataset/permissions.py +31 -0
  24. udata/core/dataset/tasks.py +50 -10
  25. udata/core/discussions/models.py +1 -0
  26. udata/core/metrics/__init__.py +0 -6
  27. udata/core/organization/api.py +8 -5
  28. udata/core/organization/mails.py +1 -1
  29. udata/core/organization/models.py +9 -1
  30. udata/core/organization/notifications.py +84 -0
  31. udata/core/organization/permissions.py +1 -1
  32. udata/core/organization/tasks.py +3 -0
  33. udata/core/pages/tests/test_api.py +32 -0
  34. udata/core/post/api.py +24 -69
  35. udata/core/post/models.py +84 -16
  36. udata/core/post/tests/test_api.py +24 -1
  37. udata/core/reports/api.py +18 -0
  38. udata/core/reports/models.py +42 -2
  39. udata/core/reuse/models.py +1 -1
  40. udata/core/reuse/tasks.py +7 -0
  41. udata/core/site/models.py +2 -6
  42. udata/core/spatial/commands.py +2 -4
  43. udata/core/spatial/forms.py +2 -2
  44. udata/core/spatial/models.py +0 -10
  45. udata/core/spatial/tests/test_api.py +1 -36
  46. udata/core/user/models.py +15 -2
  47. udata/cors.py +2 -5
  48. udata/db/migrations.py +279 -0
  49. udata/features/notifications/api.py +7 -18
  50. udata/features/notifications/models.py +56 -0
  51. udata/features/notifications/tasks.py +25 -0
  52. udata/flask_mongoengine/engine.py +0 -4
  53. udata/frontend/__init__.py +3 -122
  54. udata/frontend/markdown.py +2 -1
  55. udata/harvest/actions.py +24 -9
  56. udata/harvest/api.py +30 -22
  57. udata/harvest/backends/__init__.py +21 -9
  58. udata/harvest/backends/base.py +29 -3
  59. udata/harvest/backends/ckan/harvesters.py +13 -2
  60. udata/harvest/backends/dcat.py +3 -0
  61. udata/harvest/backends/maaf.py +1 -0
  62. udata/harvest/commands.py +39 -4
  63. udata/harvest/filters.py +17 -6
  64. udata/harvest/forms.py +9 -6
  65. udata/harvest/models.py +16 -0
  66. udata/harvest/permissions.py +27 -0
  67. udata/harvest/tasks.py +3 -5
  68. udata/harvest/tests/ckan/test_ckan_backend.py +35 -2
  69. udata/harvest/tests/ckan/test_ckan_backend_errors.py +1 -1
  70. udata/harvest/tests/ckan/test_ckan_backend_filters.py +1 -1
  71. udata/harvest/tests/ckan/test_dkan_backend.py +1 -1
  72. udata/harvest/tests/dcat/udata.xml +6 -6
  73. udata/harvest/tests/factories.py +1 -1
  74. udata/harvest/tests/test_actions.py +63 -8
  75. udata/harvest/tests/test_api.py +278 -123
  76. udata/harvest/tests/test_base_backend.py +88 -1
  77. udata/harvest/tests/test_dcat_backend.py +60 -13
  78. udata/harvest/tests/test_filters.py +6 -0
  79. udata/i18n.py +11 -273
  80. udata/mail.py +5 -1
  81. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  82. udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
  83. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  84. udata/models/__init__.py +0 -8
  85. udata/mongo/slug_fields.py +1 -1
  86. udata/rdf.py +45 -6
  87. udata/routing.py +2 -10
  88. udata/sentry.py +4 -10
  89. udata/settings.py +23 -17
  90. udata/tasks.py +4 -3
  91. udata/templates/mail/message.html +5 -31
  92. udata/tests/__init__.py +28 -12
  93. udata/tests/api/__init__.py +108 -21
  94. udata/tests/api/test_activities_api.py +36 -0
  95. udata/tests/api/test_auth_api.py +121 -95
  96. udata/tests/api/test_base_api.py +7 -4
  97. udata/tests/api/test_dataservices_api.py +29 -1
  98. udata/tests/api/test_datasets_api.py +45 -21
  99. udata/tests/api/test_organizations_api.py +192 -197
  100. udata/tests/api/test_reports_api.py +157 -0
  101. udata/tests/api/test_reuses_api.py +147 -147
  102. udata/tests/api/test_security_api.py +12 -12
  103. udata/tests/api/test_swagger.py +4 -4
  104. udata/tests/api/test_tags_api.py +8 -8
  105. udata/tests/api/test_user_api.py +13 -1
  106. udata/tests/apiv2/test_swagger.py +4 -4
  107. udata/tests/apiv2/test_topics.py +1 -1
  108. udata/tests/cli/test_cli_base.py +8 -9
  109. udata/tests/dataset/test_dataset_commands.py +4 -4
  110. udata/tests/dataset/test_dataset_model.py +66 -26
  111. udata/tests/dataset/test_dataset_rdf.py +99 -5
  112. udata/tests/dataset/test_resource_preview.py +0 -1
  113. udata/tests/frontend/test_auth.py +24 -1
  114. udata/tests/frontend/test_csv.py +0 -3
  115. udata/tests/helpers.py +37 -27
  116. udata/tests/organization/test_notifications.py +67 -2
  117. udata/tests/plugin.py +6 -261
  118. udata/tests/site/test_site_csv_exports.py +22 -10
  119. udata/tests/test_activity.py +9 -9
  120. udata/tests/test_cors.py +1 -1
  121. udata/tests/test_dcat_commands.py +2 -2
  122. udata/tests/test_discussions.py +5 -5
  123. udata/tests/test_migrations.py +181 -481
  124. udata/tests/test_notifications.py +15 -57
  125. udata/tests/test_notifications_task.py +43 -0
  126. udata/tests/test_owned.py +81 -1
  127. udata/tests/test_storages.py +25 -19
  128. udata/tests/test_topics.py +77 -61
  129. udata/tests/test_uris.py +33 -0
  130. udata/tests/workers/test_jobs_commands.py +23 -23
  131. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  132. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  133. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  134. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  135. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  136. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  137. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  138. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  139. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  140. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  141. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  142. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  143. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  144. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  145. udata/translations/udata.pot +215 -106
  146. udata/uris.py +0 -2
  147. udata/utils.py +5 -0
  148. udata-14.4.1.dev7.dist-info/METADATA +109 -0
  149. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +153 -166
  150. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +3 -5
  151. udata/core/followers/views.py +0 -15
  152. udata/core/post/forms.py +0 -30
  153. udata/entrypoints.py +0 -93
  154. udata/features/identicon/__init__.py +0 -0
  155. udata/features/identicon/api.py +0 -13
  156. udata/features/identicon/backends.py +0 -131
  157. udata/features/identicon/tests/__init__.py +0 -0
  158. udata/features/identicon/tests/test_backends.py +0 -18
  159. udata/features/territories/__init__.py +0 -49
  160. udata/features/territories/api.py +0 -25
  161. udata/features/territories/models.py +0 -51
  162. udata/flask_mongoengine/json.py +0 -38
  163. udata/migrations/__init__.py +0 -367
  164. udata/templates/mail/base.html +0 -105
  165. udata/templates/mail/base.txt +0 -6
  166. udata/templates/mail/button.html +0 -3
  167. udata/templates/mail/layouts/1-column.html +0 -19
  168. udata/templates/mail/layouts/2-columns.html +0 -20
  169. udata/templates/mail/layouts/center-panel.html +0 -16
  170. udata/tests/cli/test_db_cli.py +0 -68
  171. udata/tests/features/territories/__init__.py +0 -20
  172. udata/tests/features/territories/test_territories_api.py +0 -185
  173. udata/tests/frontend/test_hooks.py +0 -149
  174. udata-13.0.1.dev12.dist-info/METADATA +0 -133
  175. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
  176. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
  177. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
@@ -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