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
@@ -1,75 +1,33 @@
1
- from datetime import datetime
2
-
3
- import pytz
4
1
  from flask import url_for
5
2
 
6
- from udata.core.user.factories import UserFactory
7
- from udata.features.notifications import actions
8
-
9
- from .api import APITestCase, DBTestCase
10
-
11
-
12
- class NotificationsMixin(object):
13
- def setUp(self):
14
- actions._providers = {}
15
-
16
-
17
- class NotificationsActionsTest(NotificationsMixin, DBTestCase):
18
- def test_registered_provider_is_listed(self):
19
- def fake_provider(user):
20
- return []
21
-
22
- actions.register_provider("fake", fake_provider)
23
-
24
- self.assertIn("fake", actions.list_providers())
3
+ from udata.core.organization.factories import OrganizationFactory
4
+ from udata.core.organization.models import Member
25
5
 
26
- def test_registered_provider_with_decorator_is_listed(self):
27
- @actions.notifier("fake")
28
- def fake_provider(user):
29
- return []
6
+ from .api import APITestCase
30
7
 
31
- self.assertIn("fake", actions.list_providers())
32
8
 
33
- def test_registered_provider_provide_values(self):
34
- dt = datetime.utcnow()
35
-
36
- def fake_provider(user):
37
- return [(dt, {"some": "value"})]
38
-
39
- actions.register_provider("fake", fake_provider)
40
-
41
- user = UserFactory()
42
- notifs = actions.get_notifications(user)
43
-
44
- self.assertEqual(len(notifs), 1)
45
- self.assertEqual(notifs[0]["type"], "fake")
46
- self.assertEqual(notifs[0]["details"], {"some": "value"})
47
- self.assertEqualDates(notifs[0]["created_on"], dt)
48
-
49
-
50
- class NotificationsAPITest(NotificationsMixin, APITestCase):
9
+ class NotificationsAPITest(APITestCase):
51
10
  def test_no_notifications(self):
52
11
  self.login()
53
12
  response = self.get(url_for("api.notifications"))
54
13
  self.assert200(response)
55
14
 
56
- self.assertEqual(len(response.json), 0)
15
+ self.assertEqual(response.json["total"], 0)
57
16
 
58
17
  def test_has_notifications(self):
18
+ admin = self.login()
59
19
  self.login()
60
- dt = datetime.utcnow()
20
+ organization = OrganizationFactory(members=[Member(user=admin, role="admin")])
21
+ data = {"comment": "a comment"}
61
22
 
62
- @actions.notifier("fake")
63
- def fake_notifier(user):
64
- return [(dt, {"some": "value"}), (dt, {"another": "value"})]
23
+ response = self.post(url_for("api.request_membership", org=organization), data)
24
+ self.assert201(response)
65
25
 
26
+ self.login(admin)
66
27
  response = self.get(url_for("api.notifications"))
67
28
  self.assert200(response)
68
29
 
