udata 14.5.1.dev9__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.
- udata/api_fields.py +85 -15
- udata/auth/forms.py +1 -1
- udata/core/badges/tests/test_tasks.py +0 -2
- udata/core/dataservices/apiv2.py +1 -1
- udata/core/dataset/models.py +15 -3
- udata/core/dataset/rdf.py +10 -14
- udata/core/organization/apiv2.py +1 -1
- udata/core/organization/models.py +25 -5
- udata/core/pages/models.py +49 -0
- udata/core/pages/tests/test_api.py +165 -1
- udata/core/post/api.py +1 -1
- udata/core/post/constants.py +8 -0
- udata/core/post/models.py +27 -3
- udata/core/post/tests/test_api.py +116 -2
- udata/core/post/tests/test_models.py +24 -0
- udata/core/reuse/apiv2.py +1 -1
- udata/core/user/models.py +21 -6
- udata/features/notifications/models.py +4 -1
- udata/features/transfer/actions.py +2 -0
- udata/features/transfer/models.py +17 -0
- udata/features/transfer/notifications.py +96 -0
- udata/harvest/backends/ckan/harvesters.py +10 -2
- udata/migrations/2021-08-17-harvest-integrity.py +23 -16
- udata/migrations/2025-12-16-create-transfer-request-notifications.py +69 -0
- udata/migrations/2026-01-14-add-default-kind-to-posts.py +17 -0
- udata/tasks.py +1 -0
- udata/tests/apiv2/test_dataservices.py +14 -0
- udata/tests/apiv2/test_organizations.py +9 -0
- udata/tests/apiv2/test_reuses.py +11 -0
- udata/tests/dataset/test_dataset_rdf.py +49 -0
- udata/tests/search/test_search_integration.py +37 -0
- udata/tests/test_transfer.py +181 -2
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +310 -158
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +314 -160
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +313 -160
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +476 -202
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +318 -162
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +316 -161
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +324 -164
- udata/translations/udata.pot +169 -124
- udata/utils.py +23 -0
- {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/METADATA +2 -2
- {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/RECORD +54 -50
- udata/tests/apiv2/test_search.py +0 -30
- {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/WHEEL +0 -0
- {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/entry_points.txt +0 -0
- {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/licenses/LICENSE +0 -0
- {udata-14.5.1.dev9.dist-info → udata-14.7.3.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from udata.core.dataservices.factories import DataserviceFactory
|
|
2
|
+
from udata.tests.api import APITestCase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DataserviceSearchAPIV2Test(APITestCase):
|
|
6
|
+
def test_dataservice_search_with_model_query_param(self):
|
|
7
|
+
"""Searching dataservices with 'model' as query param should not crash.
|
|
8
|
+
|
|
9
|
+
Regression test for: TypeError: query() got multiple values for argument 'model'
|
|
10
|
+
"""
|
|
11
|
+
DataserviceFactory.create_batch(3)
|
|
12
|
+
|
|
13
|
+
response = self.get("/api/2/dataservices/search/?model=malicious")
|
|
14
|
+
self.assert200(response)
|
|
@@ -4,6 +4,15 @@ from udata.core.organization.factories import Member, OrganizationFactory
|
|
|
4
4
|
from udata.tests.api import APITestCase
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
class OrganizationSearchAPIV2Test(APITestCase):
|
|
8
|
+
def test_organization_search_with_model_query_param(self):
|
|
9
|
+
"""Searching organizations with 'model' as query param should not crash."""
|
|
10
|
+
OrganizationFactory.create_batch(3)
|
|
11
|
+
|
|
12
|
+
response = self.get("/api/2/organizations/search/?model=malicious")
|
|
13
|
+
self.assert200(response)
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
class OrganizationExtrasAPITest(APITestCase):
|
|
8
17
|
def setUp(self):
|
|
9
18
|
self.login()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from udata.core.reuse.factories import ReuseFactory
|
|
2
|
+
from udata.tests.api import APITestCase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ReuseSearchAPIV2Test(APITestCase):
|
|
6
|
+
def test_reuse_search_with_model_query_param(self):
|
|
7
|
+
"""Searching reuses with 'model' as query param should not crash."""
|
|
8
|
+
ReuseFactory.create_batch(3)
|
|
9
|
+
|
|
10
|
+
response = self.get("/api/2/reuses/search/?model=malicious")
|
|
11
|
+
self.assert200(response)
|
|
@@ -518,6 +518,22 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
|
|
|
518
518
|
assert isinstance(dataset, Dataset)
|
|
519
519
|
assert dataset.harvest.modified_at is None
|
|
520
520
|
|
|
521
|
+
def test_unparseable_modified_at(self):
|
|
522
|
+
"""Regression test: template strings like {{modified:toISO}} should not crash parsing."""
|
|
523
|
+
node = BNode()
|
|
524
|
+
g = Graph()
|
|
525
|
+
|
|
526
|
+
g.add((node, RDF.type, DCAT.Dataset))
|
|
527
|
+
g.add((node, DCT.identifier, Literal(faker.uuid4())))
|
|
528
|
+
g.add((node, DCT.title, Literal(faker.sentence())))
|
|
529
|
+
g.add((node, DCT.modified, Literal("{{modified:toISO}}")))
|
|
530
|
+
|
|
531
|
+
dataset = dataset_from_rdf(g)
|
|
532
|
+
dataset.validate()
|
|
533
|
+
|
|
534
|
+
assert isinstance(dataset, Dataset)
|
|
535
|
+
assert dataset.harvest.modified_at is None
|
|
536
|
+
|
|
521
537
|
def test_contact_point_individual_vcard(self):
|
|
522
538
|
g = Graph()
|
|
523
539
|
node = URIRef("https://test.org/dataset")
|
|
@@ -864,6 +880,39 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
|
|
|
864
880
|
assert resource.harvest.modified_at.date() == modified.date()
|
|
865
881
|
assert resource.format == "csv"
|
|
866
882
|
|
|
883
|
+
def test_resource_future_modified_at(self):
|
|
884
|
+
node = BNode()
|
|
885
|
+
g = Graph()
|
|
886
|
+
|
|
887
|
+
modified = faker.future_datetime()
|
|
888
|
+
|
|
889
|
+
g.add((node, RDF.type, DCAT.Distribution))
|
|
890
|
+
g.add((node, DCT.title, Literal(faker.sentence())))
|
|
891
|
+
g.add((node, DCAT.downloadURL, Literal(faker.uri())))
|
|
892
|
+
g.add((node, DCT.modified, Literal(modified)))
|
|
893
|
+
|
|
894
|
+
resource = resource_from_rdf(g)
|
|
895
|
+
resource.validate()
|
|
896
|
+
|
|
897
|
+
assert isinstance(resource, Resource)
|
|
898
|
+
assert resource.harvest.modified_at is None
|
|
899
|
+
|
|
900
|
+
def test_resource_unparseable_modified_at(self):
|
|
901
|
+
"""Regression test: template strings like {{modified:toISO}} should not crash parsing."""
|
|
902
|
+
node = BNode()
|
|
903
|
+
g = Graph()
|
|
904
|
+
|
|
905
|
+
g.add((node, RDF.type, DCAT.Distribution))
|
|
906
|
+
g.add((node, DCT.title, Literal(faker.sentence())))
|
|
907
|
+
g.add((node, DCAT.downloadURL, Literal(faker.uri())))
|
|
908
|
+
g.add((node, DCT.modified, Literal("{{modified:toISO}}")))
|
|
909
|
+
|
|
910
|
+
resource = resource_from_rdf(g)
|
|
911
|
+
resource.validate()
|
|
912
|
+
|
|
913
|
+
assert isinstance(resource, Resource)
|
|
914
|
+
assert resource.harvest.modified_at is None
|
|
915
|
+
|
|
867
916
|
def test_download_url_over_access_url(self):
|
|
868
917
|
node = BNode()
|
|
869
918
|
g = Graph()
|
|
@@ -3,6 +3,8 @@ import time
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from udata.core.dataset.factories import DatasetFactory
|
|
6
|
+
from udata.core.organization.factories import OrganizationFactory
|
|
7
|
+
from udata.core.reuse.factories import VisibleReuseFactory
|
|
6
8
|
from udata.tests.api import APITestCase
|
|
7
9
|
from udata.tests.helpers import requires_search_service
|
|
8
10
|
|
|
@@ -31,3 +33,38 @@ class SearchIntegrationTest(APITestCase):
|
|
|
31
33
|
|
|
32
34
|
titles = [d["title"] for d in response.json["data"]]
|
|
33
35
|
assert "Données spectaculaires sur les transports" in titles
|
|
36
|
+
|
|
37
|
+
def test_reuse_search_with_organization_filter(self):
|
|
38
|
+
"""
|
|
39
|
+
Regression test for: 500 Server Error when None values are passed to search service.
|
|
40
|
+
|
|
41
|
+
When searching reuses with only an organization filter, other params should not be
|
|
42
|
+
sent as literal 'None' strings (e.g. ?q=None&tag=None).
|
|
43
|
+
"""
|
|
44
|
+
org = OrganizationFactory()
|
|
45
|
+
reuse = VisibleReuseFactory(organization=org)
|
|
46
|
+
|
|
47
|
+
time.sleep(1)
|
|
48
|
+
|
|
49
|
+
response = self.get(f"/api/2/reuses/search/?organization={org.id}")
|
|
50
|
+
self.assert200(response)
|
|
51
|
+
assert response.json["total"] >= 1
|
|
52
|
+
ids = [r["id"] for r in response.json["data"]]
|
|
53
|
+
assert str(reuse.id) in ids
|
|
54
|
+
|
|
55
|
+
def test_organization_search_with_query(self):
|
|
56
|
+
"""
|
|
57
|
+
Regression test for: 500 Server Error when None values are passed to search service.
|
|
58
|
+
|
|
59
|
+
When searching organizations, other params should not be sent as literal
|
|
60
|
+
'None' strings (e.g. ?badge=None).
|
|
61
|
+
"""
|
|
62
|
+
org = OrganizationFactory(name="Organisation Unique Test")
|
|
63
|
+
|
|
64
|
+
time.sleep(1)
|
|
65
|
+
|
|
66
|
+
response = self.get("/api/2/organizations/search/?q=unique")
|
|
67
|
+
self.assert200(response)
|
|
68
|
+
assert response.json["total"] >= 1
|
|
69
|
+
ids = [o["id"] for o in response.json["data"]]
|
|
70
|
+
assert str(org.id) in ids
|
udata/tests/test_transfer.py
CHANGED
|
@@ -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.
|
|
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
|
|
Binary file
|