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
@@ -43,23 +43,23 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
43
43
  if migration_path.exists():
44
44
  migration_path.unlink()
45
45
 
46
- def test_list_available_migrations(self, cli):
46
+ def test_list_available_migrations(self):
47
47
  """Test that we can list available migrations"""
48
- result = cli("db status")
48
+ result = self.cli("db status")
49
49
  assert result.exit_code == 0
50
50
  # Should contain at least some output (may be empty if no migrations)
51
51
 
52
- def test_migration_workflow(self, cli, db, migration_file):
52
+ def test_migration_workflow(self, db, migration_file):
53
53
  """Test complete migration workflow: info, migrate, status, unrecord"""
54
54
 
55
55
  # 1. Test info command on non-executed migration
56
- result = cli(f"db info {migration_file}")
56
+ result = self.cli(f"db info {migration_file}")
57
57
  assert result.exit_code == 0
58
58
  assert "Test migration for integration testing" in result.output
59
59
  assert "Not applied" in result.output
60
60
 
61
61
  # 2. Test migrate command
62
- result = cli("db migrate")
62
+ result = self.cli("db migrate")
63
63
  assert result.exit_code == 0
64
64
 
65
65
  # Verify migration was executed
@@ -76,17 +76,17 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
76
76
  assert record["ops"][0]["success"] is True
77
77
 
78
78
  # 3. Test status command after migration
79
- result = cli("db status")
79
+ result = self.cli("db status")
80
80
  assert result.exit_code == 0
81
81
  assert migration_file.replace(".py", "") in result.output
82
82
 
83
83
  # 4. Test info command on executed migration
84
- result = cli(f"db info {migration_file}")
84
+ result = self.cli(f"db info {migration_file}")
85
85
  assert result.exit_code == 0
86
86
  assert "Test migration for integration testing" in result.output
87
87
 
88
88
  # 5. Test unrecord command
89
- result = cli(f"db unrecord {migration_file}")
89
+ result = self.cli(f"db unrecord {migration_file}")
90
90
  assert result.exit_code == 0
91
91
 
92
92
  # Verify migration record was removed
@@ -101,10 +101,10 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
101
101
  # Cleanup test data
102
102
  db.test_collection.delete_many({})
103
103
 
104
- def test_migrate_recordonly(self, cli, db, migration_file):
104
+ def test_migrate_recordonly(self, db, migration_file):
105
105
  """Test migrate with --record flag"""
106
106
 
107
- result = cli("db migrate --record")
107
+ result = self.cli("db migrate --record")
108
108
  assert result.exit_code == 0
109
109
 
110
110
  # Migration should be recorded
@@ -119,10 +119,10 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
119
119
  # Cleanup
120
120
  db.migrations.delete_one({"filename": migration_file})
121
121
 
122
- def test_migrate_dry_run(self, cli, db, migration_file):
122
+ def test_migrate_dry_run(self, db, migration_file):
123
123
  """Test migrate with --dry-run flag"""
124
124
 
125
- result = cli("db migrate --dry-run")
125
+ result = self.cli("db migrate --dry-run")
126
126
  assert result.exit_code == 0
127
127
 
128
128
  # Migration should NOT be recorded
@@ -133,18 +133,18 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
133
133
  inserted = db.test_collection.find_one()
134
134
  assert inserted is None
135
135
 
136
- def test_migrate_already_applied(self, cli, db, migration_file):
136
+ def test_migrate_already_applied(self, db, migration_file):
137
137
  """Test that already applied migrations are skipped"""
138
138
 
139
139
  # First migration
140
- result = cli("db migrate")
140
+ result = self.cli("db migrate")
141
141
  assert result.exit_code == 0
142
142
 
143
143
  # Count records
144
144
  count_before = db.test_collection.count_documents({})
145
145
 
146
146
  # Second migration attempt
147
- result = cli("db migrate")
147
+ result = self.cli("db migrate")
148
148
  assert result.exit_code == 0
149
149
  assert "Skipped" in result.output
150
150
 
@@ -156,7 +156,7 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
156
156
  db.test_collection.delete_many({})
157
157
  db.migrations.delete_one({"filename": migration_file})
158
158
 
159
- def test_unrecord_with_complete_filename(self, cli, db):
159
+ def test_unrecord_with_complete_filename(self, db):
160
160
  """Should unrecord migration with complete filename"""
161
161
  db.migrations.insert_one(
162
162
  {
@@ -172,11 +172,11 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
172
172
  ],
173
173
  }
174
174
  )
175
- result = cli("db unrecord test.py")
175
+ result = self.cli("db unrecord test.py")
176
176
  assert result.exit_code == 0
177
177
  assert db.migrations.count_documents({}) == 0
178
178
 
179
- def test_unrecord_without_parameters(self, cli, db):
179
+ def test_unrecord_without_parameters(self, db):
180
180
  """Should fail when no filename is provided"""
181
181
  db.migrations.insert_one(
182
182
  {
@@ -192,17 +192,17 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
192
192
  ],
193
193
  }
194
194
  )
195
- result = cli("db unrecord", check=False)
195
+ result = self.cli("db unrecord", expect_error=True)
196
196
  assert result.exit_code != 0
197
197
  assert db.migrations.count_documents({}) == 1
198
198
 
199
- def test_all_existing_migrations_can_run(self, cli, db):
199
+ def test_all_existing_migrations_can_run(self, db):
200
200
  """Test that all existing migrations can be executed without errors on a clean database"""
201
201
  # Get all available migrations
202
202
  all_migrations = migrations.list_available()
203
203
 
204
204
  # Run migrations
205
- result = cli("db migrate")
205
+ result = self.cli("db migrate")
206
206
  assert result.exit_code == 0
207
207
 
208
208
  # Verify all migrations were recorded successfully
@@ -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