udata 14.0.3.dev1__py3-none-any.whl → 14.7.3.dev4__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.
Files changed (151) hide show
  1. udata/api/__init__.py +2 -0
  2. udata/api_fields.py +120 -19
  3. udata/app.py +18 -20
  4. udata/auth/__init__.py +4 -7
  5. udata/auth/forms.py +3 -3
  6. udata/auth/views.py +13 -6
  7. udata/commands/dcat.py +1 -1
  8. udata/commands/serve.py +3 -11
  9. udata/core/activity/api.py +5 -6
  10. udata/core/badges/tests/test_tasks.py +0 -2
  11. udata/core/csv.py +5 -0
  12. udata/core/dataservices/api.py +8 -1
  13. udata/core/dataservices/apiv2.py +3 -6
  14. udata/core/dataservices/models.py +5 -2
  15. udata/core/dataservices/rdf.py +2 -1
  16. udata/core/dataservices/tasks.py +6 -2
  17. udata/core/dataset/api.py +30 -4
  18. udata/core/dataset/api_fields.py +1 -1
  19. udata/core/dataset/apiv2.py +1 -1
  20. udata/core/dataset/constants.py +2 -9
  21. udata/core/dataset/models.py +21 -9
  22. udata/core/dataset/permissions.py +31 -0
  23. udata/core/dataset/rdf.py +18 -16
  24. udata/core/dataset/tasks.py +16 -7
  25. udata/core/discussions/api.py +15 -1
  26. udata/core/discussions/models.py +6 -0
  27. udata/core/legal/__init__.py +0 -0
  28. udata/core/legal/mails.py +128 -0
  29. udata/core/organization/api.py +16 -5
  30. udata/core/organization/api_fields.py +3 -3
  31. udata/core/organization/apiv2.py +3 -4
  32. udata/core/organization/mails.py +1 -1
  33. udata/core/organization/models.py +40 -7
  34. udata/core/organization/notifications.py +84 -0
  35. udata/core/organization/permissions.py +1 -1
  36. udata/core/organization/tasks.py +3 -0
  37. udata/core/pages/models.py +49 -0
  38. udata/core/pages/tests/test_api.py +165 -1
  39. udata/core/post/api.py +25 -70
  40. udata/core/post/constants.py +8 -0
  41. udata/core/post/models.py +109 -17
  42. udata/core/post/tests/test_api.py +140 -3
  43. udata/core/post/tests/test_models.py +24 -0
  44. udata/core/reports/api.py +18 -0
  45. udata/core/reports/models.py +42 -2
  46. udata/core/reuse/api.py +8 -0
  47. udata/core/reuse/apiv2.py +3 -6
  48. udata/core/reuse/models.py +1 -1
  49. udata/core/spatial/forms.py +2 -2
  50. udata/core/topic/models.py +8 -2
  51. udata/core/user/api.py +10 -3
  52. udata/core/user/api_fields.py +3 -3
  53. udata/core/user/models.py +33 -8
  54. udata/features/notifications/api.py +7 -18
  55. udata/features/notifications/models.py +59 -0
  56. udata/features/notifications/tasks.py +25 -0
  57. udata/features/transfer/actions.py +2 -0
  58. udata/features/transfer/models.py +17 -0
  59. udata/features/transfer/notifications.py +96 -0
  60. udata/flask_mongoengine/engine.py +0 -4
  61. udata/flask_mongoengine/pagination.py +1 -1
  62. udata/frontend/markdown.py +2 -1
  63. udata/harvest/actions.py +20 -0
  64. udata/harvest/api.py +24 -7
  65. udata/harvest/backends/base.py +27 -1
  66. udata/harvest/backends/ckan/harvesters.py +21 -4
  67. udata/harvest/backends/dcat.py +4 -1
  68. udata/harvest/commands.py +33 -0
  69. udata/harvest/filters.py +17 -6
  70. udata/harvest/models.py +16 -0
  71. udata/harvest/permissions.py +27 -0
  72. udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
  73. udata/harvest/tests/test_actions.py +46 -2
  74. udata/harvest/tests/test_api.py +161 -6
  75. udata/harvest/tests/test_base_backend.py +86 -1
  76. udata/harvest/tests/test_dcat_backend.py +68 -3
  77. udata/harvest/tests/test_filters.py +6 -0
  78. udata/i18n.py +1 -4
  79. udata/mail.py +14 -0
  80. udata/migrations/2021-08-17-harvest-integrity.py +23 -16
  81. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  82. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  83. udata/migrations/2025-12-16-create-transfer-request-notifications.py +69 -0
  84. udata/migrations/2026-01-14-add-default-kind-to-posts.py +17 -0
  85. udata/mongo/slug_fields.py +1 -1
  86. udata/rdf.py +65 -11
  87. udata/routing.py +2 -2
  88. udata/settings.py +11 -0
  89. udata/tasks.py +2 -0
  90. udata/templates/mail/message.html +3 -1
  91. udata/tests/api/__init__.py +7 -17
  92. udata/tests/api/test_activities_api.py +36 -0
  93. udata/tests/api/test_datasets_api.py +69 -0
  94. udata/tests/api/test_organizations_api.py +0 -3
  95. udata/tests/api/test_reports_api.py +157 -0
  96. udata/tests/api/test_user_api.py +1 -1
  97. udata/tests/apiv2/test_dataservices.py +14 -0
  98. udata/tests/apiv2/test_organizations.py +9 -0
  99. udata/tests/apiv2/test_reuses.py +11 -0
  100. udata/tests/cli/test_cli_base.py +0 -1
  101. udata/tests/dataservice/test_dataservice_tasks.py +29 -0
  102. udata/tests/dataset/test_dataset_model.py +13 -1
  103. udata/tests/dataset/test_dataset_rdf.py +164 -5
  104. udata/tests/dataset/test_dataset_tasks.py +25 -0
  105. udata/tests/frontend/test_auth.py +58 -1
  106. udata/tests/frontend/test_csv.py +0 -3
  107. udata/tests/helpers.py +31 -27
  108. udata/tests/organization/test_notifications.py +67 -2
  109. udata/tests/search/test_search_integration.py +70 -0
  110. udata/tests/site/test_site_csv_exports.py +22 -10
  111. udata/tests/test_activity.py +9 -9
  112. udata/tests/test_api_fields.py +10 -0
  113. udata/tests/test_discussions.py +5 -5
  114. udata/tests/test_legal_mails.py +359 -0
  115. udata/tests/test_notifications.py +15 -57
  116. udata/tests/test_notifications_task.py +43 -0
  117. udata/tests/test_owned.py +81 -1
  118. udata/tests/test_transfer.py +181 -2
  119. udata/tests/test_uris.py +33 -0
  120. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  121. udata/translations/ar/LC_MESSAGES/udata.po +309 -158
  122. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  123. udata/translations/de/LC_MESSAGES/udata.po +313 -160
  124. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  125. udata/translations/es/LC_MESSAGES/udata.po +312 -160
  126. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  127. udata/translations/fr/LC_MESSAGES/udata.po +475 -202
  128. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  129. udata/translations/it/LC_MESSAGES/udata.po +317 -162
  130. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  131. udata/translations/pt/LC_MESSAGES/udata.po +315 -161
  132. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  133. udata/translations/sr/LC_MESSAGES/udata.po +323 -164
  134. udata/translations/udata.pot +169 -124
  135. udata/uris.py +0 -2
  136. udata/utils.py +23 -0
  137. udata-14.7.3.dev4.dist-info/METADATA +109 -0
  138. {udata-14.0.3.dev1.dist-info → udata-14.7.3.dev4.dist-info}/RECORD +142 -135
  139. udata/core/post/forms.py +0 -30
  140. udata/flask_mongoengine/json.py +0 -38
  141. udata/templates/mail/base.html +0 -105
  142. udata/templates/mail/base.txt +0 -6
  143. udata/templates/mail/button.html +0 -3
  144. udata/templates/mail/layouts/1-column.html +0 -19
  145. udata/templates/mail/layouts/2-columns.html +0 -20
  146. udata/templates/mail/layouts/center-panel.html +0 -16
  147. udata-14.0.3.dev1.dist-info/METADATA +0 -132
  148. {udata-14.0.3.dev1.dist-info → udata-14.7.3.dev4.dist-info}/WHEEL +0 -0
  149. {udata-14.0.3.dev1.dist-info → udata-14.7.3.dev4.dist-info}/entry_points.txt +0 -0
  150. {udata-14.0.3.dev1.dist-info → udata-14.7.3.dev4.dist-info}/licenses/LICENSE +0 -0
  151. {udata-14.0.3.dev1.dist-info → udata-14.7.3.dev4.dist-info}/top_level.txt +0 -0
