udata 10.8.1.dev36652__py2.py3-none-any.whl → 10.8.2__py2.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 (79) hide show
  1. udata/__init__.py +1 -1
  2. udata/app.py +0 -2
  3. udata/commands/db.py +22 -9
  4. udata/core/dataset/models.py +5 -3
  5. udata/core/discussions/api.py +2 -2
  6. udata/core/jobs/api.py +3 -3
  7. udata/core/metrics/helpers.py +10 -0
  8. udata/core/metrics/tasks.py +144 -1
  9. udata/core/organization/api.py +2 -2
  10. udata/core/post/api.py +1 -1
  11. udata/core/user/api.py +1 -1
  12. udata/features/identicon/api.py +1 -1
  13. udata/harvest/actions.py +24 -28
  14. udata/harvest/api.py +28 -36
  15. udata/harvest/backends/ckan/__init__.py +3 -0
  16. udata/harvest/backends/ckan/harvesters.py +274 -0
  17. udata/harvest/backends/ckan/schemas/__init__.py +0 -0
  18. udata/harvest/backends/ckan/schemas/ckan.py +86 -0
  19. udata/harvest/backends/ckan/schemas/dkan.py +98 -0
  20. udata/harvest/commands.py +7 -7
  21. udata/harvest/tasks.py +1 -1
  22. udata/harvest/tests/ckan/conftest.py +67 -0
  23. udata/harvest/tests/ckan/data/dkan-french-w-license.json +226 -0
  24. udata/harvest/tests/ckan/test_ckan_backend.py +697 -0
  25. udata/harvest/tests/ckan/test_ckan_backend_errors.py +140 -0
  26. udata/harvest/tests/ckan/test_ckan_backend_filters.py +130 -0
  27. udata/harvest/tests/ckan/test_dkan_backend.py +68 -0
  28. udata/harvest/tests/test_actions.py +27 -32
  29. udata/harvest/tests/test_api.py +23 -18
  30. udata/harvest/tests/test_dcat_backend.py +29 -29
  31. udata/migrations/2025-07-30-purge-old-harvest-dynamic-fields.py +29 -0
  32. udata/mongo/slug_fields.py +25 -8
  33. udata/routing.py +6 -0
  34. udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.51d706fb9521c16976bc.js} +3 -3
  35. udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.51d706fb9521c16976bc.js.map} +1 -1
  36. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.39e106d56f794ebd06a0.js} +2 -2
  37. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.39e106d56f794ebd06a0.js.map} +1 -1
  38. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.70cbb4a91b002338007e.js} +2 -2
  39. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.70cbb4a91b002338007e.js.map} +1 -1
  40. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.a348a5fff8fe2801e52a.js} +3 -3
  41. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.a348a5fff8fe2801e52a.js.map} +1 -1
  42. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.343ca020a2d38cec1a14.js} +3 -3
  43. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.343ca020a2d38cec1a14.js.map} +1 -1
  44. udata/static/chunks/{6.d663709d877baa44a71e.js → 6.a3b07de9dd2ca2d24e85.js} +3 -3
  45. udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.a3b07de9dd2ca2d24e85.js.map} +1 -1
  46. udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.462bb3029de008497675.js} +2 -2
  47. udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.462bb3029de008497675.js.map} +1 -1
  48. udata/static/common.js +1 -1
  49. udata/static/common.js.map +1 -1
  50. udata/tests/api/test_datasets_api.py +0 -46
  51. udata/tests/api/test_organizations_api.py +5 -0
  52. udata/tests/cli/test_db_cli.py +12 -0
  53. udata/tests/dataset/test_dataset_model.py +0 -16
  54. udata/tests/metrics/__init__.py +0 -0
  55. udata/tests/metrics/conftest.py +15 -0
  56. udata/tests/metrics/helpers.py +58 -0
  57. udata/tests/metrics/test_metrics.py +67 -0
  58. udata/tests/metrics/test_tasks.py +171 -0
  59. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  60. udata/translations/ar/LC_MESSAGES/udata.po +72 -65
  61. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  62. udata/translations/de/LC_MESSAGES/udata.po +72 -65
  63. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  64. udata/translations/es/LC_MESSAGES/udata.po +72 -65
  65. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  66. udata/translations/fr/LC_MESSAGES/udata.po +72 -65
  67. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  68. udata/translations/it/LC_MESSAGES/udata.po +72 -65
  69. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  70. udata/translations/pt/LC_MESSAGES/udata.po +72 -65
  71. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  72. udata/translations/sr/LC_MESSAGES/udata.po +72 -65
  73. udata/translations/udata.pot +74 -70
  74. {udata-10.8.1.dev36652.dist-info → udata-10.8.2.dist-info}/METADATA +16 -2
  75. {udata-10.8.1.dev36652.dist-info → udata-10.8.2.dist-info}/RECORD +79 -62
  76. {udata-10.8.1.dev36652.dist-info → udata-10.8.2.dist-info}/entry_points.txt +2 -0
  77. {udata-10.8.1.dev36652.dist-info → udata-10.8.2.dist-info}/LICENSE +0 -0
  78. {udata-10.8.1.dev36652.dist-info → udata-10.8.2.dist-info}/WHEEL +0 -0
  79. {udata-10.8.1.dev36652.dist-info → udata-10.8.2.dist-info}/top_level.txt +0 -0
