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
@@ -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
@@ -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