From aea07ec4e5831f02ece3f5a036d8aaa9d612cb5a Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Tue, 9 Jun 2026 09:09:06 +0200 Subject: [PATCH 1/2] i18n, default title changes --- .gitmodules | 3 + pymviewer | 1 + src/routes/qgis.py | 116 +++++++++++---- src/routes/shared.py | 15 +- src/routes/versions.py | 6 +- src/static/index.html | 34 ++--- src/static/js/mviewerstudio.js | 6 + src/static/js/utils/qgis/qgis.js | 163 ++++++++++++++++++---- src/static/js/utils/qgis/qgisToMviewer.js | 48 +++++-- src/static/mviewerstudio.i18n.json | 74 ++++++++++ src/utils/config_utils.py | 10 +- 11 files changed, 389 insertions(+), 87 deletions(-) create mode 100644 .gitmodules create mode 160000 pymviewer diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..a64d5c70 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "pymviewer"] + path = pymviewer + url = https://github.com/jdev-org/pymviewer.git diff --git a/pymviewer b/pymviewer new file mode 160000 index 00000000..b3c04380 --- /dev/null +++ b/pymviewer @@ -0,0 +1 @@ +Subproject commit b3c04380a19b0abf2ad05d59f898404e73bcb5d6 diff --git a/src/routes/qgis.py b/src/routes/qgis.py index b9caf850..c0ba9474 100644 --- a/src/routes/qgis.py +++ b/src/routes/qgis.py @@ -1,4 +1,5 @@ from datetime import datetime +import logging from pathlib import Path from uuid import uuid4 from tempfile import TemporaryDirectory @@ -35,10 +36,14 @@ ET.register_namespace("rdf", RDF_NAMESPACE) ET.register_namespace("dc", DC_NAMESPACE) +logger = logging.getLogger(__name__) + def _configured_qgis_projects_url() -> str: """Return the configured QGIS projects base URL with a trailing slash.""" - internal_base_url = (current_app.config.get("QGIS_SERVER_INTERNAL_URL") or "").strip() + internal_base_url = ( + current_app.config.get("QGIS_SERVER_INTERNAL_URL") or "" + ).strip() if internal_base_url: return internal_base_url.rstrip("/") + "/" @@ -60,7 +65,9 @@ def _public_qgis_projects_url() -> str: def _fetchable_qgis_capabilities_url(capabilities_url: str) -> str: """Return a backend-reachable capabilities URL while preserving the public URL.""" - internal_base_url = (current_app.config.get("QGIS_SERVER_INTERNAL_URL") or "").strip() + internal_base_url = ( + current_app.config.get("QGIS_SERVER_INTERNAL_URL") or "" + ).strip() if not internal_base_url: return capabilities_url @@ -78,7 +85,9 @@ def _fetchable_qgis_capabilities_url(capabilities_url: str) -> str: def _is_configured_qgis_url(url: str) -> bool: """Return ``True`` when the URL targets the configured QGIS projects base.""" configured_urls = {_public_qgis_projects_url()} - internal_base_url = (current_app.config.get("QGIS_SERVER_INTERNAL_URL") or "").strip() + internal_base_url = ( + current_app.config.get("QGIS_SERVER_INTERNAL_URL") or "" + ).strip() if internal_base_url: configured_urls.add(internal_base_url.rstrip("/") + "/") return any(url.startswith(configured_url) for configured_url in configured_urls) @@ -189,7 +198,9 @@ def _find_direct_child(element: ET.Element, child_name: str) -> ET.Element | Non def _find_direct_children(element: ET.Element, child_name: str) -> list[ET.Element]: """Return all direct children matching the provided local name.""" - return [child for child in list(element) if _xml_local_name(child.tag) == child_name] + return [ + child for child in list(element) if _xml_local_name(child.tag) == child_name + ] def _format_bbox_value(value: float) -> str: @@ -197,7 +208,9 @@ def _format_bbox_value(value: float) -> str: return f"{value:.12g}" -def _parse_bbox_attributes(bbox_node: ET.Element) -> tuple[float, float, float, float] | None: +def _parse_bbox_attributes( + bbox_node: ET.Element, +) -> tuple[float, float, float, float] | None: """Return bbox coordinates from a WMS ``BoundingBox``-like XML node.""" try: return ( @@ -221,7 +234,9 @@ def _root_layer_bbox_from_capabilities( capability_node = _find_direct_child(capabilities_root, "Capability") root_layer_node = ( - _find_direct_child(capability_node, "Layer") if capability_node is not None else None + _find_direct_child(capability_node, "Layer") + if capability_node is not None + else None ) if root_layer_node is None: return None @@ -247,21 +262,35 @@ def _root_layer_bbox_from_capabilities( matching_bbox_node = bounding_boxes[0] bbox_values = ( - _parse_bbox_attributes(matching_bbox_node) if matching_bbox_node is not None else None + _parse_bbox_attributes(matching_bbox_node) + if matching_bbox_node is not None + else None ) if bbox_values is None and normalized_projection in ("", "EPSG:4326", "CRS:84"): latlon_bbox_node = _find_direct_child(root_layer_node, "LatLonBoundingBox") bbox_values = ( - _parse_bbox_attributes(latlon_bbox_node) if latlon_bbox_node is not None else None + _parse_bbox_attributes(latlon_bbox_node) + if latlon_bbox_node is not None + else None ) if bbox_values is None: - geographic_bbox_node = _find_direct_child(root_layer_node, "EX_GeographicBoundingBox") + geographic_bbox_node = _find_direct_child( + root_layer_node, "EX_GeographicBoundingBox" + ) if geographic_bbox_node is not None: - west_node = _find_direct_child(geographic_bbox_node, "westBoundLongitude") - south_node = _find_direct_child(geographic_bbox_node, "southBoundLatitude") - east_node = _find_direct_child(geographic_bbox_node, "eastBoundLongitude") - north_node = _find_direct_child(geographic_bbox_node, "northBoundLatitude") + west_node = _find_direct_child( + geographic_bbox_node, "westBoundLongitude" + ) + south_node = _find_direct_child( + geographic_bbox_node, "southBoundLatitude" + ) + east_node = _find_direct_child( + geographic_bbox_node, "eastBoundLongitude" + ) + north_node = _find_direct_child( + geographic_bbox_node, "northBoundLatitude" + ) try: bbox_values = ( float(west_node.text), @@ -294,8 +323,7 @@ def _apply_root_layer_bbox_to_mviewer_xml( minx, miny, maxx, maxy = [float(value) for value in bbox.split(",")] center = ",".join( - _format_bbox_value(value) - for value in ((minx + maxx) / 2, (miny + maxy) / 2) + _format_bbox_value(value) for value in ((minx + maxx) / 2, (miny + maxy) / 2) ) mapoptions_node.set("extent", bbox) @@ -309,14 +337,24 @@ def _store_uploaded_qgis_project() -> dict: if not qgs_dir: raise BadRequest("Missing QGS folder configuration") + logger.info( + "QGIS upload request received: content_type=%s files=%s form=%s", + request.content_type, + list(request.files.keys()), + list(request.form.keys()), + ) uploaded_file = request.files.get("file") if not uploaded_file or not uploaded_file.filename: + logger.info("QGIS upload rejected: missing file field in multipart payload") raise BadRequest("Missing QGS file") filename = secure_filename(uploaded_file.filename) + logger.info("QGIS upload received file=%s target_dir=%s", filename, qgs_dir) lower_filename = filename.lower() if not lower_filename.endswith((".qgs", ".zip", ".qgz")): - raise BadRequest("Only .qgs files, .zip archives or .qgz archives are supported") + raise BadRequest( + "Only .qgs files, .zip archives or .qgz archives are supported" + ) if not path.exists(qgs_dir): mkdir(qgs_dir) @@ -344,13 +382,15 @@ def _ensure_config_metadata(xml_content: str, project_name: str) -> str: if not xml_root.get("mviewerversion") and app_conf.get("mviewer_version"): xml_root.set("mviewerversion", str(app_conf["mviewer_version"])) - if not xml_root.get("mviewerstudioversion") and app_conf.get("mviewerstudio_version"): + if not xml_root.get("mviewerstudioversion") and app_conf.get( + "mviewerstudio_version" + ): xml_root.set("mviewerstudioversion", str(app_conf["mviewerstudio_version"])) application_node = xml_root.find("application") - app_title = (application_node.get("title") if application_node is not None else "") or ( - project_name or "Projet QGIS" - ) + app_title = ( + application_node.get("title") if application_node is not None else "" + ) or (project_name or "Projet QGIS") creator = current_user.username if current_user else "anonymous" publisher = ( current_user.normalize_name @@ -426,7 +466,11 @@ def _absolute_config_url(config_path: str) -> str: relative_path = "/".join( part for part in [conf_path, normalize_url_part(config_path)] if part ) - return f"{mviewer_instance}/{relative_path}" if mviewer_instance else f"/{relative_path}" + return ( + f"{mviewer_instance}/{relative_path}" + if mviewer_instance + else f"/{relative_path}" + ) def _studio_open_config_url(config_url: str) -> str: @@ -495,7 +539,9 @@ def list_stored_qgs_configs() -> ResponseReturnValue: for root, _, files in walk(qgs_dir): for filename in files: if filename.lower().endswith(".qgs"): - qgs_files.append(_qgs_project_payload(qgs_dir, path.join(root, filename))) + qgs_files.append( + _qgs_project_payload(qgs_dir, path.join(root, filename)) + ) qgs_files.sort(key=lambda item: item["path"].lower()) return jsonify(qgs_files) @@ -517,14 +563,24 @@ def import_qgis_project() -> ResponseReturnValue: if not qgs_dir: raise BadRequest("Missing QGS folder configuration") + logger.info( + "QGIS import request received: content_type=%s files=%s form=%s", + request.content_type, + list(request.files.keys()), + list(request.form.keys()), + ) uploaded_file = request.files.get("file") if not uploaded_file or not uploaded_file.filename: + logger.info("QGIS import rejected: missing file field in multipart payload") raise BadRequest("Missing QGS file") filename = secure_filename(uploaded_file.filename) + logger.info("QGIS import received file=%s target_dir=%s", filename, qgs_dir) lower_filename = filename.lower() if not lower_filename.endswith((".qgs", ".zip", ".qgz")): - raise BadRequest("Only .qgs files, .zip archives or .qgz archives are supported") + raise BadRequest( + "Only .qgs files, .zip archives or .qgz archives are supported" + ) if not path.exists(qgs_dir): mkdir(qgs_dir) @@ -538,6 +594,12 @@ def import_qgis_project() -> ResponseReturnValue: project_payload, overwritten = _store_qgs_file(uploaded_file, qgs_dir, filename) capabilities_url = _project_capabilities_url(project_payload["projectName"]) + logger.info( + "QGIS import resolved project=%s capabilities_url=%s overwritten=%s", + project_payload["projectName"], + capabilities_url, + overwritten, + ) xml_content = _mviewer_xml_from_capabilities_url(capabilities_url) response = current_app.response_class(xml_content, mimetype="application/xml") response.headers["X-Qgis-Project-Name"] = project_payload["projectName"] @@ -579,12 +641,16 @@ def create_and_open_qgis_project_config() -> ResponseReturnValue: @basic_store.route("/api/app/qgis/projects//open", methods=["POST"]) -def create_and_open_stored_qgis_project_config(project_name: str) -> ResponseReturnValue: +def create_and_open_stored_qgis_project_config( + project_name: str, +) -> ResponseReturnValue: """Create a Studio config from an existing stored QGIS project.""" try: _stored_qgis_project_directory(project_name) capabilities_url = _project_capabilities_url(project_name) - config_data, _ = _create_config_from_qgis_capabilities(capabilities_url, project_name) + config_data, _ = _create_config_from_qgis_capabilities( + capabilities_url, project_name + ) config_url = _absolute_config_url(config_data["url"]) studio_url = _studio_open_config_url(config_url) diff --git a/src/routes/shared.py b/src/routes/shared.py index 86371908..a90845f9 100644 --- a/src/routes/shared.py +++ b/src/routes/shared.py @@ -49,7 +49,9 @@ def _allowed_proxy_origins() -> set[str]: forwarded_host = request.headers.get("X-Forwarded-Host") if forwarded_host: allowed_origins.update( - _normalize_origin(host) for host in forwarded_host.split(",") if host.strip() + _normalize_origin(host) + for host in forwarded_host.split(",") + if host.strip() ) return allowed_origins @@ -70,16 +72,16 @@ def _is_allowed_proxy_origin(url: str) -> bool: if target_hostname in allowed_hostnames: return True - return ( - target_hostname in _localhost_aliases() - and bool(allowed_hostnames.intersection(_localhost_aliases())) + return target_hostname in _localhost_aliases() and bool( + allowed_hostnames.intersection(_localhost_aliases()) ) def _is_get_capabilities_url(url: str) -> bool: parsed_url = urlparse(url) query_params = { - key.lower(): value for key, value in parse_qsl(parsed_url.query, keep_blank_values=True) + key.lower(): value + for key, value in parse_qsl(parsed_url.query, keep_blank_values=True) } return query_params.get("request", "").lower() == "getcapabilities" @@ -214,7 +216,8 @@ def _extract_qgs_zip( extracted_projects.append(path.join(root, extracted_file)) extracted_payloads = [ - _qgs_project_payload(qgs_dir, absolute_file) for absolute_file in extracted_projects + _qgs_project_payload(qgs_dir, absolute_file) + for absolute_file in extracted_projects ] extracted_payloads.sort(key=lambda item: item["path"].lower()) return destination_dir, extracted_payloads, overwritten diff --git a/src/routes/versions.py b/src/routes/versions.py index 31076759..1c1ba93a 100644 --- a/src/routes/versions.py +++ b/src/routes/versions.py @@ -19,7 +19,11 @@ def get_all_app_versions(id) -> ResponseReturnValue: if not config: raise BadRequest("This config doesn't exists !") config = config[0] - org = current_user.normalize_name if current_user else current_app.config["DEFAULT_ORG"] + org = ( + current_user.normalize_name + if current_user + else current_app.config["DEFAULT_ORG"] + ) workspace = path.join(current_app.config["EXPORT_CONF_FOLDER"], org, config["id"]) git = Git_manager(workspace) return jsonify({"versions": git.get_versions(), "config": config}) diff --git a/src/static/index.html b/src/static/index.html index 03a104cf..dd18e3c0 100755 --- a/src/static/index.html +++ b/src/static/index.html @@ -1059,7 +1059,7 @@ @@ -1106,9 +1106,9 @@
- +
- +
- +
- +
@@ -1128,23 +1128,23 @@
- +
- - + + - +
ProjetActionProjetAction
Chargement…Chargement…
@@ -1820,7 +1820,7 @@
Paramètres de la nouvelle donnée
- +
@@ -1922,14 +1922,14 @@
Paramètres de la nouvelle donnée
-
Paramètres du projet QGIS
+
Paramètres du projet QGIS
- +
- +
@@ -1940,9 +1940,9 @@
Paramètres du projet QGIS
- +
diff --git a/src/static/js/mviewerstudio.js b/src/static/js/mviewerstudio.js index 8dbdbc03..1d552327 100644 --- a/src/static/js/mviewerstudio.js +++ b/src/static/js/mviewerstudio.js @@ -1383,6 +1383,7 @@ var loadApplicationParametersFromFile = function () { alertCustom(mviewer.tr("msg.file_read_error"), "danger"); }; showStudio(); + document.getElementById("qgis-local-file-name").value = ""; }; var loadApplicationParametersFromRemoteFile = function (url) { @@ -1616,6 +1617,11 @@ $("#mod-importfile").on("shown.bs.modal", function () { mv.getListeApplications(); }); +$("#mod-importfile").on("shown.bs.modal", function () { + document.getElementById("qgis-local-file-name").value = ""; + document.getElementById("qgis-filebutton").value = ""; +}); + var uploadSldFileToBackend = function (e) { var reader = new FileReader(); e.files[0].text().then(function (sldFile) { diff --git a/src/static/js/utils/qgis/qgis.js b/src/static/js/utils/qgis/qgis.js index 1807ee05..72ccf69c 100644 --- a/src/static/js/utils/qgis/qgis.js +++ b/src/static/js/utils/qgis/qgis.js @@ -8,6 +8,13 @@ const escapeHtmlAttribute = function (value) { .replace(/>/g, ">"); }; +const translateQgis = function (key, fallback) { + if (window.mviewer && typeof window.mviewer.tr === "function") { + return window.mviewer.tr(key); + } + return fallback; +}; + // QGIS project import utilities const qgis = { /** @@ -176,10 +183,14 @@ const qgis = { buildProjectCapabilitiesUrl: function (projectName) { const baseUrl = this.getConfiguredQgisProjectsBaseUrl(); if (!baseUrl) { - throw new Error("La configuration qgis.url est manquante"); + throw new Error( + translateQgis("qgis.errors.missing_config_url", "La configuration qgis.url est manquante") + ); } if (!projectName) { - throw new Error("Le nom du projet QGIS est manquant"); + throw new Error( + translateQgis("qgis.errors.missing_project_name", "Le nom du projet QGIS est manquant") + ); } return `${baseUrl}${encodeURIComponent( @@ -212,7 +223,12 @@ const qgis = { if (!this.isGetCapabilitiesUrl(normalizedUrl)) { return Promise.reject( - new Error("L'URL du projet QGIS doit être une requête GetCapabilities") + new Error( + translateQgis( + "qgis.errors.invalid_capabilities_url", + "L'URL du projet QGIS doit être une requête GetCapabilities" + ) + ) ); } @@ -264,7 +280,11 @@ const qgis = { const message = typeof error === "string" ? error - : error?.message || "La conversion du projet QGIS a échoué"; + : error?.message || + translateQgis( + "qgis.errors.convert_failed", + "La conversion du projet QGIS a échoué" + ); window.alertCustom(message, "danger"); }) .finally(() => { @@ -286,6 +306,7 @@ const qgis = { const urlInput = document.getElementById("getQgisApp"); const localFileNameInput = document.getElementById("qgis-local-file-name"); + const localFileInput = document.getElementById("qgis-filebutton"); if (localFileNameInput) { localFileNameInput.value = file.name; @@ -305,6 +326,12 @@ const qgis = { }) .then((mviewerXml) => { this.parseMviewerApplicationXml(window.$.parseXML(mviewerXml)); + if (localFileNameInput) { + localFileNameInput.value = ""; + } + if (localFileInput) { + localFileInput.value = ""; + } window.showStudio(); }); }, @@ -337,7 +364,10 @@ const qgis = { return data; }) .catch((error) => { - window.alertCustom(error || "QGS upload failed", "danger"); + window.alertCustom( + error || translateQgis("qgis.errors.upload_failed", "QGS upload failed"), + "danger" + ); return Promise.reject(error); }); }, @@ -369,8 +399,10 @@ const qgis = { } if (!projects || projects.length === 0) { - tableBody.innerHTML = - 'Aucune configuration QGIS disponible.'; + tableBody.innerHTML = `${translateQgis( + "qgis.projects.empty", + "Aucune configuration QGIS disponible." + )}`; return; } @@ -382,13 +414,20 @@ const qgis = { ${projectName} -
+