diff --git a/src/dso_api/dynamic_api/routers.py b/src/dso_api/dynamic_api/routers.py index 58ae28508..13cc570fd 100644 --- a/src/dso_api/dynamic_api/routers.py +++ b/src/dso_api/dynamic_api/routers.py @@ -52,7 +52,9 @@ DatasetMVTView, DatasetTileJSONView, DatasetWFSView, + OGCRecordDetailView, OGCRecordsCollectionDetailView, + OGCRecordsCollectionItemsView, OGCRecordsCollectionView, viewset_factory, ) @@ -580,6 +582,28 @@ def _build_ogc_views(self, datasets: Iterable[Dataset]) -> list[URLPattern]: }, ) ) + results.append( + path( + f"/ogc/records/collections/{dataset.path}/items", + OGCRecordsCollectionItemsView.as_view(), + name="ogc-collections-items", + kwargs={ + "dataset_name": dataset_id, + }, + ) + ) + for version, _ in dataset.schema.versions.items(): + results.append( + path( + f"/ogc/records/collections/{dataset.path}/items/{dataset_id}_{version}", + OGCRecordDetailView.as_view(), + name="ogc-record", + kwargs={ + "dataset_name": dataset_id, + "dataset_version": version, + }, + ) + ) return results diff --git a/src/dso_api/dynamic_api/views/__init__.py b/src/dso_api/dynamic_api/views/__init__.py index a447017af..40daa1efd 100644 --- a/src/dso_api/dynamic_api/views/__init__.py +++ b/src/dso_api/dynamic_api/views/__init__.py @@ -7,7 +7,9 @@ from .oauth import oauth2_redirect from .ogc import ( OGCIndexView, + OGCRecordDetailView, OGCRecordsCollectionDetailView, + OGCRecordsCollectionItemsView, OGCRecordsCollectionView, OGCRecordsIndexView, ) @@ -27,6 +29,8 @@ "OGCRecordsIndexView", "OGCRecordsCollectionView", "OGCRecordsCollectionDetailView", + "OGCRecordsCollectionItemsView", + "OGCRecordDetailView", "DocsIndexView", "oauth2_redirect", "viewset_factory", diff --git a/src/dso_api/dynamic_api/views/ogc.py b/src/dso_api/dynamic_api/views/ogc.py index d4eb66867..4f012f7da 100644 --- a/src/dso_api/dynamic_api/views/ogc.py +++ b/src/dso_api/dynamic_api/views/ogc.py @@ -1,5 +1,6 @@ import logging +from django.utils.functional import cached_property from django.views.generic import TemplateView from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -48,6 +49,7 @@ class OGCRecordsCollectionView(APIView): renderer_classes = [JSONRenderer] name = "Record Collections" + @cached_property def get_datasets(self): return [ ds @@ -57,7 +59,7 @@ def get_datasets(self): def get(self, request, *args, **kwargs): base = request.build_absolute_uri("/").rstrip("/") - datasets = self.get_datasets() + datasets = self.get_datasets collections = [] @@ -81,11 +83,12 @@ def get(self, request, *args, **kwargs): class OGCRecordsCollectionDetailView(APIView): - """An JSON page for a single OGC record collection (dataset).""" + """A JSON page for a single OGC record collection (dataset).""" renderer_classes = [JSONRenderer] name = "Records" + @cached_property def get_datasets(self): return [ ds @@ -95,7 +98,7 @@ def get_datasets(self): def get(self, request, *args, **kwargs): base = request.build_absolute_uri("/").rstrip("/") - datasets = self.get_datasets() + datasets = self.get_datasets dataset_name = self.kwargs.get("dataset_name") collection_url = f"{base}/v1/ogc/records/collections/{dataset_name}" records = [] @@ -110,13 +113,14 @@ def get(self, request, *args, **kwargs): self.build_record_collection( ds=ds, version=version, + base=base, collection_url=collection_url, ) ) return Response({"records": records}) - def build_record_collection(self, ds, version, collection_url): + def build_record_collection(self, ds, version, base, collection_url): """Handle missing attributes and ensure each OGC field has the expected datatype.""" metadata = { "id": f"{ds.schema.id}_{version}", @@ -137,6 +141,16 @@ def build_record_collection(self, ds, version, collection_url): ], } + for version, _ in ds.schema.versions.items(): + metadata["links"].append( + { + "rel": f"{version}", + "type": "application/json", + "href": f"{base}/v1/ogc/records/collections/{ds.schema.id}/items" + f"/{ds.schema.id}_{version}", + } + ) + if ds.schema.get("dateCreated"): metadata["created"] = ds.schema.get("dateCreated") @@ -158,3 +172,122 @@ def build_record_collection(self, ds, version, collection_url): metadata["license"] = ds.schema.license return [metadata] + + +class OGCRecordsCollectionItemsView(APIView): + """Shows a JSON page with an overview of record collection items.""" + + renderer_classes = [JSONRenderer] + name = "Collection Items" + + @cached_property + def get_datasets(self): + return [ + ds + for ds in get_active_datasets().db_enabled().order_by("name") + if ds.has_geometry_fields + ] + + def get(self, request, *args, **kwargs): + base = request.build_absolute_uri("/").rstrip("/") + datasets = self.get_datasets + dataset_name = self.kwargs.get("dataset_name") + + ds = next( + (dataset for dataset in datasets if str(dataset.schema.id) == str(dataset_name)), + None, + ) + + items = [] + + for version, _ in ds.schema.versions.items(): + items.append( + { + "id": f"{ds.schema.id}_{version}", + "title": ds.schema.title or ds.name, + "description": ds.schema.description or "", + "links": [ + { + "rel": "self", + "type": "application/json", + "href": (f"{base}/v1/ogc/records/collections/{ds.schema.id}/items"), + }, + { + "rel": f"{version}", + "type": "application/json", + "href": f"{base}/v1/ogc/records/collections/{ds.schema.id}/items" + f"/{ds.schema.id}_{version}", + }, + ], + } + ) + + return Response({"items": items}) + + +class OGCRecordDetailView(APIView): + """A JSON page for a single OGC record (datasetversion).""" + + renderer_classes = [JSONRenderer] + name = "Record" + + @cached_property + def get_datasets(self): + return [ + ds + for ds in get_active_datasets().db_enabled().order_by("name") + if ds.has_geometry_fields + ] + + def get(self, request, *args, **kwargs): + base = request.build_absolute_uri("/").rstrip("/") + datasets = self.get_datasets + dataset_name = self.kwargs.get("dataset_name") + dataset_version = self.kwargs.get("dataset_version") + record_url = ( + f"{base}/v1/ogc/records/collections/{dataset_name}/items" + f"/{dataset_name}_{dataset_version}" + ) + + wfs_url = ( + f"{base}/v1/wfs/{dataset_name}/{dataset_version}?service=WFS&request=GetCapabilities" + ) + + ds = next( + (dataset for dataset in datasets if str(dataset.schema.id) == str(dataset_name)), + None, + ) + + record = self.build_record( + ds=ds, + version=dataset_version, + record_url=record_url, + wfs_url=wfs_url, + ) + + return Response({"record": record}) + + def build_record(self, ds, version, record_url, wfs_url): + """Handle missing attributes and ensure each OGC field has the expected datatype.""" + metadata = { + "id": f"{ds.schema.id}_{version}", + "conformsTo": [], + "language": {"code": "nl", "name": "Dutch (Netherlands)"}, + "links": [ + { + "rel": "self", + "type": "application/json", + "href": record_url, + }, + { + "rel": "WFS", + "type": "application/json", + "href": wfs_url, + }, + ], + } + + if ds.schema.get("dateCreated"): + metadata["created"] = ds.schema.get("dateCreated") + + return metadata diff --git a/src/templates/dso_api/dynamic_api/ogc_records.html b/src/templates/dso_api/dynamic_api/ogc_records.html index f8cc63a8a..c0a66b626 100644 --- a/src/templates/dso_api/dynamic_api/ogc_records.html +++ b/src/templates/dso_api/dynamic_api/ogc_records.html @@ -12,7 +12,7 @@ op een gestandaardiseerde manier beschikbaar te stellen. Iedere DSO-dataset wordt ontsloten als een record collection, met metadata over deze dataset uit het Amsterdam Schema en de beschikbare datasetversies. Iedere datasetversie wordt vervolgens beschreven door een afzonderlijk OGC-record met de bijbehorende - metadata en links naar de relevante WFS- en WMS-services. Hierdoor kunnen kaartviewers zoals NL3D en + metadata en links naar de relevante WFS-services. Hierdoor kunnen kaartviewers zoals NL3D en Geosquare deze datasets en datasetversies op een uniforme manier ontdekken en de bijbehorende geografische gegevens raadplegen.