@@ -395,7 +395,7 @@ class HarvestAPITest(MockBackendsMixin):
395
395
  "url": new_url,
396
396
  "backend": "factory",
397
397
  }
398
- api_url = url_for("api.harvest_source", ident=str(source.id))
398
+ api_url = url_for("api.harvest_source", source=source)
399
399
  response = api.put(api_url, data)
400
400
  assert200(response)
401
401
  assert response.json["url"] == new_url
@@ -403,7 +403,7 @@ class HarvestAPITest(MockBackendsMixin):
403
403
  # Source is now owned by orga, with user as member
404
404
  source.organization = OrganizationFactory(members=[Member(user=user)])
405
405
  source.save()
406
- api_url = url_for("api.harvest_source", ident=str(source.id))
406
+ api_url = url_for("api.harvest_source", source=source)
407
407
  response = api.put(api_url, data)
408
408
  assert200(response)
409
409
 
@@ -418,7 +418,7 @@ class HarvestAPITest(MockBackendsMixin):
418
418
  "url": new_url,
419
419
  "backend": "factory",
420
420
  }
421
- api_url: str = url_for("api.harvest_source", ident=str(source.id))
421
+ api_url: str = url_for("api.harvest_source", source=source)
422
422
  response = api.put(api_url, data)
423
423
 
424
424
  assert403(response)
@@ -429,7 +429,7 @@ class HarvestAPITest(MockBackendsMixin):
429
429
  source = HarvestSourceFactory()
430
430
 
431
431
  data = {"state": VALIDATION_ACCEPTED}
432
- url = url_for("api.validate_harvest_source", ident=str(source.id))
432
+ url = url_for("api.validate_harvest_source", source=source)
433
433
  response = api.post(url, data)
434
434
  assert200(response)
435
435
 
@@ -443,7 +443,7 @@ class HarvestAPITest(MockBackendsMixin):
443
443
  source = HarvestSourceFactory()
444
444
 
445
445
  data = {"state": VALIDATION_REFUSED, "comment": "Not valid"}
446
- url = url_for("api.validate_harvest_source", ident=str(source.id))
446
+ url = url_for("api.validate_harvest_source", source=source)
447
447
  response = api.post(url, data)
448
448
  assert200(response)
449
449
 
@@ -458,22 +458,27 @@ class HarvestAPITest(MockBackendsMixin):
458
458
  source = HarvestSourceFactory()
459
459
 
460
460
  data = {"validate": True}
461
- url = url_for("api.validate_harvest_source", ident=str(source.id))
461
+ url = url_for("api.validate_harvest_source", source=source)
462
462
  response = api.post(url, data)
463
463
  assert403(response)
464
464
 
465
465
  def test_get_source(self, api):
466
466
  source = HarvestSourceFactory()
467
467
 
468
- url = url_for("api.harvest_source", ident=str(source.id))
468
+ url = url_for("api.harvest_source", source=source)
469
469
  response = api.get(url)
470
470
  assert200(response)
471
471
 
472
+ def test_get_missing_source(self, api):
473
+ url = url_for("api.harvest_source", source="685bb38b9cb9284b93fd9e72")
474
+ response = api.get(url)
475
+ assert404(response)
476
+
472
477
  def test_source_preview(self, api):
473
478
  api.login()
474
479
  source = HarvestSourceFactory(backend="factory")
475
480
 
476
- url = url_for("api.preview_harvest_source", ident=str(source.id))
481
+ url = url_for("api.preview_harvest_source", source=source)
477
482
  response = api.get(url)
478
483
  assert200(response)
479
484
 
