diff --git a/doc/integrator/configuration.rst b/doc/integrator/configuration.rst index b5691ed4fe..4af42db5cb 100644 --- a/doc/integrator/configuration.rst +++ b/doc/integrator/configuration.rst @@ -10,6 +10,7 @@ This chapter describes advanced configuration settings. build runtime + interface ngeo backend caching diff --git a/doc/integrator/interface.rst b/doc/integrator/interface.rst new file mode 100644 index 0000000000..5dcd90f49e --- /dev/null +++ b/doc/integrator/interface.rst @@ -0,0 +1,79 @@ +.. _integrator_interface: + +Introduction +------------ + +This chapter describes how to integrate a new ``custom`` interface in a c2cgeoportal application. + +``custom`` means that the interface can be based on other frontend than ngeo event if at the end of this +document we will speak about how to integrate a ngeo as custom interface with a simple build chain. + +Review +------ + +In the c2cgeoportal application we have by default 3 interfaces: desktop, mobile and iframe. + +c2cgeoportal provide different things around the interface but they are not hard linked: + +- The interface in the admin interface is a way to define the layers visible in the interface. +- The ``interfaces_config`` in the ``vars.yaml`` is a way to define the interface configuration. +- The ``interface`` in the ``vars.yaml`` is a way to configure the interfaces route in c2cgeoportal: ``/``, + ``/theme/``, ``/`` and ``//theme/``. + +Configuration +------------- + +Here we will describe how to add a new ``custom`` interface in a c2cgeoportal application, +for that we should add a new entry in the ``interface_config`` with the ``type`` set to ``custom``. + +.. code:: yaml + + vars: + interfaces_config: + custom: + type: custom + name: my_interface + +We can also add an optional ``file`` attribute in the config to specify the file that should be used for the interface, +with relative (from ``/etc/static-frontend``) or absolute file name. + +Interface integration +--------------------- + +To publish the interfaces que should provide the interfaces files (HTML, CSS, JavaScript, images, ...) in +the ``/etc/static-frontend/`` directory. + +The interface files should be in a directory named with the interface name suffixed by ``.html``. + +Note that this folder is also available on the ``/static-frontend/`` endpoint with cache headers without +any cash bustering, then the files (other than the interfaces HTML files) should contains an hash. + +If you need cache bustering you should put your files in the ``geoportal/geomapfish_geoportal/static/`` +directory (``/etc/geomapfish/static`` in the container). + +The interface HTML file is considered as mako template and he can use the following variables: +- ``request``: the Pyramid request object. +- ``dynamicUrl``: the URL to get the interface configuration. +- ``interface``: the interface name. +- ``staticFrontend``: the URL to the static frontend directory. +- ``staticCashBuster``: the URL to the static cash buster directory. + +For that you should create a Docker image that provide a ``/etc/static-frontend/`` volume (``VOLUME /etc/static-frontend``). + +And include it in the ``docker-compose.yaml`` file with something like: + +.. code:: yaml + + services: + my_interface: + image: my_interface + user: www-data + + geoportal: + volumes_from: + - my_interface:ro + +Ngeo integration +---------------- + +TODO diff --git a/geoportal/c2cgeoportal_geoportal/__init__.py b/geoportal/c2cgeoportal_geoportal/__init__.py index e64772180c..3951180816 100644 --- a/geoportal/c2cgeoportal_geoportal/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/__init__.py @@ -71,7 +71,7 @@ TotalPythonObjectMemoryCollector, ) from c2cgeoportal_geoportal.lib.xsd import XSD -from c2cgeoportal_geoportal.views.entry import Entry, canvas_view +from c2cgeoportal_geoportal.views.entry import Entry, canvas_view, custom_view if TYPE_CHECKING: from c2cgeoportal_commons.models import static # pylint: disable=ungrouped-imports,useless-suppression @@ -99,6 +99,7 @@ def __call__(self, value: Any, system: dict[str, str]) -> bytes: INTERFACE_TYPE_NGEO = "ngeo" INTERFACE_TYPE_CANVAS = "canvas" +INTERFACE_TYPE_CUSTOM = "custom" def add_interface_config(config: pyramid.config.Configurator, interface_config: dict[str, Any]) -> None: @@ -139,6 +140,15 @@ def add_interface( interface_config=interface_config, **kwargs, ) + elif interface_type == INTERFACE_TYPE_CUSTOM: + assert interface_config is not None + add_interface_custom( + config, + route_name=interface_name, + route=route, + interface_config=interface_config, + **kwargs, + ) else: _LOG.error( "Unknown interface type '%s', should be '%s' or '%s'.", @@ -209,6 +219,39 @@ def add_interface_canvas( ) +def add_interface_custom( + config: pyramid.config.Configurator, + route_name: str, + route: str, + interface_config: dict[str, Any], + permission: Optional[str] = None, +) -> None: + """Add the ngeo interfaces views and routes.""" + + renderer = os.path.join("/etc/static-frontend", interface_config.get("file", f"{route_name}.html")) + config.add_route(route_name, route, request_method="GET") + # Permalink theme: recover the theme for generating custom viewer.js URL + config.add_route( + f"{route_name}theme", + f"{route}{'' if route[-1] == '/' else '/'}theme/{{themes}}", + request_method="GET", + ) + view = partial(custom_view, interface_config=interface_config) + view.__module__ = custom_view.__module__ + config.add_view( + view, + route_name=route_name, + renderer=renderer, + permission=permission, + ) + config.add_view( + view, + route_name=f"{route_name}theme", + renderer=renderer, + permission=permission, + ) + + def add_admin_interface(config: pyramid.config.Configurator) -> None: """Add the administration interface views and routes.""" @@ -607,12 +650,17 @@ def add_static_route(name: str, attr: str, path: str, renderer: str) -> None: config.add_route(name, path, request_method="GET") config.add_view(Entry, attr=attr, route_name=name, renderer=renderer) - add_static_route("favicon", "favicon", "/favicon.ico", "/etc/geomapfish/static/images/favicon.ico") - add_static_route("robot.txt", "robot_txt", "/robot.txt", "/etc/geomapfish/static/robot.txt") + static_files = config.get_settings().get("static_files", {}) + for name, attr, path in [ + ("favicon.ico", "favicon", "/favicon.ico"), + ("robot.txt", "robot_txt", "/robot.txt"), + ("api.js.map", "apijsmap", "/api.js.map"), + ("api.css", "apicss", "/api.css"), + ("apihelp.html", "apihelp", "/apihelp/index.html"), + ]: + if static_files.get(name): + add_static_route(name, attr, path, static_files[name]) config.add_route("apijs", "/api.js", request_method="GET") - add_static_route("apijsmap", "apijsmap", "/api.js.map", "/etc/static-ngeo/api.js.map") - add_static_route("apicss", "apicss", "/api.css", "/etc/static-ngeo/api.css") - add_static_route("apihelp", "apihelp", "/apihelp/index.html", "/etc/geomapfish/static/apihelp/index.html") c2cgeoportal_geoportal.views.add_redirect(config, "apihelp_redirect", "/apihelp.html", "apihelp") config.add_route("themes", "/themes", request_method="GET", pregenerator=C2CPregenerator(role=True)) @@ -747,6 +795,11 @@ def add_static_route(name: str, attr: str, path: str, renderer: str) -> None: path="/etc/static-ngeo", cache_max_age=int(config.get_settings()["default_max_age"]), ) + config.add_static_view( + name="static-frontend", + path="/etc/static-frontend", + cache_max_age=int(config.get_settings()["default_max_age"]), + ) # Add the c2cgeoportal static view with cache buster config.add_static_view( diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile index 9cc329fd2c..5a11730f09 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile @@ -14,7 +14,7 @@ ENV CONFIG_VARS sqlalchemy.url sqlalchemy.pool_recycle sqlalchemy.pool_size sqla dbsessions urllogin host_forward_host headers_whitelist headers_blacklist \ smtp c2c.base_path welcome_email \ lingua_extractor interfaces_config interfaces devserver_url api authentication intranet metrics pdfreport \ - vector_tiles i18next main_ogc_server + vector_tiles i18next main_ogc_server static_files COPY . /tmp/config/ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml b/geoportal/c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml index f57d410318..04527cf1fd 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml @@ -73,6 +73,14 @@ vars: escapeValue: false backend: {} + static_files: + favicon.ico: /etc/geomapfish/static/images/favicon.ico + robot.txt: /etc/geomapfish/static/robot.txt + api.js: /etc/static-ngeo/api.js + api.js.map: /etc/static-ngeo/api.js.map + api.css: /etc/static-ngeo/api.css + apihelp.html: /etc/geomapfish/static/apihelp/index.html + interfaces_config: default: constants: diff --git a/geoportal/c2cgeoportal_geoportal/views/entry.py b/geoportal/c2cgeoportal_geoportal/views/entry.py index 854325f25d..a8aaf88087 100644 --- a/geoportal/c2cgeoportal_geoportal/views/entry.py +++ b/geoportal/c2cgeoportal_geoportal/views/entry.py @@ -61,8 +61,8 @@ def get_ngeo_index_vars(self) -> dict[str, Any]: @staticmethod @_CACHE_REGION.cache_on_arguments() - def get_apijs(api_name: Optional[str]) -> str: - with open("/etc/static-ngeo/api.js", encoding="utf-8") as api_file: + def get_apijs(api_filename: str, api_name: Optional[str]) -> str: + with open(api_filename, encoding="utf-8") as api_file: api = api_file.read().split("\n") sourcemap = api.pop(-1) if api_name: @@ -77,7 +77,10 @@ def get_apijs(api_name: Optional[str]) -> str: @view_config(route_name="apijs") # type: ignore def apijs(self) -> pyramid.response.Response: - self.request.response.text = self.get_apijs(self.request.registry.settings["api"].get("name")) + self.request.response.text = self.get_apijs( + self.request.registry.settings["static_files"]["api.js"], + self.request.registry.settings["api"].get("name"), + ) set_common_headers(self.request, "api", Cache.PUBLIC, content_type="application/javascript") return self.request.response @@ -138,3 +141,18 @@ def canvas_view(request: pyramid.request.Request, interface_config: dict[str, An ), "spinner": spinner, } + + +def custom_view(request: pyramid.request.Request, interface_config: dict[str, Any]) -> dict[str, Any]: + """Get view used as entry point of a canvas interface.""" + + set_common_headers(request, "index", Cache.PUBLIC_NO, content_type="text/html") + + dynamic_url = request.route_url("dynamic") + return { + "request": request, + "dynamicUrl": dynamic_url, + "interface": interface_config["name"], + "staticFrontend": request.static_url("/etc/static-frontend"), + "staticCashBuster": request.static_url("/etc/geomapfish/static"), + }