From 92bf8eb940c6fcdbf0986f9e904a49a32163dfdb Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Wed, 24 Jun 2026 18:25:13 -0400 Subject: [PATCH 1/9] Minor coverages improvements --- pygeoapi/api/collection.py | 25 +- pygeoapi/api/coverages.py | 44 ++- .../collections/coverages/coverage.html | 271 ++++++++++++++++++ 3 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 pygeoapi/templates/collections/coverages/coverage.html diff --git a/pygeoapi/api/collection.py b/pygeoapi/api/collection.py index 3b1f24922..3cf6ae13a 100644 --- a/pygeoapi/api/collection.py +++ b/pygeoapi/api/collection.py @@ -228,13 +228,7 @@ def gen_collection(api, request, dataset: str, 'rel': f'{OGC_RELTYPES_BASE}/schema', 'title': l10n.translate('Schema of collection in HTML', locale_), 'href': f'{api.get_collections_url()}/{dataset}/schema?f={F_HTML}' - }]) - - if is_vector_tile or collection_data_type in ['feature', 'record']: - # TODO: translate - data['itemType'] = collection_data_type - LOGGER.debug('Adding feature/record based links') - data['links'].extend([{ + },{ 'type': 'application/schema+json', 'rel': f'{OGC_RELTYPES_BASE}/queryables', 'title': l10n.translate('Queryables for this collection as JSON', locale_), # noqa @@ -244,7 +238,13 @@ def gen_collection(api, request, dataset: str, 'rel': f'{OGC_RELTYPES_BASE}/queryables', 'title': l10n.translate('Queryables for this collection as HTML', locale_), # noqa 'href': f'{api.get_collections_url()}/{dataset}/queryables?f={F_HTML}' # noqa - }, { + }]) + + if is_vector_tile or collection_data_type in ['feature', 'record']: + # TODO: translate + data['itemType'] = collection_data_type + LOGGER.debug('Adding feature/record based links') + data['links'].extend([{ 'type': 'application/geo+json', 'rel': 'items', 'title': l10n.translate('Items as GeoJSON', locale_), @@ -278,12 +278,17 @@ def gen_collection(api, request, dataset: str, elif collection_data_type == 'coverage': LOGGER.debug('Adding coverage based links') - data['links'].append({ + data['links'].extend([{ 'type': 'application/prs.coverage+json', 'rel': f'{OGC_RELTYPES_BASE}/coverage', 'title': l10n.translate('Coverage data', locale_), 'href': f'{api.get_collections_url()}/{dataset}/coverage?f={F_JSON}' # noqa - }) + }, { + 'type': 'text/html', + 'rel': f'{OGC_RELTYPES_BASE}/coverage', + 'title': l10n.translate('Coverage data', locale_), + 'href': f'{api.get_collections_url()}/{dataset}/coverage?f={F_HTML}' # noqa + }]) if collection_data_format is not None: title_ = l10n.translate('Coverage data as', locale_) title_ = f"{title_} {collection_data_format['name']}" diff --git a/pygeoapi/api/coverages.py b/pygeoapi/api/coverages.py index 67755407d..a114c74f7 100644 --- a/pygeoapi/api/coverages.py +++ b/pygeoapi/api/coverages.py @@ -41,14 +41,15 @@ import logging from http import HTTPStatus from typing import Tuple +import urllib from pygeoapi import l10n -from pygeoapi.formats import F_JSON +from pygeoapi.formats import F_JSON, F_COVERAGEJSON, F_HTML from pygeoapi.openapi import get_oas_30_parameters from pygeoapi.plugin import load_plugin from pygeoapi.provider.base import ProviderGenericError, ProviderTypeError from pygeoapi.provider import get_provider_by_type -from pygeoapi.util import filter_dict_by_key_value, to_json +from pygeoapi.util import filter_dict_by_key_value, to_json, render_j2_template from . import ( APIRequest, API, SYSTEM_LOCALE, validate_bbox, validate_datetime, @@ -86,6 +87,8 @@ def get_collection_coverage( # Force response content type and language (en-US only) headers headers = request.get_response_headers(SYSTEM_LOCALE, **api.api_headers) + collections = filter_dict_by_key_value(api.config['resources'], + 'type', 'collection') LOGGER.debug('Loading provider') try: @@ -140,7 +143,9 @@ def get_collection_coverage( 'InvalidParameterValue', msg) query_args['datetime_'] = datetime_ - query_args['format_'] = format_ + + if request.format == F_HTML: + query_args['format_'] = F_COVERAGEJSON properties = request.params.get('properties') if properties: @@ -191,6 +196,39 @@ def get_collection_coverage( headers['Content-Type'] = collection_def['format']['mimetype'] return headers, HTTPStatus.OK, data + if request.format == F_HTML: # render + tpl_config = api.get_dataset_templates(dataset) + + uri = f'{api.get_collections_url()}/{dataset}/coverage' + serialized_query_params = '' + for k, v in request.params.items(): + if k != 'f': + serialized_query_params += '&' + serialized_query_params += urllib.parse.quote(k, safe='') + serialized_query_params += '=' + serialized_query_params += urllib.parse.quote(str(v), safe=',') + + data['query_path'] = uri + data['dataset_path'] = '/'.join(uri.split('/')[:-1]) + data['collections_path'] = api.get_collections_url() + + data['links'] = [{ + 'rel': 'collection', + 'title': collections[dataset]['title'], + 'href': data['dataset_path'] + },{ + 'type': 'application/vnd.cov+json', + 'rel': request.get_linkrel(F_COVERAGEJSON), + 'title': l10n.translate('This document as CoverageJSON', request.locale), # noqa + 'href': f'{uri}?f={F_COVERAGEJSON}{serialized_query_params}' + }] + + content = render_j2_template(api.tpl_config, tpl_config, + 'collections/coverages/coverage.html', data, + api.default_locale) + + return headers, HTTPStatus.OK, content + elif format_ == F_JSON: headers['Content-Type'] = 'application/prs.coverage+json' return headers, HTTPStatus.OK, to_json(data, api.pretty_print) diff --git a/pygeoapi/templates/collections/coverages/coverage.html b/pygeoapi/templates/collections/coverages/coverage.html new file mode 100644 index 000000000..59cc2afc4 --- /dev/null +++ b/pygeoapi/templates/collections/coverages/coverage.html @@ -0,0 +1,271 @@ +{% extends "_base.html" %} +{% block title %}{{ super() }} {{ data['title'] }} {% endblock %} +{% block crumbs %}{{ super() }} +/ {% trans %}Collections{% endtrans %} +{% for link in data['links'] %} + {% if link.rel == 'collection' %} / + {{ link['title'] | truncate( 25 ) }} + {% set col_title = link['title'] %} + {% endif %} +{% endfor %} +/ {% trans query_type=data.query_type %}Coverage{% endtrans %} +{% endblock %} +{% block extrahead %} + + + + {% if data.type == "Coverage" or data.type == "CoverageCollection" %} + + + + + + + {% elif data.type == "Feature" or data.type == "FeatureCollection" %} + + + + {% endif %} +{% endblock %} + +{% block body %} +
+ {% if data.features or data.coverages or data.ranges or data.references %} +
+ {% else %} +
+

