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