@@ -488,7 +493,7 @@ class HarvestAPITest(MockBackendsMixin):
488
493
  validation=HarvestSourceValidation(state=VALIDATION_ACCEPTED),
489
494
  )
490
495
 
491
- url = url_for("api.run_harvest_source", ident=str(source.id))
496
+ url = url_for("api.run_harvest_source", source=source)
492
497
  response = api.post(url)
493
498
  assert200(response)
494
499
 
@@ -505,7 +510,7 @@ class HarvestAPITest(MockBackendsMixin):
505
510
  validation=HarvestSourceValidation(state=VALIDATION_ACCEPTED),
506
511
  )
507
512
 
508
- url = url_for("api.run_harvest_source", ident=str(source.id))
513
+ url = url_for("api.run_harvest_source", source=source)
509
514
  response = api.post(url)
510
515
  assert400(response)
511
516
 
@@ -523,7 +528,7 @@ class HarvestAPITest(MockBackendsMixin):
523
528
  validation=HarvestSourceValidation(state=VALIDATION_ACCEPTED),
524
529
  )
525
530
 
526
- url = url_for("api.run_harvest_source", ident=str(source.id))
531
+ url = url_for("api.run_harvest_source", source=source)
527
532
  response = api.post(url)
528
533
  assert403(response)
529
534
 
@@ -540,7 +545,7 @@ class HarvestAPITest(MockBackendsMixin):
540
545
  validation=HarvestSourceValidation(state=VALIDATION_PENDING),
541
546
  )
542
547
 
543
- url = url_for("api.run_harvest_source", ident=str(source.id))
548
+ url = url_for("api.run_harvest_source", source=source)
544
549
  response = api.post(url)
545
550
  assert400(response)
546
551
 
@@ -556,7 +561,7 @@ class HarvestAPITest(MockBackendsMixin):
556
561
  user = api.login()
557
562
  source = HarvestSourceFactory(owner=user)
558
563
 
559
- url = url_for("api.harvest_source", ident=str(source.id))
564
+ url = url_for("api.harvest_source", source=source)
560
565
  response = api.delete(url)
561
566
  assert204(response)
562
567
 
@@ -568,7 +573,7 @@ class HarvestAPITest(MockBackendsMixin):
568
573
  api.login()
569
574
  source = HarvestSourceFactory()
570
575
 
571
- url = url_for("api.harvest_source", ident=str(source.id))
576
+ url = url_for("api.harvest_source", source=source)
572
577
  response = api.delete(url)
573
578
 
574
579
  assert403(response)
@@ -579,7 +584,7 @@ class HarvestAPITest(MockBackendsMixin):
579
584
  source = HarvestSourceFactory()
580
585
 
581
586
  data = "0 0 * * *"
582
- url = url_for("api.schedule_harvest_source", ident=str(source.id))
587
+ url = url_for("api.schedule_harvest_source", source=source)
583
588
  response = api.post(url, data)
584
589
  assert200(response)
585
590
 
@@ -601,7 +606,7 @@ class HarvestAPITest(MockBackendsMixin):
601
606
  source = HarvestSourceFactory()
602
607
 
603
608
  data = "0 0 * * *"
604
- url = url_for("api.schedule_harvest_source", ident=str(source.id))
609
+ url = url_for("api.schedule_harvest_source", source=source)
605
610
  response = api.post(url, data)
606
611
  assert403(response)
607
612
 
@@ -620,7 +625,7 @@ class HarvestAPITest(MockBackendsMixin):
620
625
  )
621
626
  source = HarvestSourceFactory(periodic_task=periodic_task)
622
627
 
623
- url = url_for("api.schedule_harvest_source", ident=str(source.id))
628
+ url = url_for("api.schedule_harvest_source", source=source)
624
629
  response = api.delete(url)
625
630
  assert204(response)
626
631
 
@@ -639,7 +644,7 @@ class HarvestAPITest(MockBackendsMixin):
639
644
  )
640
645
  source = HarvestSourceFactory(periodic_task=periodic_task)
641
646
 
642
- url = url_for("api.schedule_harvest_source", ident=str(source.id))
647
+ url = url_for("api.schedule_harvest_source", source=source)
643
648
  response = api.delete(url)
644
649
  assert403(response)
645
650
 
@@ -74,7 +74,7 @@ class DcatBackendTest:
74
74
  org = OrganizationFactory()
75
75
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
76
76
 
77
- actions.run(source.slug)
77
+ actions.run(source)
78
78
 
79
79
  source.reload()
