diff --git a/CHANGES.rst b/CHANGES.rst index 81229cef9..674d6b45a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,13 @@ Changes Changes: -------- +- Add `HTML` shortcut links to easily toggle contextually between full `Provider` or only local `Process` listing. +- Add `HTML` support of ``/providers/{providerID}/processes/?f=html`` to list all `Process` references of a `Provider`. +- Add `HTML` support of ``/processes/?providers=true`` to list all `Provider` and local `Process` references. +- Add ``?provider={providerID}`` and ``?service={providerID}`` query parameters to ``/processes/{processID}`` request + as alias to resolve the corresponding ``/providers/{providerID}/processes/{processID}`` resource. +- Add ``provider`` details embedded within the ``/processes/{processID}`` response if ``?provider={providerID}`` + or ``?service={providerID}`` query parameter is specified and that the `Provider`'s `Process` can be resolved. - Add links and breadcrumbs including the `Process` ID within `HTML` pages of `Job` responses if referenced by the request. - Add the full URI definitions for ``Accept-Profile`` header that correspond to equivalent shorthand notation diff --git a/tests/test_datatype.py b/tests/test_datatype.py index 9f3b01757..8327b32e8 100644 --- a/tests/test_datatype.py +++ b/tests/test_datatype.py @@ -3,14 +3,15 @@ from datetime import datetime, timedelta import pytest -from visibility import Visibility from tests import resources from weaver.datatype import Authentication, AuthenticationTypes, DockerAuthentication, Job, Process, Service from weaver.execute import ExecuteControlOption, ExecuteMode, ExecuteResponse, ExecuteReturnPreference from weaver.formats import ContentType +from weaver.processes.types import ProcessType from weaver.status import Status from weaver.utils import localize_datetime, now +from weaver.visibility import Visibility TEST_UUID = uuid.uuid4() @@ -215,6 +216,11 @@ def test_process_split_version(process_id, result): assert Process.split_version(process_id) == result +def test_process_cleanup_keywords(): + proc = Process(id="test", type=ProcessType.WPS_REMOTE, keywords=[" PyWPS ", " WPS", " OGC", " demo "], package={}) + assert proc.keywords == ["PyWPS", "WPS", "OGC", "demo", ProcessType.WPS_REMOTE] + + @pytest.mark.parametrize( ["attribute", "value", "result"], [ diff --git a/tests/wps_restapi/test_processes.py b/tests/wps_restapi/test_processes.py index 476ade2b8..e173af9d7 100644 --- a/tests/wps_restapi/test_processes.py +++ b/tests/wps_restapi/test_processes.py @@ -640,6 +640,7 @@ def test_get_processes_invalid_schemas_handled(self): assert resp.content_type == ContentType.APP_JSON assert process_name in resp.json.get("description") + @pytest.mark.html def test_get_processes_html_accept_header(self): path = "/processes" resp = self.app.get(path, headers=self.html_headers) @@ -649,6 +650,7 @@ def test_get_processes_html_accept_header(self): assert "" in resp.text assert "Processes" in resp.text + @pytest.mark.html def test_get_processes_html_format_query(self): path = "/processes" resp = self.app.get(path, params={"f": OutputFormat.HTML}) @@ -658,6 +660,7 @@ def test_get_processes_html_format_query(self): assert "" in resp.text assert "Processes" in resp.text + @pytest.mark.html def test_describe_process_html_accept_header(self): path = f"/processes/{self.process_public.identifier}" resp = self.app.get(path, headers=self.html_headers) @@ -668,6 +671,7 @@ def test_describe_process_html_accept_header(self): assert "Process:" in resp.text assert self.process_public.identifier in resp.text + @pytest.mark.html def test_describe_process_html_format_query(self): path = f"/processes/{self.process_public.identifier}" resp = self.app.get(path, params={"f": OutputFormat.HTML}) @@ -678,6 +682,7 @@ def test_describe_process_html_format_query(self): assert "Process:" in resp.text assert self.process_public.identifier in resp.text + @pytest.mark.html def test_get_processes_html_accept_header_user_agent_browser_disabled(self): path = "/processes" headers = copy.deepcopy(dict(self.html_headers)) @@ -689,6 +694,7 @@ def test_get_processes_html_accept_header_user_agent_browser_disabled(self): assert "" in resp.text assert "Processes" in resp.text + @pytest.mark.html def test_get_processes_html_accept_header_user_agent_browser_override(self): path = "/processes" headers = copy.deepcopy(dict(self.html_headers)) @@ -2776,6 +2782,7 @@ def test_execute_process_nested_invalid_results_amount(self, test_outputs, mock_ # pylint: disable=C0103,invalid-name @pytest.mark.functional +@pytest.mark.html class WpsRestApiProcessesNoHTMLTest(WpsConfigBase): settings = { "weaver.url": "https://localhost", diff --git a/tests/wps_restapi/test_providers.py b/tests/wps_restapi/test_providers.py index f3f1d248d..dd62d7ffe 100644 --- a/tests/wps_restapi/test_providers.py +++ b/tests/wps_restapi/test_providers.py @@ -16,7 +16,7 @@ from weaver.config import WeaverConfiguration from weaver.datatype import Service from weaver.execute import ExecuteControlOption, ExecuteTransmissionMode -from weaver.formats import ContentType +from weaver.formats import ContentType, OutputFormat from weaver.processes.constants import ProcessSchema @@ -43,6 +43,7 @@ def setUpClass(cls): cls.config = setup_config_with_mongodb(settings=cls.settings) cls.app = get_test_weaver_app(config=cls.config) cls.json_headers = {"Accept": ContentType.APP_JSON, "Content-Type": ContentType.APP_JSON} + cls.html_headers = {"Accept": ContentType.TEXT_HTML} def setUp(self): # rebuild clean db on each test @@ -287,6 +288,36 @@ def test_get_provider_processes(self): remote_processes.append(process["id"]) assert resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID in remote_processes + @mocked_remote_server_requests_wps1([ + resources.TEST_REMOTE_SERVER_URL, + resources.TEST_REMOTE_SERVER_WPS1_GETCAP_XML, + [resources.TEST_REMOTE_SERVER_WPS1_DESCRIBE_PROCESS_XML], + ]) + def test_get_provider_processes_with_providers_query(self): + self.register_provider() + + path = "/processes" + query = {"providers": True} + resp = self.app.get(path, headers=self.json_headers, params=query) + assert resp.status_code == 200 + assert resp.content_type == ContentType.APP_JSON + assert "providers" in resp.json and isinstance(resp.json["providers"], list) + assert len(resp.json["providers"]) == 1 + provider = resp.json["providers"][0] + assert "processes" in provider and isinstance(provider["processes"], list) + assert len(provider["processes"]) == 2 + remote_processes = [] + for process in resp.json["processes"]: + assert "id" in process and isinstance(process["id"], str) + assert "title" in process and isinstance(process["title"], str) + assert "version" in process and isinstance(process["version"], str) + assert "keywords" in process and isinstance(process["keywords"], list) + assert "metadata" in process and isinstance(process["metadata"], list) + assert len(process["jobControlOptions"]) == 1 + assert ExecuteControlOption.ASYNC in process["jobControlOptions"] + remote_processes.append(process["id"]) + assert resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID in remote_processes + @pytest.mark.xfail(condition=Version(owslib.__version__) <= Version("0.25.0"), reason="OWSLib fix for retrieval of processVersion from DescribeProcess not yet available " "(https://github.com/geopython/OWSLib/pull/794)") @@ -390,6 +421,58 @@ def test_get_provider_process_description_ogc_schema(self): assert all(isinstance(p_io, str) and isinstance(process["outputs"][p_io], dict) for p_io in process["outputs"]) assert all("id" not in process["outputs"][p_io] for p_io in process["outputs"]) + @pytest.mark.html + @mocked_remote_server_requests_wps1([ + resources.TEST_REMOTE_SERVER_URL, + resources.TEST_REMOTE_SERVER_WPS1_GETCAP_XML, + [resources.TEST_REMOTE_SERVER_WPS1_DESCRIBE_PROCESS_XML], + ]) + def test_get_provider_process_description_html(self): + self.register_provider() + + path = f"/providers/{self.remote_provider_name}/processes/{resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID}" + resp = self.app.get(path, headers=self.html_headers) + assert resp.status_code == 200 + assert resp.content_type == ContentType.TEXT_HTML + assert resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID in resp.text + assert self.remote_provider_name in resp.text + + @pytest.mark.html + @mocked_remote_server_requests_wps1([ + resources.TEST_REMOTE_SERVER_URL, + resources.TEST_REMOTE_SERVER_WPS1_GETCAP_XML, + [resources.TEST_REMOTE_SERVER_WPS1_DESCRIBE_PROCESS_XML], + ]) + def test_get_provider_process_description_html_with_provider_query_ogc_schema(self): + self.register_provider() + + path = f"/processes/{resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID}" + query = {"provider": self.remote_provider_name, "f": OutputFormat.HTML} + resp = self.app.get(path, params=query) + assert resp.status_code == 200 + assert resp.content_type == ContentType.TEXT_HTML + assert resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID in resp.text + assert self.remote_provider_name in resp.text + assert resources.TEST_REMOTE_SERVER_URL in resp.text + + @pytest.mark.html + @mocked_remote_server_requests_wps1([ + resources.TEST_REMOTE_SERVER_URL, + resources.TEST_REMOTE_SERVER_WPS1_GETCAP_XML, + [resources.TEST_REMOTE_SERVER_WPS1_DESCRIBE_PROCESS_XML], + ]) + def test_get_provider_process_description_html_with_provider_query_old_schema(self): + self.register_provider() + + path = f"/processes/{resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID}" + query = {"provider": self.remote_provider_name, "f": OutputFormat.HTML, "schema": ProcessSchema.OLD} + resp = self.app.get(path, params=query) + assert resp.status_code == 200 + assert resp.content_type == ContentType.TEXT_HTML + assert resources.TEST_REMOTE_SERVER_WPS1_PROCESS_ID in resp.text + assert self.remote_provider_name in resp.text + assert resources.TEST_REMOTE_SERVER_URL in resp.text + @mocked_remote_server_requests_wps1([ resources.TEST_REMOTE_SERVER_URL, resources.TEST_REMOTE_SERVER_WPS1_GETCAP_XML, diff --git a/weaver/datatype.py b/weaver/datatype.py index 9a1a38ad4..b53bf6536 100644 --- a/weaver/datatype.py +++ b/weaver/datatype.py @@ -533,7 +533,8 @@ def keywords(self, container=None): Obtains the keywords relevant to the service provider. """ wps = self.wps(container=container) - return wps.identification.keywords + keywords = [kw.strip() for kw in wps.identification.keywords] + return keywords def summary(self, container, fetch=True, ignore=False): # type: (AnySettingsContainer, bool, bool) -> Optional[JSON] @@ -2306,6 +2307,7 @@ def _set_desc(self, description): def keywords(self): # type: () -> List[str] keywords = self.setdefault("keywords", []) + keywords = list({key.strip(): None for key in keywords}) if self.type not in keywords: keywords.append(self.type) self["keywords"] = keywords @@ -2314,7 +2316,8 @@ def keywords(self): @keywords.setter def keywords(self, keywords): # type: (List[str]) -> None - self["keywords"] = list(set(sd.KeywordList().deserialize(keywords))) + keywords = {key.strip(): None for key in keywords} + self["keywords"] = list(sd.KeywordList().deserialize(list(keywords))) @property def metadata(self): @@ -2843,8 +2846,8 @@ def href(self, container=None): proc_desc = base_url + sd.process_service.path.format(process_id=self.id) return proc_desc - def offering(self, schema=ProcessSchema.OGC, request=None): - # type: (ProcessSchemaType, AnyRequestType) -> JSON + def offering(self, schema=ProcessSchema.OGC, request=None, **additional_properties): + # type: (ProcessSchemaType, AnyRequestType, **JSON) -> JSON """ Obtains the :term:`JSON` or :term:`XML` serializable offering/description representation of the :term:`Process`. @@ -2852,6 +2855,9 @@ def offering(self, schema=ProcessSchema.OGC, request=None): :param schema: One of values defined by :class:`sd.ProcessDescriptionSchemaQuery` to select which process description representation to generate (see each schema for details). + :param additional_properties: + Additional properties to include in the JSON representation. + These properties still need to validate against the resolved schema if explicitly defined in it. .. note:: Property name ``offering`` is employed to differentiate from the string process ``description`` field. @@ -2890,9 +2896,10 @@ def offering(self, schema=ProcessSchema.OGC, request=None): # fields nested under 'process' + I/O as lists for io_type in ["inputs", "outputs"]: process[io_type] = normalize_ordered_io(process[io_type], io_hints[io_type]) - process.update({"process": dict(process)}) + process.update({"process": dict(process), **additional_properties}) return sd.ProcessDescriptionOLD(schema_meta_include=True).deserialize(process) # process fields directly at root + I/O as mappings + process.update(additional_properties) return sd.ProcessDescriptionOGC(schema_meta_include=True).deserialize(process) def summary(self, revision=False, links=True, container=None): diff --git a/weaver/wps_restapi/processes/processes.py b/weaver/wps_restapi/processes/processes.py index 998a8fdc9..c11615c56 100644 --- a/weaver/wps_restapi/processes/processes.py +++ b/weaver/wps_restapi/processes/processes.py @@ -35,7 +35,8 @@ from weaver.visibility import Visibility from weaver.wps_restapi import swagger_definitions as sd from weaver.wps_restapi.processes.utils import get_process_list_links, get_processes_filtered_by_valid_schemas -from weaver.wps_restapi.providers.utils import get_provider_services +from weaver.wps_restapi.providers.providers import describe_provider_process +from weaver.wps_restapi.providers.utils import get_provider_id, get_provider_services if TYPE_CHECKING: from pyramid.config import Configurator @@ -275,7 +276,13 @@ def get_local_process(request): Get a registered local process information (DescribeProcess). """ try: - process = get_process(request=request) + provider = None + provider_id = get_provider_id(request) + if provider_id: + process, service = describe_provider_process(request, provider_id) + provider = service.summary(request, fetch=True, ignore=True) # fetch is lazy since done during describe + else: + process = get_process(request=request) process["inputs"] = opensearch.replace_inputs_describe_process(process.inputs, process.payload) schema = request.params.get("schema") ctype = guess_target_format(request) @@ -285,7 +292,7 @@ def get_local_process(request): ctype_xml = add_content_type_charset(ContentType.APP_XML, "UTF-8") proc_url = process.href(request) if ctype in ContentType.ANY_XML or str(schema).upper() == ProcessSchema.WPS: - offering = process.offering(ProcessSchema.WPS, request=request) + offering = process.offering(ProcessSchema.WPS, request=request, provider=provider) headers = [ ("Link", make_link_header(f"{proc_url}?f=json", rel="alternate", type=ctype_json)), ("Link", make_link_header(f"{proc_url}?f=html", rel="alternate", type=ctype_html)), @@ -294,7 +301,7 @@ def get_local_process(request): ] return Response(offering, headerlist=headers) elif ctype == ContentType.APP_YAML: - offering = process.offering(schema) + offering = process.offering(schema, provider=provider) content = OutputFormat.convert(offering, OutputFormat.YAML) headers = [ ("Link", make_link_header(f"{proc_url}?f=json", rel="alternate", type=ctype_json)), @@ -306,7 +313,7 @@ def get_local_process(request): ] return HTTPOk(headers=headers, content_type=ctype, charset="utf-8", body=content) elif ctype == ContentType.APP_JSON: - offering = process.offering(schema) + offering = process.offering(schema, provider=provider) request.response.content_type = ctype_json request.response.headers.extend([ ("Link", make_link_header(f"{proc_url}?f=xml", rel="alternate", type=ctype_xml)), @@ -317,7 +324,7 @@ def get_local_process(request): ]) return Box(offering) else: # HTML - offering = process.offering(schema) + offering = process.offering(schema, provider=provider) request.response.headers.extend([ ("Link", make_link_header(f"{proc_url}?f=json", rel="alternate", type=ctype_json)), ("Link", make_link_header(f"{proc_url}?f=yaml", rel="alternate", type=ctype_yaml)), diff --git a/weaver/wps_restapi/providers/providers.py b/weaver/wps_restapi/providers/providers.py index 57cbacdb0..8a1ffb711 100644 --- a/weaver/wps_restapi/providers/providers.py +++ b/weaver/wps_restapi/providers/providers.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING import colander +from box import Box from pyramid.httpexceptions import ( HTTPBadRequest, HTTPConflict, @@ -15,18 +16,19 @@ from weaver.database import get_db from weaver.datatype import Process, Service from weaver.exceptions import ServiceNotFound, ServiceParsingError, log_unhandled_exceptions -from weaver.formats import ContentType, OutputFormat +from weaver.formats import ContentType, OutputFormat, guess_target_format from weaver.owsexceptions import OWSMissingParameterValue, OWSNotImplemented from weaver.processes.execution import submit_job from weaver.store.base import StoreServices from weaver.utils import get_any_id -from weaver.wps.utils import get_wps_client from weaver.wps_restapi import swagger_definitions as sd from weaver.wps_restapi.processes.utils import get_process_list_links from weaver.wps_restapi.providers.utils import check_provider_requirements, get_provider_services, get_service from weaver.wps_restapi.utils import get_schema_ref, handle_schema_validation if TYPE_CHECKING: + from typing import Optional, Tuple + from pyramid.config import Configurator from weaver.typedefs import AnyViewResponse, PyramidRequest @@ -174,7 +176,16 @@ def get_provider(request): return HTTPOk(json=data) -# FIXME: Add HTML view??? (same as local process, but extra 'provider' field?) +@sd.provider_processes_service.get( + tags=[sd.TAG_PROVIDERS, sd.TAG_PROCESSES, sd.TAG_GETCAPABILITIES], + schema=sd.ProviderProcessesEndpoint(), + accept=ContentType.TEXT_HTML, + renderer="weaver.wps_restapi:templates/responses/process_listing.mako", + response_schemas=sd.derive_responses( + sd.get_provider_processes_responses, + sd.GenericHTMLResponse(name="HTMLProcessListing", description="Listing of processes.") + ), +) @sd.provider_processes_service.get( tags=[sd.TAG_PROVIDERS, sd.TAG_PROCESSES, sd.TAG_GETCAPABILITIES], schema=sd.ProviderProcessesEndpoint(), @@ -199,25 +210,45 @@ def get_provider_processes(request): links = get_process_list_links(request, paging={}, total=None, provider=service) body = {"processes": processes, "links": links} body = sd.ProcessesListing().deserialize(body) + ctype = guess_target_format(request) + if ctype == ContentType.TEXT_HTML: + # remap to properties expected by HTML template + # it assumes that both local/remote processes can be returned + # therefore, remove the "local" processes to list only the current provider + provider_processes = body.pop("processes") + body["providers"] = [{"id": service.id, "url": service.url, "processes": provider_processes}] + body["processes"] = [] + body["total"] = len(provider_processes) + return Box(body) return HTTPOk(json=body) @check_provider_requirements -def describe_provider_process(request): - # type: (PyramidRequest) -> Process +def describe_provider_process(request, provider_id=None): + # type: (PyramidRequest, Optional[str]) -> Tuple[Process, Service] """ Obtains a remote service process description in a compatible local process format. Note: this processes won't be stored to the local process storage. """ - service = get_service(request) + service = get_service(request, provider_id=provider_id) # FIXME: support other providers (https://github.com/crim-ca/weaver/issues/130) - wps = get_wps_client(service.url, request) + wps = service.wps(request) # will cache, returned 'service' can reuse metadata without re-fetch proc_id = request.matchdict.get("process_id") process = wps.describeprocess(proc_id) - return Process.convert(process, service, container=request) + return Process.convert(process, service, container=request), service +@sd.provider_process_service.get( + tags=[sd.TAG_PROVIDERS, sd.TAG_PROCESSES, sd.TAG_DESCRIBEPROCESS], + schema=sd.ProviderProcessesEndpoint(), + accept=ContentType.TEXT_HTML, + renderer="weaver.wps_restapi:templates/responses/process_description.mako", + response_schemas=sd.derive_responses( + sd.get_provider_process_responses, + sd.GenericHTMLResponse(name="HTMLProcessDescription", description="Process description.") + ), +) @sd.provider_process_service.get( tags=[sd.TAG_PROVIDERS, sd.TAG_PROCESSES, sd.TAG_DESCRIBEPROCESS], accept=ContentType.APP_JSON, @@ -233,10 +264,11 @@ def get_provider_process(request): """ Retrieve a remote provider's process description (DescribeProcess). """ - process = describe_provider_process(request) + process, service = describe_provider_process(request) schema = request.params.get("schema") offering = process.offering(schema) - return HTTPOk(json=offering) + offering["provider"] = service.summary(request) + return Box(offering) @sd.provider_process_package_service.get( @@ -253,7 +285,7 @@ def get_provider_process_package(request): """ Retrieve a remote provider's process Application Package definition. """ - process = describe_provider_process(request) + process, _ = describe_provider_process(request) return HTTPOk(json=process.package or {}) diff --git a/weaver/wps_restapi/providers/utils.py b/weaver/wps_restapi/providers/utils.py index feee53a02..4f5659047 100644 --- a/weaver/wps_restapi/providers/utils.py +++ b/weaver/wps_restapi/providers/utils.py @@ -14,7 +14,7 @@ from typing import Any, Callable, List, Optional from weaver.datatype import Service - from weaver.typedefs import AnyRequestType, AnySettingsContainer + from weaver.typedefs import AnyRequestType, AnySettingsContainer, Params, Return LOGGER = logging.getLogger(__name__) @@ -22,7 +22,7 @@ def get_provider_services(container, check=True, ignore=True): # type: (AnySettingsContainer, bool, bool) -> List[Service] """ - Obtain the list of remote provider services. + Obtain the list of remote :term:`Provider` services. :param container: definition to retrieve settings and database connection. :param check: request that all provider services are remotely accessible to fetch metadata from them. @@ -55,28 +55,42 @@ def forbid_local_only(container): }) -def check_provider_requirements(func): - # type: (Callable[[AnySettingsContainer], Any]) -> Callable[[AnySettingsContainer], Any] +def check_provider_requirements( + func # type: Callable[[AnySettingsContainer, ..., Any], Return] +): # type: (...) -> Callable[[AnySettingsContainer, Any], Return] """ Decorator to validate if :term:`Provider` operations are applicable for the current `Weaver` instance. """ @functools.wraps(func) - def forbid_local(container): - # type: (AnySettingsContainer) -> Any + def forbid_local(container, *_, **__): + # type: (AnySettingsContainer, Params.args, Params.kwargs) -> Return forbid_local_only(container) - return func(container) + return func(container, *_, **__) return forbid_local def get_service(request, provider_id=None): # type: (AnyRequestType, Optional[str]) -> Service """ - Get the request service using provider_id from the service store. + Get the request :term:`Service` using provider_id from the service store. """ store = get_db(request).get_store(StoreServices) - prov_id = provider_id or request.matchdict.get("provider_id") + prov_id = provider_id or get_provider_id(request) try: service = store.fetch_by_name(prov_id) except ServiceNotFound: raise HTTPNotFound(f"Provider {prov_id} cannot be found.") return service + + +def get_provider_id(request): + # type: (AnyRequestType) -> Optional[str] + """ + Get any :term:`Provider` reference from the request. + """ + return ( + request.matchdict.get("provider_id") + or request.params.get("provider") + or request.params.get("service") + or None + ) diff --git a/weaver/wps_restapi/swagger_definitions.py b/weaver/wps_restapi/swagger_definitions.py index c38c89776..1bacde170 100644 --- a/weaver/wps_restapi/swagger_definitions.py +++ b/weaver/wps_restapi/swagger_definitions.py @@ -267,7 +267,8 @@ "keywords", "metadata", "inputs", - "outputs" + "outputs", + "provider", ] PROCESS_DESCRIPTION_FIELD_AFTER = [ "processDescriptionURL", @@ -277,7 +278,7 @@ "links" ] # fields ordering for nested process definition of OLD schema format of ProcessDescription -PROCESS_DESCRIPTION_FIELD_FIRST_OLD_SCHEMA = ["process"] +PROCESS_DESCRIPTION_FIELD_FIRST_OLD_SCHEMA = ["process", "provider"] PROCESS_DESCRIPTION_FIELD_AFTER_OLD_SCHEMA = ["links"] PROCESS_IO_FIELD_FIRST = ["id", "title", "description", "minOccurs", "maxOccurs"] @@ -3374,7 +3375,18 @@ class ProviderProcessEndpoint(ProviderProcessPath): querystring = ProcessDescriptionQuery() -class LocalProcessDescriptionQuery(ProcessDescriptionQuery, LocalProcessQuery, FormatQuery): +class ProviderServiceQuery(ExtendedMappingSchema): + provider = ProcessIdentifier( + default=None, missing=drop, + description="Request a process offered by a specific provider." + ) + service = ProcessIdentifier( + default=None, missing=drop, deprecated=True, + description="Request a process offered by a specific provider (alias to 'provider' for legacy reasons)." + ) + + +class LocalProcessDescriptionQuery(ProviderServiceQuery, ProcessDescriptionQuery, LocalProcessQuery, FormatQuery): pass @@ -3795,7 +3807,11 @@ class Process( _sort_after = PROCESS_DESCRIPTION_FIELD_AFTER -class ProcessDescriptionOLD(ProcessControl, ProcessDeploymentProfile, DescriptionLinks): +class ProcessDescriptionProvider(ExtendedMappingSchema): + provider = ProviderSummarySchema(missing=drop) + + +class ProcessDescriptionOLD(ProcessDescriptionProvider, ProcessControl, ProcessDeploymentProfile, DescriptionLinks): """ Old schema for process description. """ @@ -3809,6 +3825,7 @@ class ProcessDescriptionOLD(ProcessControl, ProcessDeploymentProfile, Descriptio class ProcessDescriptionOGC( ProcessSummary, ProcessContext, + ProcessDescriptionProvider, ProcessVisibility, ProcessLocations, ProcessDeploymentProfile, diff --git a/weaver/wps_restapi/templates/responses/process_description.mako b/weaver/wps_restapi/templates/responses/process_description.mako index dfcb7d202..9ace445ff 100644 --- a/weaver/wps_restapi/templates/responses/process_description.mako +++ b/weaver/wps_restapi/templates/responses/process_description.mako @@ -1,10 +1,19 @@ <%inherit file="weaver.wps_restapi:templates/responses/base.mako"/> <%namespace name="util" file="weaver.wps_restapi:templates/responses/util.mako"/> +<% + provider_id = provider.id if provider else None +%> + <%block name="breadcrumbs">
  • Home
  • +%if provider: +
  • Processes
  • +
  • ${id}
  • +%else:
  • Processes
  • ${id}
  • +%endif

    @@ -22,8 +31,8 @@

    + %if provider: +
  • + +
  • + %endif
  • + %if provider: +
    +

    + Provider +

    + ${util.render_provider(provider)} +
    + %endif +

    Inputs diff --git a/weaver/wps_restapi/templates/responses/process_listing.mako b/weaver/wps_restapi/templates/responses/process_listing.mako index ec795f168..f6382c093 100644 --- a/weaver/wps_restapi/templates/responses/process_listing.mako +++ b/weaver/wps_restapi/templates/responses/process_listing.mako @@ -11,7 +11,11 @@

    @@ -23,6 +27,15 @@ Return to API Frontpage
  • + %if providers: +
  • + List only Local Processes +
  • + %else: +
  • + List with Provider Processes +
  • + %endif ${util.get_paging_links()} @@ -34,18 +47,28 @@
    - %for process in processes: + <% + all_processes = [(None, proc) for proc in processes] + if providers: + all_processes.extend([ + (prov.id, proc) + for prov in providers + for proc in prov["processes"] + ]) + all_processes.sort(key=lambda x: (x[1]["id"]).lower()) + %> + %for prov, process in all_processes:
    %if process.get("title"): ${process.title} %endif
    @@ -69,6 +92,14 @@ %endfor %endif + %if prov: +
    +
    Provider:
    +
    + ${prov} +
    +
    + %endif
    %endfor
    diff --git a/weaver/wps_restapi/templates/responses/util.mako b/weaver/wps_restapi/templates/responses/util.mako index eec1503ca..9e90dc3e7 100644 --- a/weaver/wps_restapi/templates/responses/util.mako +++ b/weaver/wps_restapi/templates/responses/util.mako @@ -16,8 +16,12 @@ ${_prefix}/processes${f"?{query}" if query else ""}\ -<%def name="get_process_link(process_id, provider_id='', query='')">\ -${get_processes_link(provider_id=provider_id)}/${process_id}${f"?{query}" if query else ""}\ +<%def name="get_process_link(process_id, provider_id='', provider_uri=False, query='')">\ +<% + _prefix = get_processes_link(provider_id=provider_id if provider_id and provider_uri else None) + _suffix = f"&provider={provider_id}" if provider_id and not provider_uri else "" +%> +${_prefix}/${process_id}${f"?{query}{_suffix}" if query or _suffix else ""}\ @@ -197,6 +201,65 @@ NOTE: class 'language-json' used by the 'ajax/libs/highlight.js' library inserte +<%def name="render_provider(provider, detail=False)"> +
    +
    +
    + + %if "title" in provider: + + ${provider.title} + %endif +
    +
    +
    +
    Type:
    +
    ${provider.type}
    +
    +
    +
    Location:
    + +
    + %if provider.get("description"): +
    +
    Description:
    +
    ${provider.description}
    +
    + %endif + %if "version" in provider: +
    +
    Version:
    +
    ${provider.version}
    +
    + %endif + %if "keywords" in provider: +
    +
    Keywords:
    + %for keyword in provider.keywords: +
    ${keyword}
    + %endfor +
    + %endif + %if detail and provider.get("metadata"): +
    +
    Metadata:
    + ${render_metadata(provider.metadata)} +
    + %endif + %if detail and provider.get("links"): +
    +
    Links:
    + ${render_links(provider.links)} +
    + %endif +
    +
    +
    + + + <%def name="render_inputs(inputs)">
    %for input_id, input_data in inputs.items(): diff --git a/weaver/wps_restapi/templates/static/style.css b/weaver/wps_restapi/templates/static/style.css index 61016ccf7..e1104b480 100644 --- a/weaver/wps_restapi/templates/static/style.css +++ b/weaver/wps_restapi/templates/static/style.css @@ -111,6 +111,11 @@ button.active { text-decoration: underline; } +/* Breadcrumbs ID */ +.breadcrumbs li a .code { + font-size: 1.25em; +} + /* --- Generic Formats --- */ .undefined { @@ -159,6 +164,10 @@ button.active { margin-top: 0.2em; } +.field > .uri { + margin-top: 0; +} + .field > .language { margin-top: 0.2em; }