@@ -10,11 +10,13 @@ from udata.core.user.factories import UserFactory
10
10
  from udata.core.user.metrics import (
11
11
  update_owner_metrics, # noqa needed to register signals
12
12
  )
13
- from udata.features.transfer.actions import accept_transfer, request_transfer
13
+ from udata.features.notifications.models import Notification
14
+ from udata.features.transfer.actions import accept_transfer, refuse_transfer, request_transfer
14
15
  from udata.features.transfer.factories import TransferFactory
15
16
  from udata.features.transfer.notifications import transfer_request_notifications
16
17
  from udata.models import Member
17
- from udata.tests.api import PytestOnlyDBTestCase
18
+ from udata.tests.api import DBTestCase, PytestOnlyDBTestCase
19
+ from udata.tests.helpers import assert_equal_dates
18
20
  from udata.utils import faker
19
21
 
20
22
 
@@ -218,3 +220,180 @@ class TransferNotificationsTest(PytestOnlyDBTestCase):
218
220
  transfer = transfers[details["id"]]
219
221
  assert details["subject"]["class"] == "dataset"
220
222
  assert details["subject"]["id"] == transfer.subject.id
223
+
224
+
225
+ class TransferRequestNotificationTest(DBTestCase):
226
+ def test_notification_created_for_user_recipient(self):
227
+ """Notification is created for user recipient when transfer is requested"""
228
+ owner = UserFactory()
229
+ recipient = UserFactory()
230
+ dataset = DatasetFactory(owner=owner)
231
+
232
+ login_user(owner)
233
+ transfer = request_transfer(dataset, recipient, faker.sentence())
234
+
235
+ notifications = Notification.objects.all()
236
+ assert len(notifications) == 1
237
+
238
+ notification = notifications[0]
239
+ assert notification.user == recipient
240
+ assert notification.details.transfer_owner == owner
241
+ assert notification.details.transfer_recipient == recipient
242
+ assert notification.details.transfer_subject == dataset
243
+ assert_equal_dates(notification.created_at, transfer.created)
244
+
245
+ def test_notification_created_for_org_admins_only(self):
246
+ """Notifications are created for all admin users of recipient org, not editors"""
247
+ owner = UserFactory()
248
+ admin1 = UserFactory()
249
+ admin2 = UserFactory()
250
+ editor = UserFactory()
251
+ members = [
252
+ Member(user=editor, role="editor"),
253
+ Member(user=admin1, role="admin"),
254
+ Member(user=admin2, role="admin"),
255
+ ]
256
+ org = OrganizationFactory(members=members)
257
+ dataset = DatasetFactory(owner=owner)
258
+
259
+ login_user(owner)
260
+ transfer = request_transfer(dataset, org, faker.sentence())
261
+
262
+ notifications = Notification.objects.all()
263
+ assert len(notifications) == 2
264
+
265
+ admin_users = [notif.user for notif in notifications]
266
+ self.assertIn(admin1, admin_users)
267
+ self.assertIn(admin2, admin_users)
268
+
269
+ for notification in notifications:
270
+ assert notification.details.transfer_owner == owner
271
+ assert notification.details.transfer_recipient == org
272
+ assert notification.details.transfer_subject == dataset
273
+ assert_equal_dates(notification.created_at, transfer.created)
274
+
275
+ def test_no_duplicate_notifications(self):
276
+ """Duplicate notifications are not created for same transfer"""
277
+ owner = UserFactory()
278
+ recipient = UserFactory()
279
+ dataset = DatasetFactory(owner=owner)
280
+
281
+ login_user(owner)
282
+ request_transfer(dataset, recipient, faker.sentence())
283
+ request_transfer(dataset, recipient, faker.sentence())
284
+
285
+ assert Notification.objects.count() == 1
286
+
287
+ def test_multiple_transfers_create_separate_notifications(self):
288
+ """Multiple transfer requests create separate notifications"""
289
+ owner = UserFactory()
290
+ recipient = UserFactory()
291
+ dataset1 = DatasetFactory(owner=owner)
292
+ dataset2 = DatasetFactory(owner=owner)
293
+
294
+ login_user(owner)
295
+ request_transfer(dataset1, recipient, faker.sentence())
296
+ request_transfer(dataset2, recipient, faker.sentence())
297
+
298
+ notifications = Notification.objects.all()
299
+ assert len(notifications) == 2
300
+
301
+ subjects = [notif.details.transfer_subject for notif in notifications]
302
+ self.assertIn(dataset1, subjects)
303
+ self.assertIn(dataset2, subjects)
304
+
305
+ def test_notification_not_created_if_previous_exists(self):
306
+ """Notification is created when transferring from org to user"""
307
+ admin = UserFactory()
308
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
309
+ dataset = DatasetFactory(organization=org)
310
+ recipient = UserFactory()
311
+
312
+ login_user(admin)
313
+ request_transfer(dataset, recipient, faker.sentence())
314
+
315
+ notifications = Notification.objects.all()
316
+ assert len(notifications) == 1
317
+
318
+ request_transfer(dataset, recipient, faker.sentence())
319
+
320
+ notifications = Notification.objects.all()
321
+ assert len(notifications) == 1
322
+
323
+ def test_notification_created_if_previous_handled(self):
324
+ """Notification is created when transferring from org to user"""
325
+ admin = UserFactory()
326
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
327
+ dataset = DatasetFactory(organization=org)
328
+ recipient = UserFactory()
329
+
330
+ login_user(admin)
331
+ transfer = request_transfer(dataset, recipient, faker.sentence())
332
+
333
+ login_user(recipient)
334
+ refuse_transfer(transfer)
335
+
336
+ notifications = Notification.objects.all()
337
+ assert len(notifications) == 1
338
+
339
+ login_user(admin)
340
+ request_transfer(dataset, recipient, faker.sentence())
341
+
342
+ notifications = Notification.objects.all()
343
+ assert len(notifications) == 2
344
+
345
+ def test_notification_created_for_org_to_user_transfer(self):
346
+ """Notification is created when transferring from org to user"""
347
+ admin = UserFactory()
348
+ org = OrganizationFactory(members=[Member(user=admin, role="admin")])
349
+ dataset = DatasetFactory(organization=org)
350
+ recipient = UserFactory()
351
+
352
+ login_user(admin)
353
+ transfer = request_transfer(dataset, recipient, faker.sentence())
354
+
355
+ notifications = Notification.objects.all()
356
+ assert len(notifications) == 1
357
+
358
+ notification = notifications[0]
359
+ assert notification.user == recipient
360
+ assert notification.details.transfer_owner == org
361
+ assert notification.details.transfer_recipient == recipient
362
+ assert notification.details.transfer_subject == dataset
363
+ assert_equal_dates(notification.created_at, transfer.created)
364
+
365
+ def test_notification_handled_when_transfer_accepted(self):
366
+ """Notification's handled_at is updated when transfer is accepted"""
367
+ owner = UserFactory()
368
+ recipient = UserFactory()
369
+ dataset = DatasetFactory(owner=owner)
370
+
371
+ login_user(owner)
372
+ # First create the notification by requesting the transfer
373
+ transfer = request_transfer(dataset, recipient, faker.sentence())
374
+ # Then accept the transfer
375
+ login_user(recipient)
376
+ accept_transfer(transfer)
377
+
378
+ # Check that the notification has been handled
379
+ notifications = Notification.objects.all()
380
+ assert len(notifications) == 1
381
+ assert notifications[0].handled_at is not None
382
+
383
+ def test_notification_handled_when_transfer_refused(self):
384
+ """Notification's handled_at is updated when transfer is refused"""
385
+ owner = UserFactory()
386
+ recipient = UserFactory()
387
+ dataset = DatasetFactory(owner=owner)
388
+
389
+ login_user(owner)
390
+ # First create the notification by requesting the transfer
391
+ transfer = request_transfer(dataset, recipient, faker.sentence())
392
+ # Then refuse the transfer
393
+ login_user(recipient)
394
+ refuse_transfer(transfer)
395
+
396
+ # Check that the notification has been handled
397
+ notifications = Notification.objects.all()
398
+ assert len(notifications) == 1
399
+ assert notifications[0].handled_at is not None
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
+ )
Binary file