80
80
 
@@ -123,7 +123,7 @@ class DcatBackendTest:
123
123
  org = OrganizationFactory()
124
124
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
125
125
 
126
- actions.run(source.slug)
126
+ actions.run(source)
127
127
 
128
128
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
129
129
 
@@ -154,7 +154,7 @@ class DcatBackendTest:
154
154
  org = OrganizationFactory()
155
155
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
156
156
 
157
- actions.run(source.slug)
157
+ actions.run(source)
158
158
 
159
159
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
160
160
 
@@ -171,7 +171,7 @@ class DcatBackendTest:
171
171
  org = OrganizationFactory()
172
172
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
173
173
 
174
- actions.run(source.slug)
174
+ actions.run(source)
175
175
 
176
176
  dataservices = Dataservice.objects
177
177
 
@@ -192,7 +192,7 @@ class DcatBackendTest:
192
192
  org = OrganizationFactory()
193
193
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
194
194
 
195
- actions.run(source.slug)
195
+ actions.run(source)
196
196
 
197
197
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
198
198
  assert len(datasets) == 8
@@ -244,7 +244,7 @@ class DcatBackendTest:
244
244
  org = OrganizationFactory()
245
245
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
246
246
 
247
- actions.run(source.slug)
247
+ actions.run(source)
248
248
 
249
249
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
250
250
 
@@ -306,7 +306,7 @@ class DcatBackendTest:
306
306
  org = OrganizationFactory()
307
307
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
308
308
 
309
- actions.run(source.slug)
309
+ actions.run(source)
310
310
 
311
311
  assert Dataset.objects.count() == 2
312
312
  assert HarvestJob.objects.first().status == "done"
@@ -320,7 +320,7 @@ class DcatBackendTest:
320
320
  org = OrganizationFactory()
321
321
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
322
322
 
323
- actions.run(source.slug)
323
+ actions.run(source)
324
324
 
325
325
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
326
326
 
@@ -345,7 +345,7 @@ class DcatBackendTest:
345
345
  org = OrganizationFactory()
346
346
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
347
347
 
348
- actions.run(source.slug)
348
+ actions.run(source)
349
349
 
350
350
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
351
351
 
@@ -388,7 +388,7 @@ class DcatBackendTest:
388
388
  url = mock_dcat(rmock, filename)
389
389
  source = HarvestSourceFactory(backend="dcat", url=url, organization=OrganizationFactory())
390
390
 
391
- actions.run(source.slug)
391
+ actions.run(source)
392
392
 
393
393
  source.reload()
394
394
 
@@ -416,8 +416,8 @@ class DcatBackendTest:
416
416
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
417
417
 
418
418
  # Run the same havester twice
419
- actions.run(source.slug)
420
- actions.run(source.slug)
419
+ actions.run(source)
420
+ actions.run(source)
421
421
 
422
422
  datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
423
423
 
@@ -431,7 +431,7 @@ class DcatBackendTest:
431
431
  org = OrganizationFactory()
432
432
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
433
433
 
434
- actions.run(source.slug)
434
+ actions.run(source)
435
435
 
436
436
  source.reload()
437
437
 
@@ -443,7 +443,7 @@ class DcatBackendTest:
443
443
  org = OrganizationFactory()
444
444
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
445
445
 
446
- actions.run(source.slug)
446
+ actions.run(source)
447
447
 
448
448
  source.reload()
449
449
 
@@ -456,7 +456,7 @@ class DcatBackendTest:
456
456
  org = OrganizationFactory()
457
457
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
458
458
 
459
- actions.run(source.slug)
459
+ actions.run(source)
460
460
 
461
461
  source.reload()
462
462
 
@@ -470,7 +470,7 @@ class DcatBackendTest:
470
470
  org = OrganizationFactory()
471
471
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
472
472
 
473
- actions.run(source.slug)
473
+ actions.run(source)
474
474
 
475
475
  source.reload()
476
476
 
@@ -493,7 +493,7 @@ class DcatBackendTest:
493
493
  org = OrganizationFactory()
494
494
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
495
495
 
496
- actions.run(source.slug)
496
+ actions.run(source)
497
497
 
498
498
  # test dct:license support
499
499
  dataset = Dataset.objects.get(harvest__dct_identifier="3")
@@ -587,7 +587,7 @@ class DcatBackendTest:
587
587
  url = mock_dcat(rmock, "geonetwork.xml", path="catalog.xml")
588
588
  org = OrganizationFactory()