69
- self.assertEqual(len(response.json), 2)
70
-
71
- for notification in response.json:
72
- self.assertEqual(notification["created_on"], pytz.utc.localize(dt).isoformat())
73
- self.assertEqual(notification["type"], "fake")
74
- self.assertEqual(response.json[0]["details"], {"some": "value"})
75
- self.assertEqual(response.json[1]["details"], {"another": "value"})
30
+ self.assertEqual(response.json["total"], 1)
31
+ self.assertEqual(
32
+ response.json["data"][0]["details"]["request_organization"]["id"], str(organization.id)
33
+ )
@@ -0,0 +1,43 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import pytest
4
+ from flask import current_app
5
+
6
+ from udata.core.organization.models import Member, MembershipRequest, Organization
7
+ from udata.core.user.factories import UserFactory
8
+ from udata.features.notifications import tasks
9
+ from udata.features.notifications.models import Notification
10
+ from udata.tests.api import APITestCase
11
+
12
+
13
+ class UserTasksTest(APITestCase):
14
+ @pytest.mark.options(DAYS_AFTER_NOTIFICATION_EXPIRED=3)
15
+ def test_notify_inactive_users(self):
16
+ self.login()
17
+ member = Member(user=self.user, role="admin")
18
+ org = Organization.objects.create(
19
+ name="with transfert", description="XXX", members=[member]
20
+ )
21
+
22
+ notification_handled_date = (
23
+ datetime.utcnow()
24
+ - timedelta(days=current_app.config["DAYS_AFTER_NOTIFICATION_EXPIRED"])
25
+ - timedelta(days=1) # add margin
26
+ )
27
+
28
+ applicant = UserFactory()
29
+
30
+ request = MembershipRequest(user=applicant, comment="test")
31
+ org.add_membership_request(request)
32
+
33
+ assert Notification.objects.count() == 1
34
+
35
+ request.status = "accepted"
36
+ request.handled_by = self.user
37
+ request.handled_on = notification_handled_date
38
+ org.save()
39
+ MembershipRequest.after_handle.send(request, org=org)
40
+
41
+ tasks.delete_expired_notifications()
42
+
43
+ assert Notification.objects.count() == 0
udata/tests/test_owned.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from mongoengine import post_save
2
2
 
3
3
  import udata.core.owned as owned
4
+ from udata.core.dataset.permissions import OwnableReadPermission
4
5
  from udata.core.organization.factories import OrganizationFactory
5
6
  from udata.core.organization.models import Organization
6
7
  from udata.core.user.factories import AdminFactory, UserFactory
7
8
  from udata.core.user.models import User
8
9
  from udata.models import Member
9
10
  from udata.mongo import db
10
- from udata.tests.api import DBTestCase
11
+ from udata.tests.api import APITestCase, DBTestCase
11
12
 
12
13
 
13
14
  class CustomQuerySet(owned.OwnedQuerySet):
@@ -265,3 +266,82 @@ class OwnedQuerysetTest(DBTestCase):
265
266
  name="private_owned_by_other_user"
266
267
  )
267
268
  self.assertEqual(len(result), 0)