{% trans %}No items{% endtrans %}

+
+ {% endif %} +
+{% endblock %} + +{% block extrafoot %} +{% if data %} + +{% endif %} +{% endblock %} From abc70384d3e01dd2e728a8544a19c54634296761 Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Wed, 24 Jun 2026 19:09:29 -0400 Subject: [PATCH 2/9] Fix tests --- pygeoapi/api/collection.py | 2 +- pygeoapi/api/coverages.py | 12 +++++++----- tests/api/test_coverages.py | 9 +++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pygeoapi/api/collection.py b/pygeoapi/api/collection.py index 3cf6ae13a..373047dd8 100644 --- a/pygeoapi/api/collection.py +++ b/pygeoapi/api/collection.py @@ -228,7 +228,7 @@ def gen_collection(api, request, dataset: str, 'rel': f'{OGC_RELTYPES_BASE}/schema', 'title': l10n.translate('Schema of collection in HTML', locale_), 'href': f'{api.get_collections_url()}/{dataset}/schema?f={F_HTML}' - },{ + }, { 'type': 'application/schema+json', 'rel': f'{OGC_RELTYPES_BASE}/queryables', 'title': l10n.translate('Queryables for this collection as JSON', locale_), # noqa diff --git a/pygeoapi/api/coverages.py b/pygeoapi/api/coverages.py index a114c74f7..4b1dda586 100644 --- a/pygeoapi/api/coverages.py +++ b/pygeoapi/api/coverages.py @@ -143,8 +143,10 @@ def get_collection_coverage( 'InvalidParameterValue', msg) query_args['datetime_'] = datetime_ + query_args['format_'] = format_ if request.format == F_HTML: + # Request as JSON to render HTML query_args['format_'] = F_COVERAGEJSON properties = request.params.get('properties') @@ -196,7 +198,7 @@ def get_collection_coverage( headers['Content-Type'] = collection_def['format']['mimetype'] return headers, HTTPStatus.OK, data - if request.format == F_HTML: # render + elif format_ == F_HTML: # render tpl_config = api.get_dataset_templates(dataset) uri = f'{api.get_collections_url()}/{dataset}/coverage' @@ -216,7 +218,7 @@ def get_collection_coverage( 'rel': 'collection', 'title': collections[dataset]['title'], 'href': data['dataset_path'] - },{ + }, { 'type': 'application/vnd.cov+json', 'rel': request.get_linkrel(F_COVERAGEJSON), 'title': l10n.translate('This document as CoverageJSON', request.locale), # noqa @@ -224,9 +226,9 @@ def get_collection_coverage( }] content = render_j2_template(api.tpl_config, tpl_config, - 'collections/coverages/coverage.html', data, - api.default_locale) - + 'collections/coverages/coverage.html', + data, api.default_locale) + return headers, HTTPStatus.OK, content elif format_ == F_JSON: diff --git a/tests/api/test_coverages.py b/tests/api/test_coverages.py index bea6ac612..0ea2372ad 100644 --- a/tests/api/test_coverages.py +++ b/tests/api/test_coverages.py @@ -57,7 +57,7 @@ def test_describe_collections(config, api_): collection = json.loads(response) assert collection['id'] == 'gdps-temperature' - assert len(collection['links']) == 10 + assert len(collection['links']) == 13 assert collection['extent']['spatial']['grid'][0]['cellsCount'] == 2400 assert collection['extent']['spatial']['grid'][0]['resolution'] == 0.15000000000000002 # noqa assert collection['extent']['spatial']['grid'][1]['cellsCount'] == 1201 @@ -122,17 +122,14 @@ def test_get_collection_coverage(config, api_): rsp_headers, code, response = get_collection_coverage( api_, req, 'gdps-temperature') - assert code == HTTPStatus.BAD_REQUEST + assert code == HTTPStatus.OK assert rsp_headers['Content-Type'] == 'text/html' req = mock_api_request(HTTP_ACCEPT='text/html') rsp_headers, code, response = get_collection_coverage( api_, req, 'gdps-temperature') - # NOTE: This test used to assert the code to be 200 OK, - # but it requested HTML, which is not available, - # so it should be 400 Bad Request - assert code == HTTPStatus.BAD_REQUEST + assert code == HTTPStatus.OK assert rsp_headers['Content-Type'] == 'text/html' req = mock_api_request({'subset': 'Lat(5:10),Long(5:10)'}) From 0ad79dcd8b9e32e22bad4a5b30c2aed9f4efc7ca Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Wed, 24 Jun 2026 19:12:33 -0400 Subject: [PATCH 3/9] Advertise Coverage at collection level HTML --- pygeoapi/api/collection.py | 2 +- .../templates/collections/collection.html | 29 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pygeoapi/api/collection.py b/pygeoapi/api/collection.py index 373047dd8..1958f33f7 100644 --- a/pygeoapi/api/collection.py +++ b/pygeoapi/api/collection.py @@ -218,6 +218,7 @@ def gen_collection(api, request, dataset: str, }]) if collection_data_type in ['feature', 'coverage', 'record']: + data['itemType'] = collection_data_type data['links'].extend([{ 'type': 'application/schema+json', 'rel': f'{OGC_RELTYPES_BASE}/schema', @@ -242,7 +243,6 @@ def gen_collection(api, request, dataset: str, if is_vector_tile or collection_data_type in ['feature', 'record']: # TODO: translate - data['itemType'] = collection_data_type LOGGER.debug('Adding feature/record based links') data['links'].extend([{ 'type': 'application/geo+json', diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html index 2b3904dcc..4cbd7ee33 100644 --- a/pygeoapi/templates/collections/collection.html +++ b/pygeoapi/templates/collections/collection.html @@ -49,13 +49,18 @@

{% trans %}License{% endtrans %}

{% endif %} {% endfor %} - {% if data['itemType'] == 'feature' or data['itemType'] == 'record' %} + {% if data['itemType'] == 'feature' or data['itemType'] == 'record' or data['itemType'] == 'coverage' %}

{% trans %}Browse{% endtrans %}

{% trans %}Queryables{% endtrans %}

@@ -74,20 +79,20 @@

{% trans %}Schema{% endtrans %}

{% trans %}Display Schema of{% endtrans %} "{{ data['title'] }}" + {% endif %} + {% for provider in config['resources'][data['id']]['providers'] %} {% if 'tile' in provider['type'] %} -

{% trans %}Tiles{% endtrans %}

- - {% endif %} +

{% trans %}Tiles{% endtrans %}

+ + {% endif %} {% endfor %} - {% endif %} - {%- if data['data_queries'] -%}

Data Queries

From 97d85bdb75a9f714e0c6ef5f8f8be4aae25a774b Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 25 Jun 2026 09:35:53 -0400 Subject: [PATCH 4/9] Implement PR feedback --- pygeoapi/api/collection.py | 2 +- .../templates/collections/collection.html | 35 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pygeoapi/api/collection.py b/pygeoapi/api/collection.py index 1958f33f7..2cd8b3ea6 100644 --- a/pygeoapi/api/collection.py +++ b/pygeoapi/api/collection.py @@ -218,7 +218,6 @@ def gen_collection(api, request, dataset: str, }]) if collection_data_type in ['feature', 'coverage', 'record']: - data['itemType'] = collection_data_type data['links'].extend([{ 'type': 'application/schema+json', 'rel': f'{OGC_RELTYPES_BASE}/schema', @@ -244,6 +243,7 @@ def gen_collection(api, request, dataset: str, if is_vector_tile or collection_data_type in ['feature', 'record']: # TODO: translate LOGGER.debug('Adding feature/record based links') + data['itemType'] = collection_data_type data['links'].extend([{ 'type': 'application/geo+json', 'rel': 'items', diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html index 4cbd7ee33..982333c8d 100644 --- a/pygeoapi/templates/collections/collection.html +++ b/pygeoapi/templates/collections/collection.html @@ -49,18 +49,13 @@

{% trans %}License{% endtrans %}

{% endif %} {% endfor %} - {% if data['itemType'] == 'feature' or data['itemType'] == 'record' or data['itemType'] == 'coverage' %} + {% if data['itemType'] == 'feature' or data['itemType'] == 'record' %}

{% trans %}Browse{% endtrans %}

{% trans %}Queryables{% endtrans %}

@@ -92,6 +87,34 @@

{% trans %}Tiles{% endtrans %}

{% endif %} + + {% if 'coverage' in provider['type'] %} +

{% trans %}Coverage{% endtrans %}

+ +

{% trans %}Queryables{% endtrans %}

+ +

{% trans %}Schema{% endtrans %}

+ + {% endif %} + {% endfor %} {%- if data['data_queries'] -%}

Data Queries

From 592f248603ff5048d0951ca883c76240b53e60ba Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 25 Jun 2026 09:50:31 -0400 Subject: [PATCH 5/9] Remove queryables advertisement --- pygeoapi/templates/collections/collection.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html index 982333c8d..c5e276baf 100644 --- a/pygeoapi/templates/collections/collection.html +++ b/pygeoapi/templates/collections/collection.html @@ -97,14 +97,6 @@

{% trans %}Coverage{% endtrans %}

{% trans %}Display coverage of{% endtrans %} "{{ data['title'] }}" -

{% trans %}Queryables{% endtrans %}

-

{% trans %}Schema{% endtrans %}

  • From aff72ad27623d6f80cc0317f9f9afcb53e76af6c Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 25 Jun 2026 09:58:32 -0400 Subject: [PATCH 6/9] Fix capitalization --- pygeoapi/templates/collections/collection.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html index c5e276baf..2dfeb83d4 100644 --- a/pygeoapi/templates/collections/collection.html +++ b/pygeoapi/templates/collections/collection.html @@ -94,7 +94,7 @@

    {% trans %}Coverage{% endtrans %}

  • + {% trans %}Display Coverage of{% endtrans %} "{{ data['title'] }}"

{% trans %}Schema{% endtrans %}

From 248d35cd27c12685cd5efe7119e3588662d1dfb7 Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 25 Jun 2026 10:26:06 -0400 Subject: [PATCH 7/9] Add trans --- locale/en/LC_MESSAGES/messages.po | 12 ++++++++++++ pygeoapi/templates/collections/collection.html | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/locale/en/LC_MESSAGES/messages.po b/locale/en/LC_MESSAGES/messages.po index 34ad8c485..fab76819a 100644 --- a/locale/en/LC_MESSAGES/messages.po +++ b/locale/en/LC_MESSAGES/messages.po @@ -485,6 +485,18 @@ msgstr "" msgid "Display Tiles of" msgstr "" +#: pygeoapi/templates/collections/collection.html:92 +msgid "Coverages" +msgstr "" + +#: pygeoapi/templates/collections/collection.html:96 +msgid "Display Coverage" +msgstr "" + +#: pygeoapi/templates/collections/collection.html:97 +msgid "Display Coverage of" +msgstr "" + #: pygeoapi/templates/collections/index.html:13 msgid "Type" msgstr "" diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html index 2dfeb83d4..4cfbfca0b 100644 --- a/pygeoapi/templates/collections/collection.html +++ b/pygeoapi/templates/collections/collection.html @@ -93,7 +93,7 @@

{% trans %}Coverage{% endtrans %}

From 84ec28976fc71c05c5460a46cfc0a84dda098928 Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 25 Jun 2026 15:03:00 -0400 Subject: [PATCH 8/9] Rename coverage.html --- pygeoapi/api/coverages.py | 2 +- .../collections/coverages/{coverage.html => index.html} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pygeoapi/templates/collections/coverages/{coverage.html => index.html} (100%) diff --git a/pygeoapi/api/coverages.py b/pygeoapi/api/coverages.py index 4b1dda586..40b618cf7 100644 --- a/pygeoapi/api/coverages.py +++ b/pygeoapi/api/coverages.py @@ -226,7 +226,7 @@ def get_collection_coverage( }] content = render_j2_template(api.tpl_config, tpl_config, - 'collections/coverages/coverage.html', + 'collections/coverages/index.html', data, api.default_locale) return headers, HTTPStatus.OK, content diff --git a/pygeoapi/templates/collections/coverages/coverage.html b/pygeoapi/templates/collections/coverages/index.html similarity index 100% rename from pygeoapi/templates/collections/coverages/coverage.html rename to pygeoapi/templates/collections/coverages/index.html From b3fb127cc6f48a8f27c2a963a5768e3af79e97cd Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Thu, 25 Jun 2026 16:15:15 -0400 Subject: [PATCH 9/9] coverages -> coverage --- pygeoapi/api/coverages.py | 2 +- .../templates/collections/{coverages => coverage}/index.html | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pygeoapi/templates/collections/{coverages => coverage}/index.html (100%) diff --git a/pygeoapi/api/coverages.py b/pygeoapi/api/coverages.py index 40b618cf7..2564071e8 100644 --- a/pygeoapi/api/coverages.py +++ b/pygeoapi/api/coverages.py @@ -226,7 +226,7 @@ def get_collection_coverage( }] content = render_j2_template(api.tpl_config, tpl_config, - 'collections/coverages/index.html', + 'collections/coverage/index.html', data, api.default_locale) return headers, HTTPStatus.OK, content diff --git a/pygeoapi/templates/collections/coverages/index.html b/pygeoapi/templates/collections/coverage/index.html similarity index 100% rename from pygeoapi/templates/collections/coverages/index.html rename to pygeoapi/templates/collections/coverage/index.html