589
589
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
590
- actions.run(source.slug)
590
+ actions.run(source)
591
591
  dataset = Dataset.objects.filter(organization=org).first()
592
592
  assert dataset is not None
593
593
  assert dataset.harvest is not None
@@ -616,7 +616,7 @@ class DcatBackendTest:
616
616
  url = mock_dcat(rmock, "sig.oreme.rdf")
617
617
  org = OrganizationFactory()
618
618
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
619
- actions.run(source.slug)
619
+ actions.run(source)
620
620
  dataset = Dataset.objects.filter(organization=org).first()
621
621
 
622
622
  assert dataset is not None
@@ -646,7 +646,7 @@ class DcatBackendTest:
646
646
  url = mock_dcat(rmock, "udata.xml")
647
647
  org = OrganizationFactory()
648
648
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
649
- actions.run(source.slug)
649
+ actions.run(source)
650
650
 
651
651
  source.reload()
652
652
  job = source.get_last_job()
@@ -712,7 +712,7 @@ class DcatBackendTest:
712
712
  get_mock = rmock.get(url)
713
713
  org = OrganizationFactory()
714
714
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
715
- actions.run(source.slug)
715
+ actions.run(source)
716
716
 
717
717
  assert "User-Agent" in get_mock.last_request.headers
718
718
  assert get_mock.last_request.headers["User-Agent"] == "uData/0.1 dcat"
@@ -723,7 +723,7 @@ class DcatBackendTest:
723
723
  org = OrganizationFactory()
724
724
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
725
725
 
726
- actions.run(source.slug)
726
+ actions.run(source)
727
727
 
728
728
  source.reload()
729
729
 
@@ -741,7 +741,7 @@ class DcatBackendTest:
741
741
  org = OrganizationFactory()
742
742
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
743
743
 
744
- actions.run(source.slug)
744
+ actions.run(source)
745
745
 
746
746
  source.reload()
747
747
 
@@ -773,7 +773,7 @@ class DcatBackendTest:
773
773
  rmock.head(url, headers={"Content-Type": "application/json"})
774
774
  org = OrganizationFactory()
775
775
  source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
776
- actions.run(source.slug)
776
+ actions.run(source)
777
777
 
778
778
  source.reload()
779
779
 
@@ -787,7 +787,7 @@ class DcatBackendTest:
787
787
  rmock.get(url, status_code=404)
788
788
 
789
789
  source = HarvestSourceFactory(backend="dcat", url=url, organization=OrganizationFactory())
790
- actions.run(source.slug)
790
+ actions.run(source)
791
791
  source.reload()
792
792
 
793
793
  job = source.get_last_job()
@@ -800,7 +800,7 @@ class DcatBackendTest:
800
800
  rmock.head(url, status_code=404)
801
801
 
802
802
  source = HarvestSourceFactory(backend="dcat", url=url, organization=OrganizationFactory())
803
- actions.run(source.slug)
803
+ actions.run(source)
804
804
  source.reload()
805
805
 
806
806
  job = source.get_last_job()
@@ -817,7 +817,7 @@ class CswDcatBackendTest:
817
817
  org = OrganizationFactory()
818
818
  source = HarvestSourceFactory(backend="csw-dcat", url=url, organization=org)
819
819
 
820
- actions.run(source.slug)
820
+ actions.run(source)
821
821
 
822
822
  source.reload()
823
823
 
@@ -863,7 +863,7 @@ class CswDcatBackendTest:
863
863
  org = OrganizationFactory()
864
864
  source = HarvestSourceFactory(backend="csw-dcat", url=url, organization=org)
865
865
 
866
- actions.run(source.slug)
866
+ actions.run(source)
867
867
 
868
868
  assert "User-Agent" in get_mock.last_request.headers
869
869
  assert get_mock.last_request.headers["User-Agent"] == "uData/0.1 csw-dcat"
@@ -902,7 +902,7 @@ class CswIso19139DcatBackendTest:
902
902
  },
903
903
  )
904
904
 
905
- actions.run(source.slug)
905
+ actions.run(source)
906
906
 
907
907
  source.reload()
908
908
 