diff --git a/src/tests/test_dynamic_api/views/test_ogc.py b/src/tests/test_dynamic_api/views/test_ogc.py index 80f113909..edf95d20b 100644 --- a/src/tests/test_dynamic_api/views/test_ogc.py +++ b/src/tests/test_dynamic_api/views/test_ogc.py @@ -59,6 +59,16 @@ def test_ogc_view(self, api_client, afval_container): "type": "application/json", "href": "http://testserver/v1/ogc/records/collections/afvalwegingen", }, + { + "rel": "v1", + "type": "application/json", + "href": "http://testserver/v1/ogc/records/collections/afvalwegingen/items/afvalwegingen_v1", + }, + { + "rel": "v2", + "type": "application/json", + "href": "http://testserver/v1/ogc/records/collections/afvalwegingen/items/afvalwegingen_v2", + }, ], "contacts": [ { @@ -90,6 +100,16 @@ def test_ogc_view(self, api_client, afval_container): "type": "application/json", "href": "http://testserver/v1/ogc/records/collections/afvalwegingen", }, + { + "rel": "v1", + "type": "application/json", + "href": "http://testserver/v1/ogc/records/collections/afvalwegingen/items/afvalwegingen_v1", + }, + { + "rel": "v2", + "type": "application/json", + "href": "http://testserver/v1/ogc/records/collections/afvalwegingen/items/afvalwegingen_v2", + }, ], "contacts": [ { @@ -104,3 +124,42 @@ def test_ogc_view(self, api_client, afval_container): }, ], } + + +@pytest.mark.django_db +class TestOGCRecordsCollectionItemsView: + def test_ogc_items_view(self, api_client, afval_container): + ogc_url = "/v1/ogc/records/collections/afvalwegingen/items" + response = api_client.get(ogc_url) + json = response.json() + assert response.status_code == 200, response.content + assert len(json["items"]) == 2 + assert json["items"][0]["id"] == "afvalwegingen_v1" + assert json["items"][1]["id"] == "afvalwegingen_v2" + + +@pytest.mark.django_db +class TestOGCRecordDetailView: + def test_ogc_record_view(self, api_client, afval_container): + ogc_url = "/v1/ogc/records/collections/afvalwegingen/items/afvalwegingen_v1" + response = api_client.get(ogc_url) + assert response.status_code == 200, response.content + assert response.data == { + "record": { + "id": "afvalwegingen_v1", + "conformsTo": [], + "language": {"code": "nl", "name": "Dutch (Netherlands)"}, + "links": [ + { + "rel": "self", + "type": "application/json", + "href": "http://testserver/v1/ogc/records/collections/afvalwegingen/items/afvalwegingen_v1", + }, + { + "rel": "WFS", + "type": "application/json", + "href": "http://testserver/v1/wfs/afvalwegingen/v1?service=WFS&request=GetCapabilities", + }, + ], + }, + }