udata 10.1.3.dev34359__py2.py3-none-any.whl → 10.1.4__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 (39) hide show
  1. udata/__init__.py +1 -1
  2. udata/core/dataset/api.py +15 -1
  3. udata/core/dataset/apiv2.py +26 -4
  4. udata/core/dataset/models.py +40 -3
  5. udata/core/user/models.py +1 -1
  6. udata/core/user/tasks.py +6 -6
  7. udata/harvest/tests/dcat/udata.xml +180 -0
  8. udata/harvest/tests/test_dcat_backend.py +65 -0
  9. udata/rdf.py +6 -1
  10. udata/routing.py +13 -2
  11. udata/settings.py +4 -0
  12. udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.55ab79044cda0271b595.js} +3 -3
  13. udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.55ab79044cda0271b595.js.map} +1 -1
  14. udata/static/chunks/{13.f29411b06be1883356a3.js → 13.2d06442dd9a05d9777b5.js} +2 -2
  15. udata/static/chunks/{13.f29411b06be1883356a3.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
  16. udata/static/chunks/{17.3bd0340930d4a314ce9c.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
  17. udata/static/chunks/{17.3bd0340930d4a314ce9c.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
  18. udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.f03a102365af4315f9db.js} +3 -3
  19. udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
  20. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.5660483641193b7f8295.js} +3 -3
  21. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.5660483641193b7f8295.js.map} +1 -1
  22. udata/static/chunks/{6.d663709d877baa44a71e.js → 6.30dce49d17db07600b06.js} +3 -3
  23. udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.30dce49d17db07600b06.js.map} +1 -1
  24. udata/static/chunks/{8.54e44b102164ae5e7a67.js → 8.b58fcd977fcaf3415571.js} +2 -2
  25. udata/static/chunks/{8.54e44b102164ae5e7a67.js.map → 8.b58fcd977fcaf3415571.js.map} +1 -1
  26. udata/static/common.js +1 -1
  27. udata/static/common.js.map +1 -1
  28. udata/templates/mail/account_inactivity.html +1 -1
  29. udata/templates/mail/account_inactivity.txt +1 -1
  30. udata/tests/api/test_datasets_api.py +49 -8
  31. udata/tests/apiv2/test_datasets.py +21 -0
  32. udata/tests/dataset/test_dataset_model.py +25 -0
  33. udata/tests/user/test_user_tasks.py +7 -7
  34. {udata-10.1.3.dev34359.dist-info → udata-10.1.4.dist-info}/METADATA +10 -2
  35. {udata-10.1.3.dev34359.dist-info → udata-10.1.4.dist-info}/RECORD +39 -38
  36. {udata-10.1.3.dev34359.dist-info → udata-10.1.4.dist-info}/LICENSE +0 -0
  37. {udata-10.1.3.dev34359.dist-info → udata-10.1.4.dist-info}/WHEEL +0 -0
  38. {udata-10.1.3.dev34359.dist-info → udata-10.1.4.dist-info}/entry_points.txt +0 -0
  39. {udata-10.1.3.dev34359.dist-info → udata-10.1.4.dist-info}/top_level.txt +0 -0
udata/__init__.py CHANGED
@@ -4,5 +4,5 @@
4
4
  udata