269
+
270
+
271
+ class OwnableReadPermissionTest(APITestCase):
272
+ def setUp(self):
273
+ super().setUp()
274
+ from flask import g
275
+ from flask_principal import AnonymousIdentity
276
+
277
+ g.identity = AnonymousIdentity()
278
+
279
+ def test_public_object_visible_by_anonymous(self):
280
+ """Public objects should be visible by anonymous users."""
281
+ obj = Owned.objects.create(owner=UserFactory(), private=False)
282
+ assert OwnableReadPermission(obj).can() is True
283
+
284
+ def test_public_object_visible_by_authenticated(self):
285
+ """Public objects should be visible by authenticated users."""
286
+ obj = Owned.objects.create(owner=UserFactory(), private=False)
287
+ self.login()
288
+ assert OwnableReadPermission(obj).can() is True
289
+
290
+ def test_private_object_not_visible_by_anonymous(self):
291
+ """Private objects should not be visible by anonymous users."""
292
+ obj = Owned.objects.create(owner=UserFactory(), private=True)
293
+ assert OwnableReadPermission(obj).can() is False
294
+
295
+ def test_private_object_not_visible_by_other_user(self):
296
+ """Private objects should not be visible by other users."""
297
+ obj = Owned.objects.create(owner=UserFactory(), private=True)
298
+ self.login()
299
+ assert OwnableReadPermission(obj).can() is False
300
+
301
+ def test_private_object_visible_by_owner(self):
302
+ """Private objects should be visible by their owner."""
303
+ owner = UserFactory()
304
+ obj = Owned.objects.create(owner=owner, private=True)
305
+ self.login(owner)
306
+ assert OwnableReadPermission(obj).can() is True
307
+
308
+ def test_private_object_visible_by_org_admin(self):
309
+ """Private objects should be visible by organization admins."""
310
+ admin = UserFactory()
311
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
312
+ obj = Owned.objects.create(organization=org, private=True)
313
+ self.login(admin)
314
+ assert OwnableReadPermission(obj).can() is True
315
+
316
+ def test_private_object_visible_by_org_editor(self):
317
+ """Private objects should be visible by organization editors."""
318
+ editor = UserFactory()
319
+ org = OrganizationFactory(members=[Member(user=editor, role="editor")])
320
+ obj = Owned.objects.create(organization=org, private=True)
321
+ self.login(editor)
322
+ assert OwnableReadPermission(obj).can() is True
323
+
324
+ def test_private_object_not_visible_by_other_org_member(self):
325
+ """Private objects should not be visible by members of other organizations."""
326
+ member = UserFactory()
327
+ OrganizationFactory(members=[Member(user=member, role="admin")])
328
+ org = OrganizationFactory()
329
+ obj = Owned.objects.create(organization=org, private=True)
330
+ self.login(member)
331
+ assert OwnableReadPermission(obj).can() is False
332
+
333
+ def test_private_object_visible_by_admin(self):
334
+ """Private objects should be visible by sysadmins."""
335
+ admin = AdminFactory()
336
+ obj = Owned.objects.create(owner=UserFactory(), private=True)
337
+ self.login(admin)
338
+ assert OwnableReadPermission(obj).can() is True
339
+
340
+ def test_object_without_private_attribute(self):
341
+ """Objects without private attribute should be visible by everyone."""
342
+
343
+ class OwnedWithoutPrivate(owned.Owned, db.Document):
344
+ name = db.StringField()
345
+
346
+ obj = OwnedWithoutPrivate.objects.create(owner=UserFactory())
347
+ assert OwnableReadPermission(obj).can() is True
@@ -13,7 +13,7 @@ from udata.core.storages import utils
13
13
  from udata.core.storages.api import META, chunk_filename
14
14
  from udata.core.storages.tasks import purge_chunks
15
15
  from udata.tests import PytestOnlyTestCase
16
- from udata.tests.api import PytestOnlyDBTestCase
16
+ from udata.tests.api import PytestOnlyAPITestCase
17
17
  from udata.utils import faker
18
18
 
19
19
  from .helpers import assert200, assert400
@@ -108,12 +108,13 @@ class ConfigurableAllowedExtensionsTest(PytestOnlyTestCase):
108
108
 
109
109
 
110
110
  @pytest.mark.usefixtures("instance_path")
