udata 14.0.0__py3-none-any.whl → 14.5.1.dev6__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 (152) hide show
  1. udata/api/__init__.py +2 -0
  2. udata/api_fields.py +35 -4
  3. udata/app.py +18 -20
  4. udata/auth/__init__.py +29 -6
  5. udata/auth/forms.py +2 -2
  6. udata/auth/views.py +13 -6
  7. udata/commands/dcat.py +1 -1
  8. udata/commands/serve.py +3 -11
  9. udata/commands/tests/test_fixtures.py +9 -9
  10. udata/core/access_type/api.py +1 -1
  11. udata/core/access_type/constants.py +12 -8
  12. udata/core/activity/api.py +5 -6
  13. udata/core/badges/tests/test_commands.py +6 -6
  14. udata/core/csv.py +5 -0
  15. udata/core/dataservices/api.py +8 -1
  16. udata/core/dataservices/apiv2.py +2 -5
  17. udata/core/dataservices/models.py +5 -2
  18. udata/core/dataservices/rdf.py +2 -1
  19. udata/core/dataservices/tasks.py +13 -2
  20. udata/core/dataset/api.py +10 -0
  21. udata/core/dataset/models.py +6 -6
  22. udata/core/dataset/permissions.py +31 -0
  23. udata/core/dataset/rdf.py +8 -2
  24. udata/core/dataset/tasks.py +23 -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/apiv2.py +2 -3
  31. udata/core/organization/mails.py +1 -1
  32. udata/core/organization/models.py +15 -2
  33. udata/core/organization/notifications.py +84 -0
  34. udata/core/organization/permissions.py +1 -1
  35. udata/core/organization/tasks.py +3 -0
  36. udata/core/pages/tests/test_api.py +32 -0
  37. udata/core/post/api.py +24 -69
  38. udata/core/post/models.py +84 -16
  39. udata/core/post/tests/test_api.py +24 -1
  40. udata/core/reports/api.py +18 -0
  41. udata/core/reports/models.py +42 -2
  42. udata/core/reuse/api.py +8 -0
  43. udata/core/reuse/apiv2.py +2 -5
  44. udata/core/reuse/models.py +1 -1
  45. udata/core/reuse/tasks.py +7 -0
  46. udata/core/spatial/forms.py +2 -2
  47. udata/core/topic/models.py +8 -2
  48. udata/core/user/api.py +10 -3
  49. udata/core/user/models.py +12 -2
  50. udata/features/notifications/api.py +7 -18
  51. udata/features/notifications/models.py +56 -0
  52. udata/features/notifications/tasks.py +25 -0
  53. udata/flask_mongoengine/engine.py +0 -4
  54. udata/flask_mongoengine/pagination.py +1 -1
  55. udata/frontend/markdown.py +2 -1
  56. udata/harvest/actions.py +21 -1
  57. udata/harvest/api.py +25 -8
  58. udata/harvest/backends/base.py +27 -1
  59. udata/harvest/backends/ckan/harvesters.py +11 -2
  60. udata/harvest/backends/dcat.py +4 -1
  61. udata/harvest/commands.py +33 -0
  62. udata/harvest/filters.py +17 -6
  63. udata/harvest/models.py +16 -0
  64. udata/harvest/permissions.py +27 -0
  65. udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
  66. udata/harvest/tests/test_actions.py +58 -5
  67. udata/harvest/tests/test_api.py +276 -122
  68. udata/harvest/tests/test_base_backend.py +86 -1
  69. udata/harvest/tests/test_dcat_backend.py +81 -10
  70. udata/harvest/tests/test_filters.py +6 -0
  71. udata/i18n.py +1 -4
  72. udata/mail.py +19 -1
  73. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  74. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  75. udata/mongo/slug_fields.py +1 -1
  76. udata/rdf.py +58 -10
  77. udata/routing.py +2 -2
  78. udata/settings.py +11 -0
  79. udata/tasks.py +1 -0
  80. udata/templates/mail/message.html +5 -31
  81. udata/tests/__init__.py +27 -2
  82. udata/tests/api/__init__.py +108 -21
  83. udata/tests/api/test_activities_api.py +36 -0
  84. udata/tests/api/test_auth_api.py +121 -95
  85. udata/tests/api/test_base_api.py +7 -4
  86. udata/tests/api/test_datasets_api.py +50 -19
  87. udata/tests/api/test_organizations_api.py +192 -197
  88. udata/tests/api/test_reports_api.py +157 -0
  89. udata/tests/api/test_reuses_api.py +147 -147
  90. udata/tests/api/test_security_api.py +12 -12
  91. udata/tests/api/test_swagger.py +4 -4
  92. udata/tests/api/test_tags_api.py +8 -8
  93. udata/tests/api/test_user_api.py +1 -1
  94. udata/tests/apiv2/test_search.py +30 -0
  95. udata/tests/apiv2/test_swagger.py +4 -4
  96. udata/tests/cli/test_cli_base.py +8 -9
  97. udata/tests/dataservice/test_dataservice_tasks.py +29 -0
  98. udata/tests/dataset/test_dataset_commands.py +4 -4
  99. udata/tests/dataset/test_dataset_model.py +66 -26
  100. udata/tests/dataset/test_dataset_rdf.py +99 -5
  101. udata/tests/dataset/test_dataset_tasks.py +25 -0
  102. udata/tests/frontend/test_auth.py +58 -1
  103. udata/tests/frontend/test_csv.py +0 -3
  104. udata/tests/helpers.py +31 -27
  105. udata/tests/organization/test_notifications.py +67 -2
  106. udata/tests/plugin.py +6 -261
  107. udata/tests/search/test_search_integration.py +33 -0
  108. udata/tests/site/test_site_csv_exports.py +22 -10
  109. udata/tests/test_activity.py +9 -9
  110. udata/tests/test_api_fields.py +10 -0
  111. udata/tests/test_dcat_commands.py +2 -2
  112. udata/tests/test_discussions.py +5 -5
  113. udata/tests/test_legal_mails.py +359 -0
  114. udata/tests/test_migrations.py +21 -21
  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_storages.py +25 -19
  119. udata/tests/test_topics.py +77 -61
  120. udata/tests/test_uris.py +33 -0
  121. udata/tests/workers/test_jobs_commands.py +23 -23
  122. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  123. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  124. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  125. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  126. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  127. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  128. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  129. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  130. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  131. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  132. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  133. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  134. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  135. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  136. udata/translations/udata.pot +215 -106
  137. udata/uris.py +0 -2
  138. udata-14.5.1.dev6.dist-info/METADATA +109 -0
  139. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/RECORD +143 -140
  140. udata/core/post/forms.py +0 -30
  141. udata/flask_mongoengine/json.py +0 -38
  142. udata/templates/mail/base.html +0 -105
  143. udata/templates/mail/base.txt +0 -6
  144. udata/templates/mail/button.html +0 -3
  145. udata/templates/mail/layouts/1-column.html +0 -19
  146. udata/templates/mail/layouts/2-columns.html +0 -20
  147. udata/templates/mail/layouts/center-panel.html +0 -16
  148. udata-14.0.0.dist-info/METADATA +0 -132
  149. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/WHEEL +0 -0
  150. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/entry_points.txt +0 -0
  151. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/licenses/LICENSE +0 -0
  152. {udata-14.0.0.dist-info → udata-14.5.1.dev6.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ from flask import url_for
7
7
  from udata.core import csv
8
8
  from udata.core.dataservices.factories import DataserviceFactory
9
9
  from udata.core.dataset import tasks as dataset_tasks
10
+ from udata.core.dataset.constants import SPD
10
11
  from udata.core.dataset.factories import DatasetFactory, ResourceFactory
11
12
  from udata.core.organization.factories import OrganizationFactory
12
13
  from udata.core.reuse.factories import ReuseFactory
@@ -25,7 +26,6 @@ class SiteCsvExportsTest(APITestCase):
25
26
 
26
27
  self.assert200(response)
27
28
  self.assertEqual(response.mimetype, "text/csv")
28
- self.assertEqual(response.charset, "utf-8")
29
29
 
30
30
  csvfile = StringIO(response.data.decode("utf8"))
31
31
  reader = csv.get_reader(csvfile)
@@ -73,7 +73,6 @@ class SiteCsvExportsTest(APITestCase):
73
73
 
74
74
  self.assert200(response)
75
75
  self.assertEqual(response.mimetype, "text/csv")
76
- self.assertEqual(response.charset, "utf-8")
77
76
 
78
77
  csvfile = StringIO(response.data.decode("utf8"))
79
78
  reader = csv.get_reader(csvfile)
@@ -99,6 +98,27 @@ class SiteCsvExportsTest(APITestCase):
99
98
  self.assertNotIn(str(dataset.id), ids)
100
99
  self.assertNotIn(str(hidden_dataset.id), ids)
101
100
 
101
+ def test_datasets_csv_with_badge_filter(self):
102
+ self.app.config["EXPORT_CSV_MODELS"] = []
103
+ dataset_with_badge = DatasetFactory(resources=[ResourceFactory()])
104
+ dataset_with_badge.add_badge(SPD)
105
+ dataset_without_badge = DatasetFactory(resources=[ResourceFactory()])
106
+
107
+ response = self.get(url_for("api.site_datasets_csv", badge=SPD))
108
+
109
+ self.assert200(response)
110
+
111
+ csvfile = StringIO(response.data.decode("utf8"))
112
+ reader = csv.get_reader(csvfile)
113
+ next(reader) # skip header
114
+
115
+ rows = list(reader)
116
+ ids = [row[0] for row in rows]
117
+
118
+ self.assertEqual(len(rows), 1)
119
+ self.assertIn(str(dataset_with_badge.id), ids)
120
+ self.assertNotIn(str(dataset_without_badge.id), ids)
121
+
102
122
  def test_resources_csv(self):
103
123
  self.app.config["EXPORT_CSV_MODELS"] = []
104
124
  datasets = [
@@ -110,7 +130,6 @@ class SiteCsvExportsTest(APITestCase):
110
130
 
111
131
  self.assert200(response)
112
132
  self.assertEqual(response.mimetype, "text/csv")
113
- self.assertEqual(response.charset, "utf-8")
114
133
 
115
134
  csvfile = StringIO(response.data.decode("utf8"))
116
135
  reader = csv.get_reader(csvfile)
@@ -164,7 +183,6 @@ class SiteCsvExportsTest(APITestCase):
164
183
 
165
184
  self.assert200(response)
166
185
  self.assertEqual(response.mimetype, "text/csv")
167
- self.assertEqual(response.charset, "utf-8")
168
186
 
169
187
  csvfile = StringIO(response.data.decode("utf8"))
170
188
  reader = csv.get_reader(csvfile)
@@ -200,7 +218,6 @@ class SiteCsvExportsTest(APITestCase):
200
218
 
201
219
  self.assert200(response)
202
220
  self.assertEqual(response.mimetype, "text/csv")
203
- self.assertEqual(response.charset, "utf-8")
204
221
 
205
222
  csvfile = StringIO(response.data.decode("utf8"))
206
223
  reader = csv.get_reader(csvfile)
@@ -245,7 +262,6 @@ class SiteCsvExportsTest(APITestCase):
245
262
 
246
263
  self.assert200(response)
247
264
  self.assertEqual(response.mimetype, "text/csv")
248
- self.assertEqual(response.charset, "utf-8")
249
265
 
250
266
  csvfile = StringIO(response.data.decode("utf8"))
251
267
  reader = csv.get_reader(csvfile)
@@ -293,7 +309,6 @@ class SiteCsvExportsTest(APITestCase):
293
309
 
294
310
  self.assert200(response)
295
311
  self.assertEqual(response.mimetype, "text/csv")
296
- self.assertEqual(response.charset, "utf-8")
297
312
 
298
313
  csvfile = StringIO(response.data.decode("utf8"))
299
314
  reader = csv.get_reader(csvfile)
@@ -332,7 +347,6 @@ class SiteCsvExportsTest(APITestCase):
332
347
 
333
348
  self.assert200(response)
334
349
  self.assertEqual(response.mimetype, "text/csv")
335
- self.assertEqual(response.charset, "utf-8")
336
350
 
337
351
  csvfile = StringIO(response.data.decode("utf8"))
338
352
  reader = csv.get_reader(csvfile)
@@ -379,7 +393,6 @@ class SiteCsvExportsTest(APITestCase):
379
393
 
380
394
  self.assert200(response)
381
395
  self.assertEqual(response.mimetype, "text/csv")
382
- self.assertEqual(response.charset, "utf-8")
383
396
 
384
397
  csvfile = StringIO(response.data.decode("utf8"))
385
398
  reader = csv.get_reader(csvfile)
@@ -424,7 +437,6 @@ class SiteCsvExportsTest(APITestCase):
424
437
 
425
438
  self.assert200(response)
426
439
  self.assertEqual(response.mimetype, "text/csv")
427
- self.assertEqual(response.charset, "utf-8")
428
440
 
429
441
  csvfile = StringIO(response.data.decode("utf8"))
430
442
  reader = csv.get_reader(csvfile)
@@ -211,9 +211,9 @@ class AuditableTest(APITestCase):
211
211
  not_auditable="original",
212
212
  )
213
213
 
214
- def check_signal_update(args):
214
+ def check_signal_update(kwargs):
215
215
  self.assertEqual(
216
- args[1]["changed_fields"],
216
+ kwargs["changed_fields"],
217
217
  [
218
218
  "name",
219
219
  "tags",
@@ -224,13 +224,13 @@ class AuditableTest(APITestCase):
224
224
  "embedded_list.1.name",
225
225
  ],
226
226
  )
227
- self.assertEqual(args[1]["previous"]["name"], "fake")
228
- self.assertEqual(args[1]["previous"]["tags"], ["some", "tags"])
229
- self.assertEqual(args[1]["previous"]["some_date"], date(2020, 1, 1))
230
- self.assertEqual(args[1]["previous"]["daterange_embedded.start"], date(2020, 1, 1))
231
- self.assertEqual(args[1]["previous"]["daterange_embedded.end"], date(2020, 12, 31))
232
- self.assertEqual(args[1]["previous"]["some_list"], ["some", "list"])
233
- self.assertEqual(args[1]["previous"]["embedded_list.1.name"], "fake_embedded_1")
227
+ self.assertEqual(kwargs["previous"]["name"], "fake")
228
+ self.assertEqual(kwargs["previous"]["tags"], ["some", "tags"])
229
+ self.assertEqual(kwargs["previous"]["some_date"], date(2020, 1, 1))
230
+ self.assertEqual(kwargs["previous"]["daterange_embedded.start"], date(2020, 1, 1))
231
+ self.assertEqual(kwargs["previous"]["daterange_embedded.end"], date(2020, 12, 31))
232
+ self.assertEqual(kwargs["previous"]["some_list"], ["some", "list"])
233
+ self.assertEqual(kwargs["previous"]["embedded_list.1.name"], "fake_embedded_1")
234
234
 
235
235
  with assert_emit(FakeAuditableSubject.on_update, assertions_callback=check_signal_update):
236
236
  fake.name = "different"
@@ -354,3 +354,13 @@ class ApplyPaginationTest(PytestOnlyDBTestCase):
354
354
  results: DBPaginator = Fake.apply_pagination(Fake.apply_sort_filters(Fake.objects))
355
355
  assert results.page_size == 5
356
356
  assert results.page == 3
357
+
358
+ def test_negative_page_size_returns_404(self, app) -> None:
359
+ """Negative page_size should return a 404 error."""
360
+ from werkzeug.exceptions import NotFound
361
+
362
+ FakeFactory()
363
+
364
+ with app.test_request_context("/foobar", query_string={"page": 1, "page_size": -5}):
365
+ with pytest.raises(NotFound):
366
+ Fake.apply_pagination(Fake.apply_sort_filters(Fake.objects))
@@ -2,7 +2,7 @@ from udata.tests.api import PytestOnlyDBTestCase
2
2
 
3
3
 
4
4
  class ParseUrlCommandTest(PytestOnlyDBTestCase):
5
- def test_parse_url(self, cli, requests_mock, caplog) -> None:
5
+ def test_parse_url(self, requests_mock, caplog) -> None:
6
6
  logs = []
7
7
 
8
8
  def mock_echo(message: str) -> None:
@@ -15,7 +15,7 @@ class ParseUrlCommandTest(PytestOnlyDBTestCase):
15
15
  requests_mock.get(mock_url, text=test_rdf_file.read())
16
16
  requests_mock.head(mock_url, text="sig.oreme.rdf")
17
17
  dataset_id = "0437a976-cff1-4fa6-807a-c23006df2f8f"
18
- result = cli(
18
+ result = self.cli(
19
19
  "dcat",
20
20
  "parse-url",
21
21
  mock_url,
@@ -142,11 +142,11 @@ class DiscussionsTest(APITestCase):
142
142
  with assert_not_emit(on_new_discussion):
143
143
  discussion_id = None
144
144
 
145
- def check_signal(args):
145
+ def check_signal(kwargs):
146
146
  self.assertIsNotNone(discussion_id)
147
147
  self.assertIn(
148
- f"https://data.gouv.fr/datasets/{dataset.slug}/discussions/?discussion_id={discussion_id}",
149
- args[1]["message"],
148
+ f"https://data.gouv.fr/datasets/{dataset.slug}/discussions?discussion_id={discussion_id}",
149
+ kwargs["message"],
150
150
  )
151
151
 
152
152
  with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
@@ -620,8 +620,8 @@ class DiscussionsTest(APITestCase):
620
620
  self.login()
621
621
  with assert_not_emit(on_new_discussion_comment):
622
622
 
623
- def check_signal(args):
624
- self.assertIn(discussion.url_for(), args[1]["message"])
623
+ def check_signal(kwargs):
624
+ self.assertIn(discussion.url_for(), kwargs["message"])
625
625
 
626
626
  with assert_emit(on_new_potential_spam, assertions_callback=check_signal):
627
627
  response = self.post(
@@ -0,0 +1,359 @@
1
+ import pytest
2
+ from flask import url_for
3
+
4
+ from udata.core.dataservices.factories import DataserviceFactory
5
+ from udata.core.dataset.factories import DatasetFactory
6
+ from udata.core.discussions.factories import DiscussionFactory, MessageDiscussionFactory
7
+ from udata.core.organization.factories import OrganizationFactory
8
+ from udata.core.reuse.factories import ReuseFactory
9
+ from udata.core.user.factories import AdminFactory, UserFactory
10
+ from udata.tests.api import APITestCase
11
+ from udata.tests.helpers import capture_mails
12
+
13
+
14
+ class AdminMailsOnDeleteTest(APITestCase):
15
+ """Test admin mails are sent on delete when send_legal_notice=True and user is sysadmin"""
16
+
17
+ modules = []
18
+
19
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
20
+ def test_dataset_delete_with_mail_as_admin(self):
21
+ """Admin deleting dataset with send_legal_notice=True should send email to owner"""
22
+ self.login(AdminFactory())
23
+ owner = UserFactory()
24
+ dataset = DatasetFactory(owner=owner)
25
+
26
+ with capture_mails() as mails:
27
+ response = self.delete(
28
+ url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true"
29
+ )
30
+
31
+ self.assertStatus(response, 204)
32
+ assert len(mails) == 1
33
+ assert mails[0].recipients[0] == owner.email
34
+ assert "deletion" in mails[0].subject.lower()
35
+
36
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
37
+ def test_dataset_delete_without_mail_as_admin(self):
38
+ """Admin deleting dataset without send_mail should not send email"""
39
+ self.login(AdminFactory())
40
+ owner = UserFactory()
41
+ dataset = DatasetFactory(owner=owner)
42
+
43
+ with capture_mails() as mails:
44
+ response = self.delete(url_for("api.dataset", dataset=dataset))
45
+
46
+ self.assertStatus(response, 204)
47
+ assert len(mails) == 0
48
+
49
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
50
+ def test_dataset_delete_with_mail_as_non_admin(self):
51
+ """Non-admin deleting their dataset with send_legal_notice=True should not send email"""
52
+ owner = self.login()
53
+ dataset = DatasetFactory(owner=owner)
54
+
55
+ with capture_mails() as mails:
56
+ response = self.delete(
57
+ url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true"
58
+ )
59
+
60
+ self.assertStatus(response, 204)
61
+ assert len(mails) == 0
62
+
63
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
64
+ def test_dataset_delete_with_org_owner_sends_to_admins(self):
65
+ """Deleting org-owned dataset should send email to org admins"""
66
+ self.login(AdminFactory())
67
+ org_admin = UserFactory()
68
+ org = OrganizationFactory(members=[{"user": org_admin, "role": "admin"}])
69
+ dataset = DatasetFactory(organization=org)
70
+
71
+ with capture_mails() as mails:
72
+ response = self.delete(
73
+ url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true"
74
+ )
75
+
76
+ self.assertStatus(response, 204)
77
+ assert len(mails) == 1
78
+ assert mails[0].recipients[0] == org_admin.email
79
+
80
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
81
+ def test_reuse_delete_with_mail_as_admin(self):
82
+ """Admin deleting reuse with send_legal_notice=True should send email to owner"""
83
+ self.login(AdminFactory())
84
+ owner = UserFactory()
85
+ reuse = ReuseFactory(owner=owner)
86
+
87
+ with capture_mails() as mails:
88
+ response = self.delete(url_for("api.reuse", reuse=reuse) + "?send_legal_notice=true")
89
+
90
+ self.assertStatus(response, 204)
91
+ assert len(mails) == 1
92
+ assert mails[0].recipients[0] == owner.email
93
+
94
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
95
+ def test_reuse_delete_without_mail_as_admin(self):
96
+ """Admin deleting reuse without send_mail should not send email"""
97
+ self.login(AdminFactory())
98
+ owner = UserFactory()
99
+ reuse = ReuseFactory(owner=owner)
100
+
101
+ with capture_mails() as mails:
102
+ response = self.delete(url_for("api.reuse", reuse=reuse))
103
+
104
+ self.assertStatus(response, 204)
105
+ assert len(mails) == 0
106
+
107
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
108
+ def test_dataservice_delete_with_mail_as_admin(self):
109
+ """Admin deleting dataservice with send_legal_notice=True should send email to owner"""
110
+ self.login(AdminFactory())
111
+ owner = UserFactory()
112
+ dataservice = DataserviceFactory(owner=owner)
113
+
114
+ with capture_mails() as mails:
115
+ response = self.delete(
116
+ url_for("api.dataservice", dataservice=dataservice) + "?send_legal_notice=true"
117
+ )
118
+
119
+ self.assertStatus(response, 204)
120
+ assert len(mails) == 1
121
+ assert mails[0].recipients[0] == owner.email
122
+
123
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
124
+ def test_organization_delete_with_mail_as_admin(self):
125
+ """Admin deleting organization with send_legal_notice=True should send email to org admins"""
126
+ self.login(AdminFactory())
127
+ org_admin = UserFactory()
128
+ org = OrganizationFactory(members=[{"user": org_admin, "role": "admin"}])
129
+
130
+ with capture_mails() as mails:
131
+ response = self.delete(url_for("api.organization", org=org) + "?send_legal_notice=true")
132
+
133
+ self.assertStatus(response, 204)
134
+ assert len(mails) == 1
135
+ assert mails[0].recipients[0] == org_admin.email
136
+
137
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
138
+ def test_user_delete_with_legal_notice_skips_simple_notification(self):
139
+ """Admin deleting user with send_legal_notice=True automatically skips simple notification"""
140
+ self.login(AdminFactory())
141
+ user_to_delete = UserFactory()
142
+
143
+ with capture_mails() as mails:
144
+ response = self.delete(
145
+ url_for("api.user", user=user_to_delete) + "?send_legal_notice=true"
146
+ )
147
+
148
+ self.assertStatus(response, 204)
149
+ # Only legal notice mail, simple notification is automatically skipped
150
+ assert len(mails) == 1
151
+ assert mails[0].recipients[0] == user_to_delete.email
152
+ # Verify it's the legal notice (with appeal info), not the simple notification
153
+ assert "Deletion of your" in mails[0].subject # Legal notice subject
154
+ assert "contest this decision" in mails[0].body # Legal notice contains appeal info
155
+
156
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
157
+ def test_discussion_delete_with_mail_as_admin(self):
158
+ """Admin deleting discussion with send_legal_notice=True should send email to author"""
159
+ self.login(AdminFactory())
160
+ author = UserFactory()
161
+ dataset = DatasetFactory()
162
+ discussion = DiscussionFactory(subject=dataset, user=author)
163
+
164
+ with capture_mails() as mails:
165
+ response = self.delete(
166
+ url_for("api.discussion", id=discussion.id) + "?send_legal_notice=true"
167
+ )
168
+
169
+ self.assertStatus(response, 204)
170
+ assert len(mails) == 1
171
+ assert mails[0].recipients[0] == author.email
172
+
173
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
174
+ def test_message_delete_with_mail_as_admin(self):
175
+ """Admin deleting message with send_legal_notice=True should send email to author"""
176
+ self.login(AdminFactory())
177
+ author = UserFactory()
178
+ message_author = UserFactory()
179
+ dataset = DatasetFactory()
180
+ discussion = DiscussionFactory(
181
+ subject=dataset,
182
+ user=author,
183
+ discussion=[
184
+ MessageDiscussionFactory(posted_by=author),
185
+ MessageDiscussionFactory(posted_by=message_author),
186
+ ],
187
+ )
188
+
189
+ with capture_mails() as mails:
190
+ response = self.delete(
191
+ url_for("api.discussion_comment", id=discussion.id, cidx=1)
192
+ + "?send_legal_notice=true"
193
+ )
194
+
195
+ self.assertStatus(response, 204)
196
+ assert len(mails) == 1
197
+ assert mails[0].recipients[0] == message_author.email
198
+
199
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
200
+ def test_dataset_delete_without_owner_no_mail_sent(self):
201
+ """Deleting dataset without owner or organization should not send email"""
202
+ self.login(AdminFactory())
203
+ dataset = DatasetFactory(owner=None, organization=None)
204
+
205
+ with capture_mails() as mails:
206
+ response = self.delete(
207
+ url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true"
208
+ )
209
+
210
+ self.assertStatus(response, 204)
211
+ assert len(mails) == 0
212
+
213
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
214
+ def test_reuse_delete_without_owner_no_mail_sent(self):
215
+ """Deleting reuse without owner or organization should not send email"""
216
+ self.login(AdminFactory())
217
+ reuse = ReuseFactory(owner=None, organization=None)
218
+
219
+ with capture_mails() as mails:
220
+ response = self.delete(url_for("api.reuse", reuse=reuse) + "?send_legal_notice=true")
221
+
222
+ self.assertStatus(response, 204)
223
+ assert len(mails) == 0
224
+
225
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
226
+ def test_dataservice_delete_without_owner_no_mail_sent(self):
227
+ """Deleting dataservice without owner or organization should not send email"""
228
+ self.login(AdminFactory())
229
+ dataservice = DataserviceFactory(owner=None, organization=None)
230
+
231
+ with capture_mails() as mails:
232
+ response = self.delete(
233
+ url_for("api.dataservice", dataservice=dataservice) + "?send_legal_notice=true"
234
+ )
235
+
236
+ self.assertStatus(response, 204)
237
+ assert len(mails) == 0
238
+
239
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
240
+ def test_organization_delete_without_admins_no_mail_sent(self):
241
+ """Deleting organization without admin members should not send email"""
242
+ self.login(AdminFactory())
243
+ editor = UserFactory()
244
+ org = OrganizationFactory(members=[{"user": editor, "role": "editor"}])
245
+
246
+ with capture_mails() as mails:
247
+ response = self.delete(url_for("api.organization", org=org) + "?send_legal_notice=true")
248
+
249
+ self.assertStatus(response, 204)
250
+ assert len(mails) == 0
251
+
252
+
253
+ class MailContentVariantsTest(APITestCase):
254
+ """Test mail content varies based on settings"""
255
+
256
+ modules = []
257
+
258
+ @pytest.mark.options(
259
+ DEFAULT_LANGUAGE="en",
260
+ TERMS_OF_USE_URL="https://example.com/terms",
261
+ TERMS_OF_USE_DELETION_ARTICLE="5.1.2",
262
+ TELERECOURS_URL="https://telerecours.fr",
263
+ )
264
+ def test_mail_with_all_settings(self):
265
+ """Mail should contain terms of use reference and telerecours when all settings defined"""
266
+ self.login(AdminFactory())
267
+ owner = UserFactory()
268
+ dataset = DatasetFactory(owner=owner)
269
+
270
+ with capture_mails() as mails:
271
+ self.delete(url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true")
272
+
273
+ assert len(mails) == 1
274
+ body = mails[0].body
275
+ assert "Our terms of use specify" in body
276
+ assert "5.1.2" in body
277
+ assert "Télérecours" in body
278
+
279
+ @pytest.mark.options(
280
+ DEFAULT_LANGUAGE="en",
281
+ TERMS_OF_USE_DELETION_ARTICLE=None,
282
+ TELERECOURS_URL=None,
283
+ )
284
+ def test_mail_without_settings(self):
285
+ """Mail should use generic text when settings are not defined"""
286
+ self.login(AdminFactory())
287
+ owner = UserFactory()
288
+ dataset = DatasetFactory(owner=owner)
289
+
290
+ with capture_mails() as mails:
291
+ self.delete(url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true")
292
+
293
+ assert len(mails) == 1
294
+ body = mails[0].body
295
+ assert "Our terms of use specify" not in body
296
+ assert "Télérecours" not in body
297
+ assert "contacting us" in body
298
+
299
+ @pytest.mark.options(
300
+ DEFAULT_LANGUAGE="en",
301
+ TERMS_OF_USE_URL="https://example.com/terms",
302
+ TERMS_OF_USE_DELETION_ARTICLE="3.2",
303
+ TELERECOURS_URL=None,
304
+ )
305
+ def test_mail_with_terms_only(self):
306
+ """Mail should contain terms of use but generic appeal when only terms defined"""
307
+ self.login(AdminFactory())
308
+ owner = UserFactory()
309
+ dataset = DatasetFactory(owner=owner)
310
+
311
+ with capture_mails() as mails:
312
+ self.delete(url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true")
313
+
314
+ assert len(mails) == 1
315
+ body = mails[0].body
316
+ assert "Our terms of use specify" in body
317
+ assert "3.2" in body
318
+ assert "Télérecours" not in body
319
+ assert "contacting us" in body
320
+
321
+ @pytest.mark.options(
322
+ DEFAULT_LANGUAGE="en",
323
+ TERMS_OF_USE_DELETION_ARTICLE=None,
324
+ TELERECOURS_URL="https://telerecours.fr",
325
+ )
326
+ def test_mail_with_telerecours_only(self):
327
+ """Mail should contain telerecours but generic terms when only telerecours defined"""
328
+ self.login(AdminFactory())
329
+ owner = UserFactory()
330
+ dataset = DatasetFactory(owner=owner)
331
+
332
+ with capture_mails() as mails:
333
+ self.delete(url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true")
334
+
335
+ assert len(mails) == 1
336
+ body = mails[0].body
337
+ assert "Our terms of use specify" not in body
338
+ assert "Télérecours" in body
339
+ assert "contacting us" not in body
340
+
341
+ @pytest.mark.options(
342
+ DEFAULT_LANGUAGE="en",
343
+ TERMS_OF_USE_URL=None,
344
+ TERMS_OF_USE_DELETION_ARTICLE="5.1.2",
345
+ TELERECOURS_URL=None,
346
+ )
347
+ def test_mail_with_article_but_no_url(self):
348
+ """Mail should use generic terms when article is defined but URL is missing"""
349
+ self.login(AdminFactory())
350
+ owner = UserFactory()
351
+ dataset = DatasetFactory(owner=owner)
352
+
353
+ with capture_mails() as mails:
354
+ self.delete(url_for("api.dataset", dataset=dataset) + "?send_legal_notice=true")
355
+
356
+ assert len(mails) == 1
357
+ body = mails[0].body
358
+ assert "Our terms of use specify" not in body
359
+ assert "5.1.2" not in body