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
@@ -1,3 +1,5 @@
1
+ from datetime import datetime
2
+
1
3
  from flask import url_for
2
4
 
3
5
  from udata.core.dataset.factories import DatasetFactory
@@ -148,6 +150,28 @@ class ReportsAPITest(APITestCase):
148
150
 
149
151
  self.assertEqual(payload["data"][1]["subject"]["id"], str(spam_reuse.id))
150
152
 
153
+ def test_reports_api_list_sort_by_reported_at(self):
154
+ user = UserFactory()
155
+
156
+ dataset1 = DatasetFactory.create(owner=user)
157
+ dataset2 = DatasetFactory.create(owner=user)
158
+ dataset3 = DatasetFactory.create(owner=user)
159
+
160
+ # Create reports with different reported_at times
161
+ report1 = Report(subject=dataset1, reason="spam", reported_at=datetime(2024, 1, 1)).save()
162
+ report2 = Report(subject=dataset2, reason="spam", reported_at=datetime(2024, 1, 3)).save()
163
+ report3 = Report(subject=dataset3, reason="spam", reported_at=datetime(2024, 1, 2)).save()
164
+
165
+ self.login(AdminFactory())
166
+
167
+ # Sort by -reported_at (most recent first)
168
+ response = self.get(url_for("api.reports", sort="-reported_at"))
169
+ self.assert200(response)
170
+ payload = response.json
171
+ self.assertEqual(payload["data"][0]["id"], str(report2.id))
172
+ self.assertEqual(payload["data"][1]["id"], str(report3.id))
173
+ self.assertEqual(payload["data"][2]["id"], str(report1.id))
174
+
151
175
  def test_reports_api_get(self):
152
176
  user = UserFactory()
153
177
 
@@ -165,3 +189,136 @@ class ReportsAPITest(APITestCase):
165
189
 
166
190
  payload = response.json
167
191
  self.assertEqual(payload["subject"]["id"], str(spam_dataset.id))
192
+
193
+ def test_reports_api_dismiss(self):
194
+ user = UserFactory()
195
+ admin = AdminFactory()
196
+
197
+ spam_dataset = DatasetFactory.create(owner=user)
198
+ report = Report(subject=spam_dataset, reason="spam").save()
199
+
200
+ dismiss_time = datetime.utcnow().isoformat()
201
+
202
+ # Should require admin
203
+ response = self.patch(url_for("api.report", report=report), {"dismissed_at": dismiss_time})
204
+ self.assert401(response)
205
+
206
+ self.login(user)
207
+ response = self.patch(url_for("api.report", report=report), {"dismissed_at": dismiss_time})
208
+ self.assert403(response)
209
+
210
+ self.login(admin)
211
+ response = self.patch(url_for("api.report", report=report), {"dismissed_at": dismiss_time})
212
+ self.assert200(response)
213
+
214
+ payload = response.json
215
+ self.assertIsNotNone(payload["dismissed_at"])
216
+ self.assertEqual(payload["dismissed_by"]["id"], str(admin.id))
217
+
218
+ report.reload()
219
+ self.assertIsNotNone(report.dismissed_at)
220
+ self.assertEqual(report.dismissed_by.id, admin.id)
221
+
222
+ def test_reports_api_undismiss(self):
223
+ user = UserFactory()
224
+ admin = AdminFactory()
225
+
226
+ spam_dataset = DatasetFactory.create(owner=user)
227
+ report = Report(
228
+ subject=spam_dataset,
229
+ reason="spam",
230
+ dismissed_at=datetime.utcnow(),
231
+ dismissed_by=admin,
232
+ ).save()
233
+
234
+ # Verify report is dismissed
235
+ self.assertIsNotNone(report.dismissed_at)
236
+ self.assertIsNotNone(report.dismissed_by)
237
+
238
+ # Should require admin
239
+ response = self.patch(url_for("api.report", report=report), {"dismissed_at": None})
240
+ self.assert401(response)
241
+
242
+ self.login(user)
243
+ response = self.patch(url_for("api.report", report=report), {"dismissed_at": None})
244
+ self.assert403(response)
245
+
246
+ self.login(admin)
247
+ response = self.patch(url_for("api.report", report=report), {"dismissed_at": None})
248
+ self.assert200(response)
249
+
250
+ payload = response.json
251
+ self.assertIsNone(payload["dismissed_at"])
252
+ self.assertIsNone(payload["dismissed_by"])
253
+
254
+ report.reload()
255
+ self.assertIsNone(report.dismissed_at)
256
+ self.assertIsNone(report.dismissed_by)
257
+
258
+ def test_reports_api_filter_by_handled(self):
259
+ user = UserFactory()
260
+ admin = AdminFactory()
261
+
262
+ dataset1 = DatasetFactory.create(owner=user)
263
+ dataset2 = DatasetFactory.create(owner=user)
264
+
265
+ # Unhandled report (not dismissed)
266
+ ongoing_report = Report(subject=dataset1, reason="spam").save()
267
+
268
+ # Handled report (dismissed)
269
+ dismissed_report = Report(
270
+ subject=dataset2, reason="spam", dismissed_at=datetime.utcnow(), dismissed_by=admin
271
+ ).save()
272
+
273
+ self.login(admin)
274
+
275
+ # Filter by unhandled
276
+ response = self.get(url_for("api.reports", handled="false"))
277
+ self.assert200(response)
278
+ payload = response.json
279
+ self.assertEqual(payload["total"], 1)
280
+ self.assertEqual(payload["data"][0]["id"], str(ongoing_report.id))
281
+
282
+ # Filter by handled
283
+ response = self.get(url_for("api.reports", handled="true"))
284
+ self.assert200(response)
285
+ payload = response.json
286
+ self.assertEqual(payload["total"], 1)
287
+ self.assertEqual(payload["data"][0]["id"], str(dismissed_report.id))
288
+
289
+ # No filter (all reports)
290
+ response = self.get(url_for("api.reports"))
291
+ self.assert200(response)
292
+ payload = response.json
293
+ self.assertEqual(payload["total"], 2)
294
+
295
+ def test_reports_api_filter_handled_with_deleted_subject(self):
296
+ """Reports with deleted subjects should appear when handled="true", not handled="false"."""
297
+ user = UserFactory()
298
+ admin = AdminFactory()
299
+
300
+ dataset1 = DatasetFactory.create(owner=user)
301
+ dataset2 = DatasetFactory.create(owner=user)
302
+
303
+ # Unhandled report (not dismissed, subject exists)
304
+ ongoing_report = Report(subject=dataset1, reason="spam").save()
305
+
306
+ # Report with deleted subject (should appear in "handled", not "unhandled")
307
+ deleted_subject_report = Report(subject=dataset2, reason="spam").save()
308
+ dataset2.delete()
309
+
310
+ self.login(admin)
311
+
312
+ # Filter by unhandled - should only return the report with existing subject
313
+ response = self.get(url_for("api.reports", handled="false"))
314
+ self.assert200(response)
315
+ payload = response.json
316
+ self.assertEqual(payload["total"], 1)
317
+ self.assertEqual(payload["data"][0]["id"], str(ongoing_report.id))
318
+
319
+ # Filter by handled - should return the report with deleted subject
320
+ response = self.get(url_for("api.reports", handled="true"))
321
+ self.assert200(response)
322
+ payload = response.json
323
+ self.assertEqual(payload["total"], 1)
324
+ self.assertEqual(payload["data"][0]["id"], str(deleted_subject_report.id))