@@ -0,0 +1,29 @@
1
+ """
2
+ This migration removes legacy harvest dynamic fields
3
+ """
4
+
5
+ import logging
6
+
7
+ from mongoengine.connection import get_db
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ def migrate(db):
13
+ # Remove legacy fields (`ods_has_records`, `ods_url`, ...) from old harvested datasets and resources
14
+ dataset_legacy_fields = ["ods_has_records", "ods_url", "ods_geo"]
15
+ for field in dataset_legacy_fields:
16
+ result = get_db().dataset.update_many({}, {"$unset": {f"harvest.{field}": 1}})
17
+ log.info(
18
+ f"Harvest Dataset dynamic legacy fields ({field}) removed from {result.modified_count} objects"
19
+ )
20
+
21
+ resource_legacy_fields = ["ods_type"]
22
+ for field in resource_legacy_fields:
23
+ result = get_db().dataset.update_many(
24
+ {"resources": {"$exists": True, "$type": "array"}},
25
+ {"$unset": {f"resources.$[].harvest.{field}": 1}},
26
+ )
27
+ log.info(
28
+ f"Harvest Resource dynamic legacy fields ({field}) removed from {result.modified_count} objects"
29
+ )
@@ -172,21 +172,38 @@ def populate_slug(instance, field):
172
172
  # Ensure uniqueness
173
173
  if field.unique:
174
174
  base_slug = slug
175
- index = 1
176
- qs = instance.__class__.objects
175
+ qs = instance.__class__.objects.no_cache()
177
176
  if previous:
178
177
  qs = qs(id__ne=previous.id)
179
178
 
180
- def exists(s):
181
- return qs(**{field.db_field: s}).clear_cls_query().limit(1).count(True) > 0
179
+ def exists(slug):
180
+ return qs(**{field.db_field: slug}).clear_cls_query().limit(1).count(True) > 0
182
181
 
183
- while exists(slug):
184
- # keep space for index suffix, trim slug if needed
182
+ def get_existing_slug_suffixes(slug):
183
+ qs_suffix = qs(slug__regex=f"^{slug}-\d*$").clear_cls_query().only(field.db_field)
184
+ return [getattr(obj, field.db_field) for obj in qs_suffix]
185
+
186
+ def trim_base_slug(base_slug, index):
185
187
  slug_overflow = len("{0}-{1}".format(base_slug, index)) - field.max_length
186
188
  if slug_overflow >= 1:
187
189
  base_slug = base_slug[:-slug_overflow]
188
- slug = "{0}-{1}".format(base_slug, index)
189
- index += 1
190
+ return base_slug
191
+
192
+ if exists(base_slug):
193
+ # We'll iterate to get the first free slug suffix
194
+ index = 1
195
+ existing_slugs = None
196
+ while True:
197
+ # Keep space for index suffix, trim slug if needed
198
+ trimmed_slug = trim_base_slug(base_slug, index)
199
+ # Find all existing slugs with suffixes
200
+ if existing_slugs is None or trimmed_slug != base_slug:
201
+ base_slug = trimmed_slug
202
+ existing_slugs = set(sorted(get_existing_slug_suffixes(base_slug)))
203
+ slug = "{0}-{1}".format(base_slug, index)
204
+ if slug not in existing_slugs:
205
+ break
206
+ index += 1
190
207
 
191
208
  if is_uuid(slug):
192
209
  slug = "{0}-uuid".format(slug)
udata/routing.py CHANGED
@@ -10,6 +10,7 @@ from werkzeug.urls import url_quote
10
10
  from udata import models
11
11
  from udata.core.dataservices.models import Dataservice
12
12
  from udata.core.spatial.models import GeoZone
13
+ from udata.harvest.models import HarvestSource
13
14
  from udata.i18n import ISO_639_1_CODES
14
15
  from udata.mongo import db
15
16
  from udata.uris import cdata_url, homepage_url
@@ -136,6 +137,10 @@ class DataserviceConverter(ModelConverter):
136
137
  model = Dataservice
137
138
 
138
139
 
140
+ class HarvestSourceConverter(ModelConverter):
141
+ model = HarvestSource
142
+
143
+
139
144
  class CommunityResourceConverter(ModelConverter):
140
145
  model = models.CommunityResource
141
146
 
@@ -239,6 +244,7 @@ def init_app(app):
239
244
  app.url_map.converters["dataset"] = DatasetConverter
240
245
  app.url_map.converters["dataset_without_resources"] = DatasetWithoutResourcesConverter
241
246
  app.url_map.converters["dataservice"] = DataserviceConverter
247
+ app.url_map.converters["harvest_source"] = HarvestSourceConverter
242
248
  app.url_map.converters["crid"] = CommunityResourceConverter
243
249
  app.url_map.converters["org"] = OrganizationConverter
244
250
  app.url_map.converters["reuse"] = ReuseConverter