111
- class StorageUploadViewTest(PytestOnlyDBTestCase):
112
- def test_standard_upload(self, client):
113
- client.login()
114
- response = client.post(
115
- url_for("test-storage.upload", name="resources"),
111
+ class StorageUploadViewTest(PytestOnlyAPITestCase):
112
+ def test_standard_upload(self):
113
+ self.login()
114
+ response = self.post(
115
+ url_for("storage.upload", name="resources"),
116
116
  {"file": (BytesIO(b"aaa"), "Test with spaces.TXT")},
117
+ json=False,
117
118
  )
118
119
 
119
120
  assert200(response)
@@ -128,14 +129,14 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
128
129
  assert response.json["url"] == expected
129
130
  assert response.json["mime"] == "text/plain"
130
131
 
131
- def test_chunked_upload(self, client):
132
- client.login()
133
- url = url_for("test-storage.upload", name="tmp")
132
+ def test_chunked_upload(self):
133
+ self.login()
134
+ url = url_for("storage.upload", name="tmp")
134
135
  uuid = str(uuid4())
135
136
  parts = 4
136
137
 
137
138
  for i in range(parts):
138
- response = client.post(
139
+ response = self.post(
139
140
  url,
140
141
  {
141
142
  "file": (BytesIO(b"a"), "blob"),
@@ -147,6 +148,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
147
148
  "totalparts": parts,
148
149
  "chunksize": 1,
149
150
  },
151
+ json=False,
150
152
  )
151
153
 
152
154
  assert200(response)
@@ -157,7 +159,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
157
159
  assert "sha1" not in response.json
158
160
  assert "url" not in response.json
159
161
 
160
- response = client.post(
162
+ response = self.post(
161
163
  url,
162
164
  {
163
165
  "uuid": uuid,
@@ -165,6 +167,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
165
167
  "totalfilesize": parts,
166
168
  "totalparts": parts,
167
169
  },
170
+ json=False,
168
171
  )
169
172
  assert "filename" in response.json
170
173
  assert "url" in response.json
@@ -180,13 +183,13 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
180
183
  assert storages.tmp.read(filename) == b"aaaa"
181
184
  assert list(storages.chunks.list_files()) == []
182
185
 
183
- def test_chunked_upload_bad_chunk(self, client):
184
- client.login()
185
- url = url_for("test-storage.upload", name="tmp")
186
+ def test_chunked_upload_bad_chunk(self):
187
+ self.login()
188
+ url = url_for("storage.upload", name="tmp")
186
189
  uuid = str(uuid4())
187
190
  parts = 4
188
191
 
189
- response = client.post(
192
+ response = self.post(
190
193
  url,
191
194
  {
192
195
  "file": (BytesIO(b"a"), "blob"),
@@ -198,6 +201,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
198
201
  "totalparts": parts,
199
202
  "chunksize": 10, # Does not match
200
203
  },
204
+ json=False,
201
205
  )
202
206
 
203
207
  assert400(response)
@@ -210,10 +214,12 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
210
214
 
211
215
  assert list(storages.chunks.list_files()) == []
212
216
 
213
- def test_upload_resource_bad_request(self, client):
214
- client.login()
215
- response = client.post(
216
- url_for("test-storage.upload", name="tmp"), {"bad": (BytesIO(b"aaa"), "test.txt")}
217
+ def test_upload_resource_bad_request(self):
218
+ self.login()
219
+ response = self.post(
220
+ url_for("storage.upload", name="tmp"),
221
+ {"bad": (BytesIO(b"aaa"), "test.txt")},
222
+ json=False,
217
223
  )
218
224
 
219
225
  assert400(response)
@@ -16,6 +16,7 @@ from udata.core.topic.factories import (
16
16
  TopicWithElementsFactory,
17
17
  )
18
18
  from udata.core.topic.models import Topic, TopicElement
19
+ from udata.core.user.factories import UserFactory
19
20
  from udata.search import reindex
20
21
  from udata.tests.api import PytestOnlyDBTestCase
21
22
  from udata.tests.helpers import assert_emit
@@ -55,26 +56,32 @@ class TopicModelTest(PytestOnlyDBTestCase):
55
56
  TopicWithElementsFactory()
56
57
  job_reindex.assert_called()
57
58
 
58
- def test_topic_activities(self, api, mocker):
59
+ def test_topic_activities(self, app, mocker):
59
60
  # A user must be authenticated for activities to be emitted
60
- user = api.login()
61
+ from flask_login import login_user
62
+
63
+ user = UserFactory()
61
64
 
62
65
  mock_created = mocker.patch.object(UserCreatedTopic, "emit")
63
66
  mock_updated = mocker.patch.object(UserUpdatedTopic, "emit")
64
67
 
65
- with assert_emit(Topic.on_create):
66
- topic = TopicFactory(owner=user)
67
- mock_created.assert_called()
68
+ with app.test_request_context():
69
+ login_user(user)
68
70
 
69
- with assert_emit(Topic.on_update):
70
- topic.name = "new name"
71
- topic.save()
72
- mock_updated.assert_called()
71
+ with assert_emit(Topic.on_create):
72
+ topic = TopicFactory(owner=user)
73
+ mock_created.assert_called()
73
74
 
74
- def test_topic_element_activities(self, api, mocker):
75
+ with assert_emit(Topic.on_update):
76
+ topic.name = "new name"
77
+ topic.save()
78
+ mock_updated.assert_called()
79
+
80
+ def test_topic_element_activities(self, app, mocker):
75
81
  # A user must be authenticated for activities to be emitted
76
- user = api.login()
77
- topic = TopicFactory(owner=user)
82
+ from flask_login import login_user
83
+
84
+ user = UserFactory()
78
85
 
79
86
  mock_topic_created = mocker.patch.object(UserCreatedTopic, "emit")
80
87
  mock_topic_updated = mocker.patch.object(UserUpdatedTopic, "emit")
@@ -82,55 +89,64 @@ class TopicModelTest(PytestOnlyDBTestCase):
82
89
  mock_element_updated = mocker.patch.object(UserUpdatedTopicElement, "emit")
83
90
  mock_element_deleted = mocker.patch.object(UserDeletedTopicElement, "emit")
84
91
 
85
- # Test TopicElement creation
86
- element = TopicElementDatasetFactory(topic=topic)
87
- mock_element_created.assert_called_once()
88
- mock_topic_created.assert_not_called()
89
- mock_topic_updated.assert_not_called()
90
- mock_element_updated.assert_not_called()
91
- mock_element_deleted.assert_not_called()
92
-
93
- call_args = mock_element_created.call_args
94
- assert call_args[0][0] == topic # related_to
95
- assert call_args[0][1] == topic.organization # organization
96
- assert call_args[1]["extras"]["element_id"] == str(element.id)
97
-
98
- mock_element_created.reset_mock()
99
-
100
- # Test TopicElement update
101
- element.title = "Updated title"
102
- element.extras = {"key": "value"}
103
- element.save()
104
- mock_element_updated.assert_called_once()
105
- mock_topic_created.assert_not_called()
106
- mock_topic_updated.assert_not_called()
107
- mock_element_created.assert_not_called()
108
- mock_element_deleted.assert_not_called()
109
-
110
- call_args = mock_element_updated.call_args
111
- assert call_args[0][0] == topic # related_to
112
- assert call_args[0][1] == topic.organization # organization
113
- assert call_args[0][2] == ["title", "extras"] # changed_fields
114
- assert call_args[1]["extras"]["element_id"] == str(element.id)
115
-
116
- mock_element_updated.reset_mock()
117
-
118
- # Test TopicElement deletion
119
- element_id = element.id
120
- element.delete()
121
-
122
- # Deletion should only trigger delete activity
123
- mock_element_deleted.assert_called_once()
124
- mock_element_updated.assert_not_called()
125
- mock_topic_created.assert_not_called()
126
- mock_topic_updated.assert_not_called()
127
- mock_element_created.assert_not_called()
128
-
129
- # Verify delete activity arguments
130
- delete_call_args = mock_element_deleted.call_args
131
- assert delete_call_args[0][0] == topic # related_to
132
- assert delete_call_args[0][1] == topic.organization # organization
133
- assert delete_call_args[1]["extras"]["element_id"] == str(element_id)
92
+ with app.test_request_context():
93
+ login_user(user)
94
+
95
+ topic = TopicFactory(owner=user)
96
+
97
+ # Reset mocks after topic creation since it emits activities
98
+ mock_topic_created.reset_mock()
99
+ mock_topic_updated.reset_mock()
100
+
101
+ # Test TopicElement creation
102
+ element = TopicElementDatasetFactory(topic=topic)
103
+ mock_element_created.assert_called_once()
104
+ mock_topic_created.assert_not_called()
105
+ mock_topic_updated.assert_not_called()
106
+ mock_element_updated.assert_not_called()
107
+ mock_element_deleted.assert_not_called()
108
+
109
+ call_args = mock_element_created.call_args
110
+ assert call_args[0][0] == topic # related_to
111
+ assert call_args[0][1] == topic.organization # organization
112
+ assert call_args[1]["extras"]["element_id"] == str(element.id)
113
+
114
+ mock_element_created.reset_mock()
115
+
116
+ # Test TopicElement update
117
+ element.title = "Updated title"
118
+ element.extras = {"key": "value"}
119
+ element.save()
120
+ mock_element_updated.assert_called_once()
121
+ mock_topic_created.assert_not_called()
122
+ mock_topic_updated.assert_not_called()
123
+ mock_element_created.assert_not_called()
124
+ mock_element_deleted.assert_not_called()
125
+
126
+ call_args = mock_element_updated.call_args
127
+ assert call_args[0][0] == topic # related_to
128
+ assert call_args[0][1] == topic.organization # organization
129
+ assert call_args[0][2] == ["title", "extras"] # changed_fields
130
+ assert call_args[1]["extras"]["element_id"] == str(element.id)
131
+
132
+ mock_element_updated.reset_mock()
133
+
134
+ # Test TopicElement deletion
135
+ element_id = element.id
136
+ element.delete()
137
+
138
+ # Deletion should only trigger delete activity
139
+ mock_element_deleted.assert_called_once()
140
+ mock_element_updated.assert_not_called()
141
+ mock_topic_created.assert_not_called()
142
+ mock_topic_updated.assert_not_called()
143
+ mock_element_created.assert_not_called()
144
+
145
+ # Verify delete activity arguments
146
+ delete_call_args = mock_element_deleted.call_args
147
+ assert delete_call_args[0][0] == topic # related_to
148
+ assert delete_call_args[0][1] == topic.organization # organization
149
+ assert delete_call_args[1]["extras"]["element_id"] == str(element_id)
134
150
 
135
151
  def test_topic_element_wrong_class(self):
136
152
  # use a model instance that is not supported
udata/tests/test_uris.py CHANGED
@@ -2,6 +2,7 @@ import pytest
2
2
 
3
3
  from udata import uris
4
4
  from udata.settings import Defaults
5
+ from udata.tests import PytestOnlyTestCase
5
6
 
6
7
  PUBLIC_HOSTS = [
7
8
  "http://foo.com/blah_blah",
@@ -289,3 +290,35 @@ def test_with_credentials(url):
289
290
  def test_with_credentials_disabled(url):
290
291
  with pytest.raises(uris.ValidationError, match="Credentials in URL are not allowed"):
291
292
  uris.validate(url, credentials=False)
293
+
294
+
295
+ @pytest.mark.options(CDATA_BASE_URL="http://localhost:3000/")
296
+ class CdataUrlTest(PytestOnlyTestCase):
297
+ def test_cdata_url_without_base_url(self, app):
298
+ app.config["CDATA_BASE_URL"] = None
299
+ assert uris.cdata_url("test") is None
300
+
301
+ def test_cdata_url_with_simple_uri(self):
302
+ assert uris.cdata_url("test") == "http://localhost:3000/test"
303
+
304
+ @pytest.mark.options(MAIL_CAMPAIGN="mail")
305
+ def test_cdata_url_with_mail_campaign(self):
306
+ assert (
307
+ uris.cdata_url("test", _mailCampaign=True)
308
+ == "http://localhost:3000/test?mtm_campaign=mail"
309
+ )
310
+
311
+ def test_cdata_url_with_trailing_slash(self):
312
+ assert uris.cdata_url("test/") == "http://localhost:3000/test"
313
+
314
+ def test_cdata_url_with_append(self):
315
+ assert (
316
+ uris.cdata_url("test/", append="/discussions")
317
+ == "http://localhost:3000/test/discussions"
318
+ )
319
+
320
+ def test_cdata_url_with_append_and_kwargs(self):
321
+ assert (
322
+ uris.cdata_url("test/", append="/discussions", discussion_id="disc_id")
323
+ == "http://localhost:3000/test/discussions?discussion_id=disc_id"
324
+ )