5
5
  """
6
6
 
7
- __version__ = "10.1.3.dev"
7
+ __version__ = "10.1.4"
8
8
  __description__ = "Open data portal"
udata/core/dataset/api.py CHANGED
@@ -402,7 +402,21 @@ class ResourcesAPI(API):
402
402
  def put(self, dataset):
403
403
  """Reorder resources"""
404
404
  ResourceEditPermission(dataset).test()
405
- data = {"resources": request.json}
405
+ resources = request.json
406
+ if len(dataset.resources) != len(resources):
407
+ api.abort(
408
+ 400,
409
+ f"All resources must be reordered, you provided {len(resources)} "
410
+ f"out of {len(dataset.resources)}",
411
+ )
412
+ if set(r["id"] if isinstance(r, dict) else r for r in resources) != set(
413
+ str(r.id) for r in dataset.resources
414
+ ):
415
+ api.abort(
416
+ 400,
417
+ f"Resource ids must match existing ones in dataset, ie: {set(str(r.id) for r in dataset.resources)}",
418
+ )
419
+ data = {"resources": resources}
406
420
  form = ResourcesListForm.from_json(
407
421
  data, obj=dataset, instance=dataset, meta={"csrf": False}
408
422
  )
@@ -2,6 +2,7 @@ import logging
2
2
 
3
3
  import mongoengine
4
4
  from flask import abort, request, url_for
5
+ from flask_login import current_user
5
6
  from flask_restx import marshal
6
7
 
7
8
  from udata import search
@@ -11,7 +12,7 @@ from udata.core.organization.api_fields import member_user_with_email_fields
11
12
  from udata.core.spatial.api_fields import geojson
12
13
  from udata.utils import get_by
13
14
 
14
- from .api import ResourceMixin
15
+ from .api import DEFAULT_SORTING, DatasetApiParser, ResourceMixin
15
16
  from .api_fields import (
16
17
  badge_fields,
17
18
  catalog_schema_fields,
@@ -128,7 +129,7 @@ dataset_fields = apiv2.model(
128
129
  _external=True,
129
130
  ),
130
131
  "type": "GET",
131
- "total": len(o.resources),
132
+ "total": o.resources_len, # :ResourcesLengthProperty may call MongoDB to fetch the length if resources were not fetched
132
133
  },
133
134
  description="Link to the dataset resources",
134
135
  ),
@@ -155,7 +156,7 @@ dataset_fields = apiv2.model(
155
156
  ),
156
157
  "frequency_date": fields.ISODateTime(
157
158
  description=(
158
- "Next expected update date, you will be notified " "once that date is reached."
159
+ "Next expected update date, you will be notified once that date is reached."
159
160
  )
160
161
  ),
161
162
  "harvest": fields.Nested(
@@ -276,7 +277,28 @@ class DatasetSearchAPI(API):
276
277
  abort(500, "Internal search service error")
277
278
 
278
279
 
279
- @ns.route("/<dataset:dataset>/", endpoint="dataset", doc=common_doc)
280
+ dataset_parser = DatasetApiParser()
281
+
282
+
283
+ @ns.route("/", endpoint="datasets")
284
+ class DatasetListAPI(API):
285
+ """Datasets collection endpoint"""
286
+
287
+ @apiv2.doc("list_datasets")
288
+ @apiv2.expect(dataset_parser.parser)
289
+ @apiv2.marshal_with(dataset_page_fields)
290
+ def get(self):
291
+ """List or search all datasets"""
292
+ args = dataset_parser.parse()
293
+ datasets = Dataset.objects.exclude("resources").visible_by_user(
294
+ current_user, mongoengine.Q(private__ne=True, archived=None, deleted=None)
295
+ )
296
+ datasets = dataset_parser.parse_filters(datasets, args)
297
+ sort = args["sort"] or ("$text_score" if args["q"] else None) or DEFAULT_SORTING
298
+ return datasets.order_by(sort).paginate(args["page"], args["page_size"])
299
+
300
+
301
+ @ns.route("/<dataset_without_resources:dataset>/", endpoint="dataset", doc=common_doc)
280
302
  @apiv2.response(404, "Dataset not found")
281
303
  @apiv2.response(410, "Dataset has been deleted")
282
304
  class DatasetAPI(API):
@@ -2,6 +2,7 @@ import logging
2
2
  import re
3
3
  from datetime import datetime, timedelta
4
4
  from pydoc import locate
5
+ from typing import Self
5
6
  from urllib.parse import urlparse
6
7
 
7
8
  import requests
@@ -11,7 +12,7 @@ from flask import current_app
11
12
  from mongoengine import DynamicEmbeddedDocument
12
13
  from mongoengine import ValidationError as MongoEngineValidationError
13
14
  from mongoengine.fields import DateTimeField
14
- from mongoengine.signals import post_save, pre_save
15
+ from mongoengine.signals import post_save, pre_init, pre_save
15
16
  from stringdist import rdlevenshtein
16
17
  from werkzeug.utils import cached_property
17
18
 
@@ -617,6 +618,33 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
617
618
 
618
619
  verbose_name = _("dataset")
619
620
 
621
+ missing_resources = False
622
+
623
+ @cached_property
624
+ def resources_len(self):
625
+ # :ResourcesLengthProperty
626
+ # If we've excluded the resources from the Mongo request we need
627
+ # to do a new Mongo request to fetch the resources length manually.
628
+ # If the resources are already present, just return the `len()` of the array.
629
+ if not self.missing_resources:
630
+ return len(self.resources)
631
+
632
+ pipeline = [
633
+ {"$project": {"_id": 1, "resources_len": {"$size": {"$ifNull": ["$resources", []]}}}}
634
+ ]
635
+ data = Dataset.objects(id=self.id).aggregate(pipeline)
636
+
637
+ return next(data)["resources_len"]
638
+
639
+ @classmethod
640
+ def pre_init(cls, sender, document: Self, values, **kwargs):
641
+ # MongoEngine loses the information about raw values during the __init__ function
642
+ # Here we catch the raw values from the database (or from the creation of the object)
643
+ # and we check if resources were returned (sometimes we exclude `resources` from the
644
+ # Mongo request to improve perfs)
645
+ # This is used in :ResourcesLengthProperty
646
+ document.missing_resources = "resources" not in values
647
+
620
648
  @classmethod
621
649
  def pre_save(cls, sender, document, **kwargs):
622
650
  cls.before_save.send(document)
@@ -941,9 +969,17 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
941
969
  "url": endpoint_for("datasets.show", "api.dataset", dataset=self, _external=True),
942
970
  "name": self.title,
943
971
  "keywords": ",".join(self.tags),
944
- "distribution": [resource.json_ld for resource in self.resources],
972
+ "distribution": [
973
+ resource.json_ld
974
+ for resource in self.resources[: current_app.config["MAX_RESOURCES_IN_JSON_LD"]]
975
+ ],
945
976
  # Theses values are not standard
946
- "contributedDistribution": [resource.json_ld for resource in self.community_resources],
977
+ "contributedDistribution": [
978
+ resource.json_ld
979
+ for resource in self.community_resources[
980
+ : current_app.config["MAX_RESOURCES_IN_JSON_LD"]
981
+ ]
982
+ ],
947
983
  "extras": [get_json_ld_extra(*item) for item in self.extras.items()],
948
984
  }
949
985
 
@@ -995,6 +1031,7 @@ class Dataset(WithMetrics, DatasetBadgeMixin, Owned, db.Document):
995
1031
  self.save()
996
1032
 
997
1033
 
1034
+ pre_init.connect(Dataset.pre_init, sender=Dataset)
998
1035
  pre_save.connect(Dataset.pre_save, sender=Dataset)
999
1036
  post_save.connect(Dataset.post_save, sender=Dataset)
1000
1037
 
udata/core/user/models.py CHANGED
@@ -83,7 +83,7 @@ class User(WithMetrics, UserMixin, db.Document):
83
83
  extras = db.ExtrasField()
84
84
 
85
85
  # Used to track notification for automatic inactive users deletion
86
- # when YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION is set
86
+ # when YEARS_OF_INACTIVITY_BEFORE_DELETION is set
87
87
  inactive_deletion_notified_at = db.DateTimeField()
88
88
 
89
89
  before_save = Signal()
udata/core/user/tasks.py CHANGED
@@ -21,14 +21,14 @@ def send_test_mail(email):
21
21
 
22
22
  @job("notify-inactive-users")
23
23
  def notify_inactive_users(self):
24
- if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"]:
24
+ if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DELETION"]:
25
25
  logging.warning(
26
- "YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION setting is not set, no deletion planned"
26
+ "YEARS_OF_INACTIVITY_BEFORE_DELETION setting is not set, no deletion planned"
27
27
  )
28
28
  return
29
29
  notification_comparison_date = (
30
30
  datetime.utcnow()
31
- - timedelta(days=current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"] * 365)
31
+ - timedelta(days=current_app.config["YEARS_OF_INACTIVITY_BEFORE_DELETION"] * 365)
32
32
  + timedelta(days=current_app.config["DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY"])
33
33
  )
34
34
 
@@ -56,9 +56,9 @@ def notify_inactive_users(self):
56
56
 
57
57
  @job("delete-inactive-users")
58
58
  def delete_inactive_users(self):
59
- if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"]:
59
+ if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DELETION"]:
60
60
  logging.warning(
61
- "YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION setting is not set, no deletion planned"
61
+ "YEARS_OF_INACTIVITY_BEFORE_DELETION setting is not set, no deletion planned"
62
62
  )
63
63
  return
64
64
 
@@ -70,7 +70,7 @@ def delete_inactive_users(self):
70
70
 
71
71
  # Delete inactive users upon notification delay if user still hasn't logged in
72
72
  deletion_comparison_date = datetime.utcnow() - timedelta(
73
- days=current_app.config["YEARS_OF_INACTIVITY_BEFORE_DEACTIVATION"] * 365
73
+ days=current_app.config["YEARS_OF_INACTIVITY_BEFORE_DELETION"] * 365
74
74
  )
75
75
  notified_at = datetime.utcnow() - timedelta(
76
76
  days=current_app.config["DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY"]
@@ -0,0 +1,180 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <rdf:RDF
3
+ xmlns:adms="http://www.w3.org/ns/adms#"
4
+ xmlns:dcat="http://www.w3.org/ns/dcat#"
5
+ xmlns:dct="http://purl.org/dc/terms/"
6
+ xmlns:foaf="http://xmlns.com/foaf/0.1/"
7
+ xmlns:hydra="http://www.w3.org/ns/hydra/core#"
8
+ xmlns:ns1="http://data.europa.eu/930/"
9
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
10
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
11
+ xmlns:skos="http://www.w3.org/2004/02/skos/core#"
12
+ xmlns:vcard="http://www.w3.org/2006/vcard/ns#"
13
+ >
14
+ <rdf:Description rdf:about="https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/">
15
+ <dct:identifier>https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/</dct:identifier>
16
+ <adms:identifier rdf:nodeID="N7cbef9d8b574438f86f859e81e04e4c0"/>
17
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Dataset"/>
18
+ <dct:title>Bureaux de vote - Vanves</dct:title>
19
+ <dct:description>La liste des 23 bureaux de vote à Vanves</dct:description>
20
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
21
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
22
+ <dcat:landingPage rdf:resource="https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"/>
23
+ <dcat:keyword>administration-finances-publiques</dcat:keyword>
24
+ <dcat:keyword>bureaux-de-vote</dcat:keyword>
25
+ <dcat:keyword>elections</dcat:keyword>
26
+ <dcat:distribution rdf:resource="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-5dd4e0b2-4d96-4f36-b73e-b78ec993703c"/>
27
+ <dcat:distribution rdf:resource="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-0f80d285-72f8-49f8-a691-1dad6bd2f6db"/>
28
+ <dcat:distribution rdf:resource="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-d78d1245-6e8e-44d8-88cd-87b1dd66034f"/>
29
+ <dcat:distribution rdf:resource="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a"/>
30
+ <ns1:distributor rdf:resource="https://www.data.gouv.fr/organizations/54884a24c751df7226a3fc16/"/>
31
+ <dct:publisher rdf:resource="https://www.data.gouv.fr/api/1/contacts/67b7c3b0e2714e0e9de260a3/"/>
32
+ <dct:creator rdf:resource="https://www.data.gouv.fr/api/1/contacts/67b7c3b0e2714e0e9de260a4/"/>
33
+ </rdf:Description>
34
+ <rdf:Description rdf:about="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-5dd4e0b2-4d96-4f36-b73e-b78ec993703c">
35
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
36
+ <dct:identifier>5dd4e0b2-4d96-4f36-b73e-b78ec993703c</dct:identifier>
37
+ <dct:title>bureau-de-vote-vanves.csv</dct:title>
38
+ <dct:description>Bureaux de vote - Vanves (csv)</dct:description>
39
+ <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/csv?use_labels=true"/>
40
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/5dd4e0b2-4d96-4f36-b73e-b78ec993703c"/>
41
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
42
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
43
+ <dct:rights>License Not Specified</dct:rights>
44
+ <dcat:mediaType>text/csv</dcat:mediaType>
45
+ <dct:format>csv</dct:format>
46
+ </rdf:Description>
47
+ <rdf:Description rdf:about="https://www.data.gouv.fr/api/1/organizations/54884a24c751df7226a3fc16/catalog">
48
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Catalog"/>
49
+ <rdf:type rdf:resource="http://www.w3.org/ns/hydra/core#Collection"/>
50
+ <dct:publisher rdf:resource="https://www.data.gouv.fr/organizations/54884a24c751df7226a3fc16/"/>
51
+ <dct:title>Ville de Vanves</dct:title>
52
+ <dct:description>Ville de Vanves</dct:description>
53
+ <dcat:dataset rdf:resource="https://vanves-seineouest.opendatasoft.com/explore/dataset/vfe_public_219200755_20240506/"/>
54
+ <dcat:dataset rdf:resource="https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"/>
55
+ <dcat:service rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/"/>
56
+ <hydra:totalItems rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">2</hydra:totalItems>
57
+ <hydra:view rdf:resource="https://www.data.gouv.fr/api/1/organizations/54884a24c751df7226a3fc16/catalog.rdf?page=1&amp;page_size=100"/>
58
+ </rdf:Description>
59
+ <rdf:Description rdf:about="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-d78d1245-6e8e-44d8-88cd-87b1dd66034f">
60
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
61
+ <dct:identifier>d78d1245-6e8e-44d8-88cd-87b1dd66034f</dct:identifier>
62
+ <dct:title>bureau-de-vote-vanves.geojson</dct:title>
63
+ <dct:description>Bureaux de vote - Vanves (geojson)</dct:description>
64
+ <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/geojson"/>
65
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/d78d1245-6e8e-44d8-88cd-87b1dd66034f"/>
66
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
67
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
68
+ <dct:rights>License Not Specified</dct:rights>
69
+ <dcat:mediaType>application/json</dcat:mediaType>
70
+ <dct:format>json</dct:format>
71
+ </rdf:Description>
72
+ <rdf:Description rdf:about="https://www.data.gouv.fr/api/1/organizations/54884a24c751df7226a3fc16/catalog.rdf?page=1&amp;page_size=100">
73
+ <rdf:type rdf:resource="http://www.w3.org/ns/hydra/core#PartialCollectionView"/>
74
+ <hydra:first rdf:resource="https://www.data.gouv.fr/api/1/organizations/54884a24c751df7226a3fc16/catalog.rdf?page=1&amp;page_size=100"/>
75
+ <hydra:last rdf:resource="https://www.data.gouv.fr/api/1/organizations/54884a24c751df7226a3fc16/catalog.rdf?page=1&amp;page_size=100"/>
76
+ </rdf:Description>
77
+ <rdf:Description rdf:about="https://www.data.gouv.fr/organizations/54884a24c751df7226a3fc16/">
78
+ <rdf:type rdf:resource="http://xmlns.com/foaf/0.1/Organization"/>
79
+ <foaf:name>Ville de Vanves</foaf:name>
80
+ <rdfs:label>Ville de Vanves</rdfs:label>
81
+ <foaf:homepage rdf:resource="http://www.vanves.fr"/>
82
+ </rdf:Description>
83
+ <rdf:Description rdf:about="https://www.data.gouv.fr/datasets/66397176ff2a31840e29304c/#resource-482677b8-379c-45a5-83ef-0361fecb4cc3">
84
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
85
+ <dct:identifier>482677b8-379c-45a5-83ef-0361fecb4cc3</dct:identifier>
86
+ <dct:title>vfe_public_219200755_20240506.json</dct:title>
87
+ <dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc (json)</dct:description>
88
+ <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/vfe_public_219200755_20240506/exports/json"/>
89
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/482677b8-379c-45a5-83ef-0361fecb4cc3"/>
90
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
91
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
92
+ <dct:rights>License Not Specified</dct:rights>
93
+ <dcat:mediaType>application/json</dcat:mediaType>
94
+ <dct:format>json</dct:format>
95
+ </rdf:Description>
96
+ <rdf:Description rdf:about="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a">
97
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
98
+ <dct:identifier>b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a</dct:identifier>
99
+ <dct:title>bureau-de-vote-vanves.zip</dct:title>
100
+ <dct:description>Bureaux de vote - Vanves (shp)</dct:description>
101
+ <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/shp"/>
102
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a"/>
103
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
104
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
105
+ <dct:rights>License Not Specified</dct:rights>
106
+ <dcat:mediaType>application/zip</dcat:mediaType>
107
+ <dct:format>zip</dct:format>
108
+ </rdf:Description>
109
+ <rdf:Description rdf:about="https://www.data.gouv.fr/datasets/66397176ff2a31840e29304c/#resource-aab4d337-a617-4c18-a045-291d68787a7d">
110
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
111
+ <dct:identifier>aab4d337-a617-4c18-a045-291d68787a7d</dct:identifier>
112
+ <dct:title>vfe_public_219200755_20240506.csv</dct:title>
113
+ <dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc (csv)</dct:description>
114
+ <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/vfe_public_219200755_20240506/exports/csv?use_labels=true"/>
115
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/aab4d337-a617-4c18-a045-291d68787a7d"/>
116
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
117
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
118
+ <dct:rights>License Not Specified</dct:rights>
119
+ <dcat:mediaType>text/csv</dcat:mediaType>
120
+ <dct:format>csv</dct:format>
121
+ </rdf:Description>
122
+ <rdf:Description rdf:about="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/">
123
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#DataService"/>
124
+ <dct:identifier>https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/</dct:identifier>
125
+ <dct:title>Explore API v2 https://vanves-seineouest.opendatasoft.com</dct:title>
126
+ <dct:description></dct:description>
127
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-07-12T00:03:38.764000</dct:issued>
128
+ <dcat:endpointURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/"/>
129
+ <dcat:landingPage rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/console"/>
130
+ <dcat:endpointDescription rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/swagger.json"/>
131
+ <dcat:servesDataset rdf:resource="https://vanves-seineouest.opendatasoft.com/explore/dataset/vfe_public_219200755_20240506/"/>
132
+ <dcat:servesDataset rdf:resource="https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"/>
133
+ </rdf:Description>
134
+ <rdf:Description rdf:nodeID="N25911b8ad0604e9f8de4d7e1965eb81a">
135
+ <rdf:type rdf:resource="http://www.w3.org/ns/adms#Identifier"/>
136
+ <dct:creator>data.gouv.fr</dct:creator>
137
+ <skos:notation>https://www.data.gouv.fr/datasets/66397176ff2a31840e29304c/</skos:notation>
138
+ </rdf:Description>
139
+ <rdf:Description rdf:about="https://vanves-seineouest.opendatasoft.com/explore/dataset/vfe_public_219200755_20240506/">
140
+ <dct:identifier>https://vanves-seineouest.opendatasoft.com/explore/dataset/vfe_public_219200755_20240506/</dct:identifier>
141
+ <adms:identifier rdf:nodeID="N25911b8ad0604e9f8de4d7e1965eb81a"/>
142
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Dataset"/>
143
+ <dct:title>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc</dct:title>
144
+ <dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc</dct:description>
145
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
146
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
147
+ <dcat:landingPage rdf:resource="https://vanves-seineouest.opendatasoft.com/explore/dataset/vfe_public_219200755_20240506/"/>
148
+ <dcat:distribution rdf:resource="https://www.data.gouv.fr/datasets/66397176ff2a31840e29304c/#resource-aab4d337-a617-4c18-a045-291d68787a7d"/>
149
+ <dcat:distribution rdf:resource="https://www.data.gouv.fr/datasets/66397176ff2a31840e29304c/#resource-482677b8-379c-45a5-83ef-0361fecb4cc3"/>
150
+ <ns1:distributor rdf:resource="https://www.data.gouv.fr/organizations/54884a24c751df7226a3fc16/"/>
151
+ <dct:publisher rdf:resource="https://www.data.gouv.fr/api/1/contacts/67b7c3b0e2714e0e9de260a3/"/>
152
+ <dct:creator rdf:resource="https://www.data.gouv.fr/api/1/contacts/67b7c3b0e2714e0e9de260a4/"/>
153
+ </rdf:Description>
154
+ <rdf:Description rdf:about="https://www.data.gouv.fr/api/1/contacts/67b7c3b0e2714e0e9de260a4/">
155
+ <rdf:type rdf:resource="http://www.w3.org/2006/vcard/ns#Kind"/>
156
+ <vcard:fn>Vanves</vcard:fn>
157
+ </rdf:Description>
158
+ <rdf:Description rdf:about="https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/#resource-0f80d285-72f8-49f8-a691-1dad6bd2f6db">
159
+ <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
160
+ <dct:identifier>0f80d285-72f8-49f8-a691-1dad6bd2f6db</dct:identifier>
161
+ <dct:title>bureau-de-vote-vanves.json</dct:title>
162
+ <dct:description>Bureaux de vote - Vanves (json)</dct:description>
163
+ <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/json"/>
164
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/0f80d285-72f8-49f8-a691-1dad6bd2f6db"/>
165
+ <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
166
+ <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
167
+ <dct:rights>License Not Specified</dct:rights>
168
+ <dcat:mediaType>application/json</dcat:mediaType>
169
+ <dct:format>json</dct:format>
170
+ </rdf:Description>
171
+ <rdf:Description rdf:about="https://www.data.gouv.fr/api/1/contacts/67b7c3b0e2714e0e9de260a3/">
172
+ <rdf:type rdf:resource="http://www.w3.org/2006/vcard/ns#Kind"/>
173
+ <vcard:fn>Vanves</vcard:fn>
174
+ </rdf:Description>
175
+ <rdf:Description rdf:nodeID="N7cbef9d8b574438f86f859e81e04e4c0">
176
+ <rdf:type rdf:resource="http://www.w3.org/ns/adms#Identifier"/>
177
+ <dct:creator>data.gouv.fr</dct:creator>
178
+ <skos:notation>https://www.data.gouv.fr/datasets/61892c9d076a4d62434a1318/</skos:notation>
179
+ </rdf:Description>
180
+ </rdf:RDF>
@@ -624,6 +624,71 @@ class DcatBackendTest:
624
624
  ) # noqa
625
625
  assert dataset.harvest.last_update.date() == date.today()
626
626
 
627
+ def test_udata_xml_catalog(self, rmock):
628
+ LicenseFactory(id="fr-lo", title="Licence ouverte / Open Licence")
629
+ url = mock_dcat(rmock, "udata.xml")
630
+ org = OrganizationFactory()
631
+ source = HarvestSourceFactory(backend="dcat", url=url, organization=org)
632
+ actions.run(source.slug)
633
+
634
+ source.reload()
635
+ job = source.get_last_job()
636
+ assert len(job.items) == 3
637
+
638
+ assert Dataset.objects.filter(organization=org).count() == 2
639
+ dataset = Dataset.objects.filter(organization=org, title="Bureaux de vote - Vanves").first()
640
+
641
+ assert dataset is not None
642
+ assert "bureaux-de-vote" in dataset.tags # support dcat:keyword
643
+ assert len(dataset.resources) == 4
644
+ assert dataset.description == "La liste des 23 bureaux de vote à Vanves"
645
+ assert dataset.harvest is not None
646
+ assert (
647
+ dataset.harvest.dct_identifier
648
+ == "https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"
649
+ )
650
+ assert (
651
+ dataset.harvest.remote_id
652
+ == "https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"
653
+ )
654
+ assert dataset.harvest.created_at.isoformat() == "2019-04-19T12:21:56"
655
+ assert dataset.harvest.modified_at.isoformat() == "2019-04-19T12:21:56"
656
+ assert (
657
+ dataset.harvest.uri
658
+ == "https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"
659
+ )
660
+ assert (
661
+ dataset.harvest.remote_url
662
+ == "https://vanves-seineouest.opendatasoft.com/explore/dataset/bureau-de-vote-vanves/"
663
+ )
664
+ assert dataset.harvest.last_update.date() == date.today()
665
+
666
+ assert Dataservice.objects(organization=org).count() == 1
667
+ service = Dataservice.objects(organization=org).first()
668
+
669
+ assert service is not None
670
+ assert len(service.datasets) == 2
671
+ assert service.title == "Explore API v2 https://vanves-seineouest.opendatasoft.com"
672
+ assert service.description == ""
673
+ assert (
674
+ service.machine_documentation_url
675
+ == "https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/swagger.json"
676
+ )
677
+ assert (
678
+ service.base_api_url == "https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/"
679
+ )
680
+ assert service.harvest is not None
681
+ assert (
682
+ service.harvest.remote_id
683
+ == "https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/"
684
+ )
685
+ assert service.harvest.created_at.isoformat() == "2024-07-12T00:03:38.764000"
686
+ assert (
687
+ service.harvest.remote_url
688
+ == "https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/console"
689
+ )
690
+ assert service.harvest.last_update.date() == date.today()
691
+
627
692
  def test_user_agent_get(self, rmock):
628
693
  url = mock_dcat(rmock, "catalog.xml", path="without/extension")
629
694
  rmock.head(url, headers={"Content-Type": "application/xml; charset=utf-8"})
udata/rdf.py CHANGED
@@ -312,7 +312,12 @@ def themes_from_rdf(rdf):
312
312
  def contact_points_from_rdf(rdf, prop, role, dataset):
313
313
  for contact_point in rdf.objects(prop):
314
314
  # Read contact point information
315
- if prop == DCAT.contactPoint: # Could be split on the type of contact_point instead
315
+ if isinstance(contact_point, Literal):
316
+ log.warning(f"Found a `Literal` inside {prop}, `foaf:Agent` or `vcard:Kind` expected.")
317
+ name = contact_point.toPython()
318
+ email = None
319
+ contact_form = None
320
+ elif prop == DCAT.contactPoint: # Could be split on the type of contact_point instead
316
321
  name = rdf_value(contact_point, VCARD.fn) or ""
317
322
  email = (
318
323
  rdf_value(contact_point, VCARD.hasEmail)
udata/routing.py CHANGED
@@ -77,6 +77,9 @@ class ModelConverter(BaseConverter):
77
77
  def has_redirected_slug(self):
78
78
  return self.has_slug and self.model.slug.follow
79
79
 
80
+ def get_excludes(self):
81
+ return []
82
+
80
83
  def quote(self, value):
81
84
  if self.has_slug:
82
85
  return self.model.slug.slugify(value)
@@ -85,13 +88,13 @@ class ModelConverter(BaseConverter):
85
88
 
86
89
  def to_python(self, value):
87
90
  try:
88
- return self.model.objects.get_or_404(id=value)
91
+ return self.model.objects.exclude(*self.get_excludes()).get_or_404(id=value)
89
92
  except (NotFound, ValidationError):
90
93
  pass
91
94
  try:
92
95
  quoted = self.quote(value)
93
96
  query = db.Q(slug=value) | db.Q(slug=quoted)
94
- obj = self.model.objects(query).get()
97
+ obj = self.model.objects(query).exclude(*self.get_excludes()).get()
95
98
  except (InvalidQueryError, self.model.DoesNotExist):
96
99
  # If the model doesn't have a slug or matching slug doesn't exist.
97
100
  if self.has_redirected_slug:
@@ -121,6 +124,13 @@ class DatasetConverter(ModelConverter):
121
124
  model = models.Dataset
122
125
 
123
126
 
127
+ class DatasetWithoutResourcesConverter(ModelConverter):
128
+ model = models.Dataset
129
+
130
+ def get_excludes(self):
131
+ return ["resources"]
132
+
133
+
124
134
  class DataserviceConverter(ModelConverter):
125
135
  model = Dataservice
126
136
 
@@ -226,6 +236,7 @@ def init_app(app):
226
236
  app.url_map.converters["pathlist"] = PathListConverter
227
237
  app.url_map.converters["uuid"] = UUIDConverter
228
238
  app.url_map.converters["dataset"] = DatasetConverter
239
+ app.url_map.converters["dataset_without_resources"] = DatasetWithoutResourcesConverter
229
240
  app.url_map.converters["dataservice"] = DataserviceConverter
230
241
  app.url_map.converters["crid"] = CommunityResourceConverter
231
242
  app.url_map.converters["org"] = OrganizationConverter
udata/settings.py CHANGED
@@ -570,6 +570,10 @@ class Defaults(object):
570
570
  ###########################################################################
571
571
  TABULAR_API_DATASERVICE_ID = None
572
572
 
573
+ # JSON-LD settings
574
+ ###########################################################################
575
+ MAX_RESOURCES_IN_JSON_LD = 20
576
+
573
577
 
574
578
  class Testing(object):
575
579
  """Sane values for testing. Should be applied as override"""