From 2fd8dc398b01503203457ba9fcef0aaed7c0de95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20E?= Date: Tue, 6 May 2025 16:58:48 +0200 Subject: [PATCH 01/14] modif service ign --- lib/mv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mv.js b/lib/mv.js index 24f12657..66da159f 100755 --- a/lib/mv.js +++ b/lib/mv.js @@ -1311,7 +1311,7 @@ var mv = (function () { $("#opt-searchl-attribution").show(); $("#opt-searchl-inputlabel").show(); // TODO : need to get values from config - $("#opt-searchlocalities-url").val("https://api-adresse.data.gouv.fr/search/"); + $("#opt-searchlocalities-url").val("https://data.geopf.fr/geocodage/search/"); $("#opt-searchlocalities-attribution").val("Base adresse nationale (BAN)"); $("#opt-searchlocalities-inputlabel").val(""); $("#search-querymaponclick").show(); From 31379100209a4250224a39bfe9f7545efdb279c9 Mon Sep 17 00:00:00 2001 From: spelhate Date: Wed, 16 Jul 2025 09:58:00 +0200 Subject: [PATCH 02/14] Fix #369 - correction port mviewer dans conf nginx docker --- docker/nginx/default.conf.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/nginx/default.conf.template b/docker/nginx/default.conf.template index d12b3f75..c0e216ac 100644 --- a/docker/nginx/default.conf.template +++ b/docker/nginx/default.conf.template @@ -3,7 +3,7 @@ server { server_name ${NGINX_HOST}; location /mviewer/ { - proxy_pass http://mviewer:80/; + proxy_pass http://mviewer:8080/; } location /${MVIEWERSTUDIO_URL_PATH_PREFIX}/ { From 02c743a220df402d2eaf5bb93621bb97dbd00447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20E?= Date: Wed, 16 Jul 2025 10:27:11 +0200 Subject: [PATCH 03/14] =?UTF-8?q?Suppression=20catalogue=20r=C3=A9gion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config-python-sample.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config-python-sample.json b/config-python-sample.json index 95cdf22c..3881e149 100644 --- a/config-python-sample.json +++ b/config-python-sample.json @@ -174,11 +174,6 @@ "url": "https://geobretagne.fr/geonetwork/srv/fre/csw", "baseref": "https://geobretagne.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" }, - { - "title": "Catalogue Région Bretagne", - "url": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/csw", - "baseref": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/catalog.search#/metadata/" - }, { "title": "Catalogue de la Région Grand Est", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", From 127c7d3ef1e5883a3942805a40237f33e503b53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20E?= Date: Wed, 16 Jul 2025 10:28:08 +0200 Subject: [PATCH 04/14] =?UTF-8?q?Suppression=20catalogue=20r=C3=A9gion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config-php-sample.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config-php-sample.json b/config-php-sample.json index 2ed7b236..6012907a 100644 --- a/config-php-sample.json +++ b/config-php-sample.json @@ -180,11 +180,6 @@ "url": "https://geobretagne.fr/geonetwork/srv/fre/csw", "baseref": "https://geobretagne.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" }, - { - "title": "Catalogue Région Bretagne", - "url": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/csw", - "baseref": "https://kartenn.region-bretagne.fr/geonetwork/srv/fre/catalog.search#/metadata/" - }, { "title": "Catalogue de la Région Grand Est", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", From 885c1f41e922f48f2bdd51617cc0ae5ef5aca582 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Mon, 22 Dec 2025 16:46:54 +0100 Subject: [PATCH 05/14] Replace ogc file ajax request by fetch ogc file - change ajax to fetch request pretty code use default config py sample --- config-python-sample.json | 8 +- lib/mv.js | 2 +- lib/ogc.js | 151 ++++++++++++++++++++------------------ 3 files changed, 85 insertions(+), 76 deletions(-) diff --git a/config-python-sample.json b/config-python-sample.json index 3881e149..f134ab8e 100644 --- a/config-python-sample.json +++ b/config-python-sample.json @@ -1,15 +1,15 @@ { "app_conf": { "studio_title": "Mviewer Studio", - "mviewer_version": "3.13", - "mviewerstudio_version": "4.2", + "mviewer_version": "4", + "mviewerstudio_version": "4.3-snapshot", "api": "api/app", "store_style_service": "api/style", "mviewer_instance": "/mviewer/", "publish_url": "/mviewer/?config=apps/public/{{config}}.xml", "conf_path_from_mviewer": "apps/store/", "mviewer_short_url": { - "used": true, + "used": false, "apps_folder": "store", "public_folder": "public" }, @@ -178,7 +178,7 @@ "title": "Catalogue de la Région Grand Est", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", "baseref": "https://datagrandest.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" - } + } ], "wms": [{ "title": "Serveur WMS de la Région", diff --git a/lib/mv.js b/lib/mv.js index 66da159f..0f81f859 100755 --- a/lib/mv.js +++ b/lib/mv.js @@ -71,7 +71,7 @@ var mv = (function () { function uuid() { var dt = new Date().getTime(); var uuid = "xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - var r = (dt + Math.random() * 16) % 16 | 0; + var r = ((dt + Math.random() * 16) % 16) | 0; dt = Math.floor(dt / 16); return (c == "x" ? r : (r & 0x3) | 0x8).toString(16); }); diff --git a/lib/ogc.js b/lib/ogc.js index ce73e85b..b443203b 100755 --- a/lib/ogc.js +++ b/lib/ogc.js @@ -241,28 +241,38 @@ var ogc = (function () { mv.showCSWResults(results); }; + var _parseXml = function (text) { + var parser = new DOMParser(); + return parser.parseFromString(text, "text/xml"); + }; + var _cswAjax = function (url, body, metadataUrl, typeNames) { - $.ajax({ - type: "POST", - url: mv.ajaxURL(url), - crossDomain: true, - data: body, - dataType: "xml", - contentType: "application/xml", - success: function (data) { - _cswParse(data, url, metadataUrl, typeNames); + fetch(mv.ajaxURL(url), { + method: "POST", + headers: { + "Content-Type": "application/xml", }, - error: function (xhr, ajaxOptions, thrownError) { + body: body, + }) + .then((response) => { + if (!response.ok) { + throw new Error( + "CSW request failed: " + response.status + " " + response.statusText + ); + } + return response.text(); + }) + .then((data) => { + _cswParse(_parseXml(data), url, metadataUrl, typeNames); + }) + .catch((error) => { console.error("CSW request failed", { url: url, body: body, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête CSW :\n" + url); - }, - }); + }); }; var _wmsCapabilitiesParse = function (data, layerid) { @@ -405,25 +415,28 @@ var ogc = (function () { ? getCapabilitiesUrl + "?" + getCapabilitiesParams : getCapabilitiesUrl + "&" + getCapabilitiesParams; - $.ajax({ - type: "GET", - url: mv.ajaxURL(getCapabilitiesUrl), - crossDomain: true, - dataType: "xml", - contentType: "application/xml", - success: function (data) { - _wmsParse(data, keyword); - }, - error: function (xhr, ajaxOptions, thrownError) { + fetch(mv.ajaxURL(getCapabilitiesUrl)) + .then((response) => { + if (!response.ok) { + throw new Error( + "WMS GetCapabilities request failed: " + + response.status + + " " + + response.statusText + ); + } + return response.text(); + }) + .then((data) => { + _wmsParse(_parseXml(data), keyword); + }) + .catch((error) => { console.error("WMS GetCapabilities request failed", { url: getCapabilitiesUrl, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête WMS GetCapabilities :\n" + getCapabilitiesUrl); - }, - }); + }); }; var _DescribeFeatureTypeParse = function (data, typeName, layerid, url) { @@ -465,12 +478,7 @@ var ogc = (function () { ? descFeatTypeUrl + "?" + descFeatTypeParams : descFeatTypeUrl + "&" + descFeatTypeParams; - return fetch(mv.ajaxURL(descFeatTypeUrl), { - mode: "cors", - headers: { - "Content-Type": "application/xml", - }, - }) + return fetch(descFeatTypeUrl) .then((response) => response.text()) .then((data) => { return _DescribeFeatureTypeParse(data, typeName, layerid, url); @@ -488,25 +496,28 @@ var ogc = (function () { ? getCapabilitiesUrl + "?" + getCapabilitiesParams : getCapabilitiesUrl + "&" + getCapabilitiesParams; - $.ajax({ - type: "GET", - url: mv.ajaxURL(getCapabilitiesUrl), - crossDomain: true, - dataType: "xml", - contentType: "application/xml", - success: function (data) { - _wmsCapabilitiesParse(data, layerid); - }, - error: function (xhr, ajaxOptions, thrownError) { + fetch(getCapabilitiesUrl) + .then((response) => { + if (!response.ok) { + throw new Error( + "WMS GetCapabilities request failed: " + + response.status + + " " + + response.statusText + ); + } + return response.clone().text(); + }) + .then((data) => { + _wmsCapabilitiesParse(_parseXml(data), layerid); + }) + .catch((error) => { console.error("WMS GetCapabilities request failed", { url: getCapabilitiesUrl, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête WMS GetCapabilities :\n" + getCapabilitiesUrl); - }, - }); + }); }; var _describeLayerParse = function (xml, layerid) { @@ -524,12 +535,7 @@ var ogc = (function () { ? descLayerUrl + "?" + descLayerParams : descLayerUrl + "&" + descLayerParams; - return fetch(mv.ajaxURL(descLayerUrl), { - mode: "cors", - headers: { - "Content-Type": "application/xml", - }, - }) + return fetch(mv.ajaxURL(descLayerUrl)) .then((response) => response.text()) .then((data) => { return { @@ -613,25 +619,28 @@ var ogc = (function () { let newUrl = ogc.urlToObject(url, queryParams); let stringUrl = newUrl.toString(); - $.ajax({ - type: "GET", - url: mv.ajaxURL(stringUrl), - crossDomain: true, - dataType: "json", - contentType: "application/json", - success: function (data) { + fetch(stringUrl) + .then((response) => { + if (!response.ok) { + throw new Error( + "WFS GetFeature request failed: " + + response.status + + " " + + response.statusText + ); + } + return response.json(); + }) + .then((data) => { onSuccess(data); - }, - error: function (xhr, ajaxOptions, thrownError) { + }) + .catch((error) => { console.error("WFS GetFeature request failed", { url: stringUrl, - xhr: xhr, - ajaxOptions: ajaxOptions, - thrownError: thrownError, + error: error, }); alert("Echec de la requête WFS GetFeature :\n" + stringUrl); - }, - }); + }); }, }; })(); From db671f1c2c5ac54a65dd124ce62b3f52dbdb8764 Mon Sep 17 00:00:00 2001 From: mychka Date: Fri, 13 Feb 2026 08:57:46 +0100 Subject: [PATCH 06/14] =?UTF-8?q?fix=20maj=20fonds=20s=C3=A9lectionnables?= =?UTF-8?q?=20(#386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix maj fonds sélectionnables * fix code format * add empty option value --------- Co-authored-by: Mathilde Marchand --- lib/mv.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/mv.js b/lib/mv.js index 0f81f859..7e02b9a3 100755 --- a/lib/mv.js +++ b/lib/mv.js @@ -323,10 +323,23 @@ var mv = (function () { $(".bl." + classe + " input").bind("change", function (e) { var id = $(this).parent().parent().attr("data-layerid"); var value = $(e.currentTarget).prop("checked"); + var select = $("#frm-bl-visible"); + var currentValue = select.val(); if (value === true) { - $("#frm-bl-visible option[value='" + id + "']").removeAttr("disabled"); + select.find("option[value='" + id + "']").removeAttr("disabled"); + if (!currentValue) { + select.val(id).trigger("change"); + } } else { - $("#frm-bl-visible option[value='" + id + "']").attr("disabled", "disabled"); + select.find("option[value='" + id + "']").attr("disabled", "disabled"); + if (id === currentValue) { + var first = select.find("option:not(:disabled)").first(); + if (first.length) { + select.val(first.val()).trigger("change"); + } else { + select.val(null).trigger("change"); + } + } } }); }, From e66ddde5194e4d70eb77ab511de4293a269a2894 Mon Sep 17 00:00:00 2001 From: LoIc Date: Fri, 13 Feb 2026 10:57:48 +0100 Subject: [PATCH 07/14] modif config --- config-php-sample.json | 12 ++++++------ config-python-sample.json | 10 +++++----- themes.json | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config-php-sample.json b/config-php-sample.json index 6012907a..14c41c7e 100644 --- a/config-php-sample.json +++ b/config-php-sample.json @@ -1,8 +1,8 @@ { "app_conf": { "studio_title": "GéoBretagne mviewer studio", - "mviewer_version": "3.13", - "mviewerstudio_version": "4.2", + "mviewer_version": "4", + "mviewerstudio_version": "4.2.1", "is_php": "true", "php": { "upload_service": "srv/php/store.php", @@ -83,7 +83,7 @@ "type": "OSM", "url": "https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png", "maxzoom": "20", - "attribution": "Map tiles by CartoDb, under CC BY 3.0 " + "attribution": "Map tiles by CartoDb, under CC BY 3.0 " }, "esriworldimagery": { "id": "esriworldimagery", @@ -142,14 +142,14 @@ "osm_grey": { "id": "osm_grey", "thumbgallery": "img/basemap/osm_grey.png", - "title": "GéoBretagne - GéoGrandEst", + "title": "GéoBretagne - DataGrandEst", "label": "OpenStreetMap style noir et blanc", "type": "WMTS", "url": "https://tile.geobretagne.fr/osm/service", "layers": "osm:grey", "format": "image/png", "fromcapacity": "false", - "attribution": "GéoBretagne - GéoGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", + "attribution": "GéoBretagne - DataGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", "style": "normal", "matrixset": "PM", "maxzoom": "22" @@ -181,7 +181,7 @@ "baseref": "https://geobretagne.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" }, { - "title": "Catalogue de la Région Grand Est", + "title": "Catalogue DataGrandEst", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", "baseref": "https://datagrandest.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" } diff --git a/config-python-sample.json b/config-python-sample.json index f134ab8e..0786e50d 100644 --- a/config-python-sample.json +++ b/config-python-sample.json @@ -2,7 +2,7 @@ "app_conf": { "studio_title": "Mviewer Studio", "mviewer_version": "4", - "mviewerstudio_version": "4.3-snapshot", + "mviewerstudio_version": "4.2.1", "api": "api/app", "store_style_service": "api/style", "mviewer_instance": "/mviewer/", @@ -77,7 +77,7 @@ "type": "OSM", "url": "https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png", "maxzoom": "20", - "attribution": "Map tiles by CartoDb, under CC BY 3.0 " + "attribution": "Map tiles by CartoDb, under CC BY 3.0 " }, "esriworldimagery": { "id": "esriworldimagery", @@ -136,14 +136,14 @@ "osm_grey": { "id": "osm_grey", "thumbgallery": "img/basemap/osm_grey.png", - "title": "GéoBretagne - GéoGrandEst", + "title": "GéoBretagne - DataGrandEst", "label": "OpenStreetMap style noir et blanc", "type": "WMTS", "url": "https://tile.geobretagne.fr/osm/service", "layers": "osm:grey", "format": "image/png", "fromcapacity": "false", - "attribution": "GéoBretagne - GéoGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", + "attribution": "GéoBretagne - DataGrandEst. Données : les contributeurs d'OpenStreetMap , ODbL ", "style": "normal", "matrixset": "PM", "maxzoom": "22" @@ -175,7 +175,7 @@ "baseref": "https://geobretagne.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" }, { - "title": "Catalogue de la Région Grand Est", + "title": "Catalogue DataGrandEst", "url": "https://datagrandest.fr/geonetwork/srv/fre/csw", "baseref": "https://datagrandest.fr/geonetwork/srv/eng/catalog.search?node=srv#/metadata/" } diff --git a/themes.json b/themes.json index e5723f70..6a2719ea 100644 --- a/themes.json +++ b/themes.json @@ -12,7 +12,7 @@ }, { "mviewer": "Démo Isochrones", - "url": "http://kartenn.region-bretagne.fr/kartoviz/demo/isochrones.xml", + "url": "http://kartenn.region-bretagne.fr/kartoviz/demo/isochroneAddon.xml", "themes": [ { "id": "isochrones", "label": "Calcul isochrones" From c2dde1128bca1829bd124dedf3a096da2a12990b Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 18 Feb 2026 15:10:03 +0100 Subject: [PATCH 08/14] create swagger and doc about (#394) Co-authored-by: gaetanbrl --- docs/doc_tech/dev_corner.rst | 36 +- docs/doc_tech/install_python.rst | 58 +- srv/python/mviewerstudio_backend/route.py | 50 +- srv/python/mviewerstudio_backend/swagger.yaml | 726 ++++++++++++++---- 4 files changed, 706 insertions(+), 164 deletions(-) diff --git a/docs/doc_tech/dev_corner.rst b/docs/doc_tech/dev_corner.rst index 052c51fd..c8c01d2b 100644 --- a/docs/doc_tech/dev_corner.rst +++ b/docs/doc_tech/dev_corner.rst @@ -21,8 +21,8 @@ Prérequis - Disposer des droits d'exécution en local (via Flask) - Mviewer doit être installé et accessible en local (avec droit d'écriture) -Généralités ------------ +Généralités +----------- Le debugger VS Code permet d'utiliser le virtualenv (répertoire .venv) installé dans le répertoire `srv/python`. @@ -33,11 +33,33 @@ https://docs.posit.co/ide/server-pro/user/vs-code/guide/python-environments.html Vous pouvez également consultez cette documentation sur le debugger Python dans VS code : -https://code.visualstudio.com/docs/python/tutorial-flask - - -Configuration du debugger VS Code ---------------------------------- +https://code.visualstudio.com/docs/python/tutorial-flask + + +Synchronisation API / Swagger +----------------------------- + +Toute modification des routes backend (fichier ``srv/python/mviewerstudio_backend/route.py``) +doit être synchronisée dans la spécification Swagger/OpenAPI +(``srv/python/mviewerstudio_backend/swagger.yaml``). + +Accès à Swagger +--------------- + +Une fois le backend démarré, vous pouvez accéder à la documentation API : + +- Swagger UI : ``http://localhost:5007/swagger`` (ou ``/swagger/``) +- Spécification OpenAPI : ``http://localhost:5007/swagger.yaml`` + +Si un préfixe d'URL est configuré (ex: ``MVIEWERSTUDIO_URL_PATH_PREFIX=mviewerstudio``), +les URLs deviennent : + +- Swagger UI : ``http://localhost:5007/mviewerstudio/swagger`` +- Spécification OpenAPI : ``http://localhost:5007/mviewerstudio/swagger.yaml`` + + +Configuration du debugger VS Code +--------------------------------- 1. Ouvrir le répertoire /srv/python/ dans VS Code. 2. Ouvrir le fichier .vscode/launch.json (voir la section suivante si non existant) diff --git a/docs/doc_tech/install_python.rst b/docs/doc_tech/install_python.rst index dac79c6e..47afe4c7 100644 --- a/docs/doc_tech/install_python.rst +++ b/docs/doc_tech/install_python.rst @@ -19,13 +19,13 @@ Vous aurez besoin : - d'installer les dépendances (Linux/Debian): -.. code-block:: sh - - sudo apt install libxslt1-dev libxml2-dev python3 python3-pip python3-venv - pip install virtualenv - -- d'une version Python >= 3.9 -- d'une instance mviewer fonctionnelle (/mviewer) +.. code-block:: sh + + sudo apt install libxslt1-dev libxml2-dev python3 python3-pip python3-venv + pip install virtualenv + +- d'une version Python >= 3.11 +- d'une instance mviewer fonctionnelle (/mviewer) Procédures d'installation ========================= @@ -55,10 +55,10 @@ Installation manuelle mkdir -p "${STATIC_DIR}/apps" #Copie des dossiers ressources dans le dossier static cp -r css img js lib index.html mviewerstudio.i18n.json "${STATIC_DIR}" - #Création de l'environnement virtuel Python et installation des dépendances - cd srv/python - python3 -m venv .venv - source .venv/bin/activate + #Création de l'environnement virtuel Python et installation des dépendances + cd srv/python + python3 -m venv .venv + source .venv/bin/activate pip install -r requirements.txt -r dev-requirements.txt pip install -e . @@ -133,8 +133,8 @@ Ce paramètre est accessible dans : -Lancement de l'application avec Flask -===================================== +Lancement de l'application avec Flask +===================================== .. code-block:: sh @@ -146,13 +146,31 @@ Lancement de l'application avec Flask export EXPORT_CONF_FOLDER=/home/monuser/mviewer/apps/store/ export MVIEWERSTUDIO_PUBLISH_PATH=/home/monuser/mviewer/apps/prod export CONF_PUBLISH_PATH_FROM_MVIEWER=apps/prod - export DEFAULT_ORG=megalis - flask run -p 5007 - - - -Mise en production -****************** + export DEFAULT_ORG=megalis + flask run -p 5007 + + +Documentation Swagger (API) +=========================== + +Le backend Python expose une interface Swagger UI ainsi que le fichier OpenAPI : + +- Swagger UI : ``/swagger`` (ou ``/swagger/``) +- Spécification OpenAPI : ``/swagger.yaml`` + +Exemples : + +- sans préfixe d'URL : ``http://localhost:5007/swagger`` +- avec ``MVIEWERSTUDIO_URL_PATH_PREFIX=mviewerstudio`` : ``http://localhost:5007/mviewerstudio/swagger`` + +.. note:: + Ces routes sont servies directement par Flask via ``mviewerstudio_backend/route.py``. + Le fichier de spécification est ``srv/python/mviewerstudio_backend/swagger.yaml``. + + + +Mise en production +****************** **Cette partie décrit l'installation en production de mviewerstudio sur un serveur Linux (Ubuntu / Debian) avec le backend python.** diff --git a/srv/python/mviewerstudio_backend/route.py b/srv/python/mviewerstudio_backend/route.py index b87366e6..859e4fd3 100644 --- a/srv/python/mviewerstudio_backend/route.py +++ b/srv/python/mviewerstudio_backend/route.py @@ -1,4 +1,13 @@ -from flask import Blueprint, jsonify, Response, request, current_app, redirect +from flask import ( + Blueprint, + jsonify, + Response, + request, + current_app, + redirect, + render_template_string, + send_from_directory, +) from .utils.login_utils import current_user from .utils.config_utils import ( Config, @@ -48,6 +57,45 @@ def default_doc(): return redirect("index.html") +@basic_store.route("/swagger", methods=["GET"]) +@basic_store.route("/swagger/", methods=["GET"]) +def swagger_ui() -> Response: + """ + Serve Swagger UI for backend API. + """ + return render_template_string( + """ + + + + + + MviewerStudio API - Swagger + + + +
+ + + + + """ + ) + + +@basic_store.route("/swagger.yaml", methods=["GET"]) +def swagger_spec() -> Response: + """ + Serve OpenAPI specification file. + """ + return send_from_directory(path.dirname(__file__), "swagger.yaml") + + @basic_store.route("/api/user", methods=["GET"]) def user() -> Response: """ diff --git a/srv/python/mviewerstudio_backend/swagger.yaml b/srv/python/mviewerstudio_backend/swagger.yaml index 541cefdb..e74506b9 100644 --- a/srv/python/mviewerstudio_backend/swagger.yaml +++ b/srv/python/mviewerstudio_backend/swagger.yaml @@ -1,207 +1,661 @@ -openapi: 3.0.0 +openapi: 3.0.3 info: - title: Mviewerstudio API - description: Description de l'API de [mviewerstudio](https://github.com/mviewer/mviewerstudio). + title: MviewerStudio Backend API + description: API HTTP du backend MviewerStudio. version: 1.0.0 servers: - - url: https://kartenn.region-bretagne.fr/mviewerstudio/ - description: GéoBretagne instance - - url: https://gis.jdev.fr/mviewerstudio/# - description: JDev Instance (tests, développements) + - url: / + +tags: + - name: User + - name: Apps + - name: Versions + - name: Publication + - name: Templates + - name: Files + - name: Proxy + paths: - /proxy: + /api/user: get: - summary: Dev proxy path - description: Use as proxy for OGC URL and avoid local CORS error + tags: [User] + summary: Return current authenticated user + description: Return informations from headers (sec-proxy / geOrchestra style). + responses: + '200': + description: User payload + content: + application/json: + schema: + $ref: '#/components/schemas/User' + + /api/app: + post: + tags: [Apps] + summary: Create XML configuration + description: Create a new app configuration from XML payload. + requestBody: + required: true + content: + text/xml: + schema: + type: string + example: "..." + responses: + '200': + description: App created + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOrUpdateAppResponse' + '400': + $ref: '#/components/responses/BadRequest' + put: + tags: [Apps] + summary: Update XML configuration + description: Read XML UUID and update register and local filesystem. parameters: - - name: url + - name: message in: query - description: 'Target URL' - required: true + required: false schema: type: string + description: Custom commit message. + requestBody: + required: true + content: + text/xml: + schema: + type: string + example: "..." responses: - '200': # status code - description: URL Content - '405': # status code - description: not allowed + '200': + description: App updated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/CreateOrUpdateAppResponse' + - type: object + properties: + diff: + type: string '400': - description: Bad request - url param missing - /api/user: + $ref: '#/components/responses/BadRequest' get: - summary: User informations. - description: Return informations from header (e.g georchestra header CAS informations) + tags: [Apps] + summary: List stored configurations + description: Return all mviewer configs for current user org, optionally filtered by search. + parameters: + - name: search + in: query + required: false + schema: + type: string responses: - '200': # status code - description: SUCCESS + '200': + description: List of configs content: application/json: - schema: + schema: type: array - items: - type: string - /api/app: + items: + $ref: '#/components/schemas/ConfigSummary' + + /api/app/{id}: + delete: + tags: [Apps] + summary: Delete one mviewer config + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Delete result + content: + application/json: + schema: + type: object + properties: + deleted_files: + type: integer + success: + type: boolean + deleted_publish: + nullable: true + oneOf: + - type: boolean + - type: string + '400': + $ref: '#/components/responses/BadRequest' + '405': + $ref: '#/components/responses/MethodNotAllowed' + + /api/app/{id}/publish/{name}: post: - summary: Create new app + tags: [Publication] + summary: Publish configuration + description: Copy XML and assets from draft workspace to publication directory. + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/PublishName' + - name: instance + in: query + required: false + schema: + type: string + description: Optional instance root path used to rewrite templates URLs. requestBody: - description: Will save new app in dedicated directory + required: true content: text/xml: schema: - $ref: '#/components/schemas/Config' - required: true + type: string + responses: + '200': + description: Publish result + content: + application/json: + schema: + type: object + properties: + online_file: + type: string + draft_file: + type: string + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: Conflict (already exists with inconsistent relation) + delete: + tags: [Publication] + summary: Unpublish configuration + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/PublishName' responses: '200': - description: Successful operation + description: Unpublish result content: application/json: - examples: - config: - value: - config: - creator: anonymous - date: '2023-02-23T14:18:25.727427' - id: e507378c1fe7 - keywords: a,b,c,d - subject: '' - title: My new project title - url: /e507378c1fe7/My_new_project_title.xml - versions: - - '1' - - '2' - - '3' - filepath: /e507378c1fe7/My_new_project_title.xml - success: true + schema: + type: object + properties: + online_file: + type: string + draft_file: + type: string + '400': + $ref: '#/components/responses/BadRequest' + /api/app/{id}/versions: + get: + tags: [Versions] + summary: Get all app versions + description: Gets all tags and commits for a given UUID application. + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Versions payload + content: + application/json: + schema: + type: object + properties: + versions: + type: object + properties: + tags: + type: array + items: + $ref: '#/components/schemas/VersionTag' + commits: + type: array + items: + $ref: '#/components/schemas/VersionCommit' + config: + type: object + additionalProperties: true '400': - description: Bad request - XML missing + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/version/{version}: + put: + tags: [Versions] + summary: Switch app version + description: Switch current app workspace to a commit or tag. + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/Version' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + as_new: + type: boolean + responses: + '200': + description: Switch result content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "No XML found in the request body !" + schema: + type: object + properties: + success: + type: boolean + message: + type: string + detached: + type: boolean + '400': + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/version/{version}/preview: get: - summary: Return all available apps + tags: [Versions] + summary: Preview a specific version + description: Copy XML and assets of a specific version into preview directory and return preview URL. + parameters: + - $ref: '#/components/parameters/AppId' + - $ref: '#/components/parameters/Version' responses: '200': - description: successful operation + description: Preview URL + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + file: + type: string + '400': + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/preview: + post: + tags: [Versions] + summary: Preview uncommitted XML + description: Create a temporary preview XML from request body without saving changes. + parameters: + - $ref: '#/components/parameters/AppId' + requestBody: + required: true + content: + text/xml: + schema: + type: string + responses: + '200': + description: Preview file path + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + file: + type: string + + /api/app/{id}/version: + post: + tags: [Versions] + summary: Create a tag for app + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Tag created + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + '400': + $ref: '#/components/responses/BadRequest' delete: - summary: To delete apps - description: 'Pass a list of apps ids to delete' + tags: [Versions] + summary: Delete app versions + description: Delete each app versions except master branch based on request payload. parameters: - - name: ids - in: query - description: List of apps to delete - required: true - schema: - $ref: '#/components/schemas/ArrayOfInt' + - $ref: '#/components/parameters/AppId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [versions] + properties: + versions: + type: array + items: + type: string responses: + '200': + description: Number of deleted versions + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + deleted: + type: integer '400': - description: ids param missing + $ref: '#/components/responses/BadRequest' - /api/app/{id}/version: + /api/style: post: - summary: Create a new app version + tags: [Files] + summary: Store SLD style + description: Store SLD style content locally and return generated filepath. + requestBody: + required: true + content: + text/plain: + schema: + type: string + responses: + '200': + description: Stored style path + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + filepath: + type: string + + /api/app/{id}/template/{file_name}: + post: + tags: [Templates] + summary: Add layer template parameters: - - name: id + - $ref: '#/components/parameters/AppId' + - name: file_name in: path - description: Target app ID required: true schema: type: string + requestBody: + required: true + content: + text/plain: + schema: + type: string responses: '200': - description: successful operation - '400': - description: Bad request - Incorrect version name + description: Template stored path content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "This version ID does not exists !'" + schema: + type: object + properties: + success: + type: boolean + filepath: + type: string + '400': + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/template/{id_layer}: delete: - summary: Delete versions from list - description: 'Will delete all release excepted latest' + tags: [Templates] + summary: Delete layer template parameters: - - name: id + - $ref: '#/components/parameters/AppId' + - name: id_layer in: path - description: Target app ID required: true schema: type: string - - name: versions - in: query - description: List of versions to delete - required: true - schema: - $ref: '#/components/schemas/ArrayOfInt' responses: '200': - description: successful operation + description: Template removed + content: + application/json: + schema: + type: object + properties: + success: + type: boolean '400': - description: Bad request - Incorrect version name + $ref: '#/components/responses/BadRequest' + + /api/app/{id}/exists: + get: + tags: [Apps] + summary: Check if app exists + parameters: + - $ref: '#/components/parameters/AppId' + responses: + '200': + description: Existence check content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "Version does not exists!" - /api/app/{id}/version/{version}: - put: - summary: Change current app version + schema: + type: object + properties: + success: + type: boolean + exists: + type: boolean + + /api/download/{id}: + get: + tags: [Files] + summary: Generate zip download link for app parameters: - - name: id - in: path - description: IDs of config to use - required: true - schema: - type: string - - name: version - in: path - description: version number to delete - required: true - schema: - type: integer + - $ref: '#/components/parameters/AppId' responses: '200': - description: successful operation - '400': - description: Bad request - Incorrect version name + description: Download link content: application/json: - examples: - badRequest: - value: - name: "Bad Request" - description: "Version does not exists!" - /api/app/all: - delete: - summary: Delete all apps metadata - description: Will clean store + schema: + type: object + properties: + success: + type: boolean + url: + type: string + name: + type: string + '400': + $ref: '#/components/responses/BadRequest' + + /proxy/: + get: + tags: [Proxy] + summary: Proxy service (GET) + description: Proxy content from a whitelisted origin. + parameters: + - $ref: '#/components/parameters/ProxyUrl' responses: '200': - description: successful operation + description: Proxied raw response + content: + '*/*': + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '405': + $ref: '#/components/responses/MethodNotAllowed' + post: + tags: [Proxy] + summary: Proxy service (POST) + description: Forward XML body to a whitelisted origin. + parameters: + - $ref: '#/components/parameters/ProxyUrl' + requestBody: + required: true + content: + application/xml: + schema: + type: string + responses: + '200': + description: Proxied raw response + content: + '*/*': + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '405': + $ref: '#/components/responses/MethodNotAllowed' components: + parameters: + AppId: + name: id + in: path + required: true + schema: + type: string + description: Application UUID. + Version: + name: version + in: path + required: true + schema: + type: string + description: Git tag or commit SHA. + PublishName: + name: name + in: path + required: true + schema: + type: string + description: Publication name. + ProxyUrl: + name: url + in: query + required: true + schema: + type: string + format: uri + description: Target URL to proxy. + + responses: + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + name: Bad Request + description: Invalid request payload + MethodNotAllowed: + description: Method not allowed + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + name: Method Not Allowed + description: Not allowed ! + schemas: - ArrayOfInt: - type: array - items: - type: integer - format: int64 - example: 1 - Config: + Error: + type: object + properties: + name: + type: string + description: + type: string + + User: + type: object + properties: + user_name: + type: string + first_name: + type: string + last_name: + type: string + organisation: + type: object + properties: + legal_name: + nullable: true + type: string + roles: + type: array + items: + type: string + + ConfigSummary: + type: object + additionalProperties: true + properties: + id: + type: string + title: + type: string + description: + type: string + publisher: + type: string + creator: + type: string + url: + type: string + link: + type: string + + CreateOrUpdateAppResponse: + type: object + properties: + success: + type: boolean + filepath: + type: string + config: + type: object + additionalProperties: true + + VersionTag: type: object - xml: - name: config properties: - mviewerstudioversion: + name: + type: string + message: + type: string + commit: + type: string + + VersionCommit: + type: object + properties: + message: + type: string + id: + type: string + author: + type: string + date: + type: string + current: + type: boolean + publication: + type: string + tag: type: string - example: 3.2 - xml: - attribute: true \ No newline at end of file From 409b29f79e62992c4902c901f3066ba06651e07d Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 18 Feb 2026 15:22:02 +0100 Subject: [PATCH 09/14] up python dep and version (#395) Co-authored-by: gaetanbrl --- .github/workflows/build.yml | 4 ++-- srv/python/README.md | 3 +-- srv/python/requirements.txt | 2 +- srv/python/setup.py | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13b0ffba..876c6744 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,9 +11,9 @@ jobs: - uses: actions/checkout@v2 - name: "Set up Python" - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' - name: "python backend - install requirements" working-directory: srv/python diff --git a/srv/python/README.md b/srv/python/README.md index b0ebe084..b005802d 100644 --- a/srv/python/README.md +++ b/srv/python/README.md @@ -1,7 +1,7 @@ # MViewerStudio Backend v2 Ce dossier contient la version 2 du backend de mviewerstudio. Cette version est -écrite en python (3.7+) et utilise le framework Flask. Il n'y aucune base de +écrite en python (3.11+) et utilise le framework Flask. Il n'y aucune base de données, les données sont stockées dans des fichiers json. ## Installation @@ -19,4 +19,3 @@ Vous pouvez utiliser la composition docker présente à la racine du dépot. Le * Lancer les tests unitaires : `pytest mviewerstudio_backend/test.py` * Vérifier les types : `mypy --ignore-missing mviewerstudio_backend` - diff --git a/srv/python/requirements.txt b/srv/python/requirements.txt index 0a397685..f10a22c0 100644 --- a/srv/python/requirements.txt +++ b/srv/python/requirements.txt @@ -1,5 +1,5 @@ flask>=2 -gunicorn>20,<21 +gunicorn>20 lxml>=4.9.2 GitPython>=3 requests diff --git a/srv/python/setup.py b/srv/python/setup.py index eb1c20d8..0c94bcda 100644 --- a/srv/python/setup.py +++ b/srv/python/setup.py @@ -12,11 +12,11 @@ url="https://github.com/mviewer/mviewerstudio", packages=find_packages(), install_requires=REQUIREMENTS, + python_requires=">=3.11", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.11", "Framework :: Flask", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", From 1b432db51a526c568c629cf17ca3e88ec9ee84b1 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 18 Feb 2026 15:26:46 +0100 Subject: [PATCH 10/14] black py files --- srv/python/mviewerstudio_backend/error_handlers.py | 1 - srv/python/mviewerstudio_backend/register_utils.py | 1 - 2 files changed, 2 deletions(-) diff --git a/srv/python/mviewerstudio_backend/error_handlers.py b/srv/python/mviewerstudio_backend/error_handlers.py index 6bd3c257..31675e00 100644 --- a/srv/python/mviewerstudio_backend/error_handlers.py +++ b/srv/python/mviewerstudio_backend/error_handlers.py @@ -4,7 +4,6 @@ import json import logging - logger = logging.getLogger(__name__) diff --git a/srv/python/mviewerstudio_backend/register_utils.py b/srv/python/mviewerstudio_backend/register_utils.py index 0db4e4ba..15c850b8 100644 --- a/srv/python/mviewerstudio_backend/register_utils.py +++ b/srv/python/mviewerstudio_backend/register_utils.py @@ -7,7 +7,6 @@ import git - logger = logging.getLogger(__name__) From 633aa27da1425bfc92d75c1061c8f509044f1b93 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 18 Feb 2026 15:26:46 +0100 Subject: [PATCH 11/14] lint route py file --- srv/python/mviewerstudio_backend/route.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/srv/python/mviewerstudio_backend/route.py b/srv/python/mviewerstudio_backend/route.py index 859e4fd3..1f35f0f2 100644 --- a/srv/python/mviewerstudio_backend/route.py +++ b/srv/python/mviewerstudio_backend/route.py @@ -63,8 +63,7 @@ def swagger_ui() -> Response: """ Serve Swagger UI for backend API. """ - return render_template_string( - """ + return render_template_string(""" @@ -84,8 +83,7 @@ def swagger_ui() -> Response: - """ - ) + """) @basic_store.route("/swagger.yaml", methods=["GET"]) From e71a896f6ff393a5e0e2a9f402c6c0d23340b378 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 18 Feb 2026 15:40:00 +0100 Subject: [PATCH 12/14] add worker ref --- docs/doc_tech/install_python.rst | 80 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/docs/doc_tech/install_python.rst b/docs/doc_tech/install_python.rst index 47afe4c7..6e3f75c7 100644 --- a/docs/doc_tech/install_python.rst +++ b/docs/doc_tech/install_python.rst @@ -19,13 +19,13 @@ Vous aurez besoin : - d'installer les dépendances (Linux/Debian): -.. code-block:: sh - - sudo apt install libxslt1-dev libxml2-dev python3 python3-pip python3-venv - pip install virtualenv - -- d'une version Python >= 3.11 -- d'une instance mviewer fonctionnelle (/mviewer) +.. code-block:: sh + + sudo apt install libxslt1-dev libxml2-dev python3 python3-pip python3-venv + pip install virtualenv + +- d'une version Python >= 3.11 +- d'une instance mviewer fonctionnelle (/mviewer) Procédures d'installation ========================= @@ -55,10 +55,10 @@ Installation manuelle mkdir -p "${STATIC_DIR}/apps" #Copie des dossiers ressources dans le dossier static cp -r css img js lib index.html mviewerstudio.i18n.json "${STATIC_DIR}" - #Création de l'environnement virtuel Python et installation des dépendances - cd srv/python - python3 -m venv .venv - source .venv/bin/activate + #Création de l'environnement virtuel Python et installation des dépendances + cd srv/python + python3 -m venv .venv + source .venv/bin/activate pip install -r requirements.txt -r dev-requirements.txt pip install -e . @@ -133,8 +133,8 @@ Ce paramètre est accessible dans : -Lancement de l'application avec Flask -===================================== +Lancement de l'application avec Flask +===================================== .. code-block:: sh @@ -146,31 +146,31 @@ Lancement de l'application avec Flask export EXPORT_CONF_FOLDER=/home/monuser/mviewer/apps/store/ export MVIEWERSTUDIO_PUBLISH_PATH=/home/monuser/mviewer/apps/prod export CONF_PUBLISH_PATH_FROM_MVIEWER=apps/prod - export DEFAULT_ORG=megalis - flask run -p 5007 - - -Documentation Swagger (API) -=========================== - -Le backend Python expose une interface Swagger UI ainsi que le fichier OpenAPI : - -- Swagger UI : ``/swagger`` (ou ``/swagger/``) -- Spécification OpenAPI : ``/swagger.yaml`` - -Exemples : - -- sans préfixe d'URL : ``http://localhost:5007/swagger`` -- avec ``MVIEWERSTUDIO_URL_PATH_PREFIX=mviewerstudio`` : ``http://localhost:5007/mviewerstudio/swagger`` - -.. note:: - Ces routes sont servies directement par Flask via ``mviewerstudio_backend/route.py``. - Le fichier de spécification est ``srv/python/mviewerstudio_backend/swagger.yaml``. - - - -Mise en production -****************** + export DEFAULT_ORG=megalis + flask run -p 5007 + + +Documentation Swagger (API) +=========================== + +Le backend Python expose une interface Swagger UI ainsi que le fichier OpenAPI : + +- Swagger UI : ``/swagger`` (ou ``/swagger/``) +- Spécification OpenAPI : ``/swagger.yaml`` + +Exemples : + +- sans préfixe d'URL : ``http://localhost:5007/swagger`` +- avec ``MVIEWERSTUDIO_URL_PATH_PREFIX=mviewerstudio`` : ``http://localhost:5007/mviewerstudio/swagger`` + +.. note:: + Ces routes sont servies directement par Flask via ``mviewerstudio_backend/route.py``. + Le fichier de spécification est ``srv/python/mviewerstudio_backend/swagger.yaml``. + + + +Mise en production +****************** **Cette partie décrit l'installation en production de mviewerstudio sur un serveur Linux (Ubuntu / Debian) avec le backend python.** @@ -226,6 +226,9 @@ Ajoutez ensuite ce contenu en adaptant les valeurs (chemin, user...) selon votre fichier `mviewerstudio.service` + .. warning:: + Nous conseillons de laisser la valeur du worker à 1 (voir issue #389) + .. code-block:: sh [Unit] @@ -243,6 +246,7 @@ fichier `mviewerstudio.service` WorkingDirectory=/home/monuser/mviewerstudio/srv/python ExecStart=/home/monuser/mviewerstudio/srv/python/.venv/bin/gunicorn \ -b 127.0.0.1:5007 \ + --workers=1 \ --access-logfile /var/log/mviewerstudio/gunicorn-access.log \ --log-level info \ --error-logfile /var/log/mviewerstudio/gunicorn-error.log \ From efc8c9eb6149649a992037a9eac86abf21f16074 Mon Sep 17 00:00:00 2001 From: gaetanbrl Date: Wed, 18 Feb 2026 15:43:35 +0100 Subject: [PATCH 13/14] docker default 1 worker --- docker/Dockerfile-python-backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-python-backend b/docker/Dockerfile-python-backend index 4340adba..7413051a 100644 --- a/docker/Dockerfile-python-backend +++ b/docker/Dockerfile-python-backend @@ -44,4 +44,4 @@ ENV EXPORT_CONF_FOLDER=/home/apprunner/apps/store \ # You will probably have to override this one on runtime with your custom config COPY config-python-sample.json mviewerstudio_backend/static/apps/config.json -CMD ["gunicorn", "-w 4", "-b 0.0.0.0:8000", "mviewerstudio_backend.app:app"] +CMD ["gunicorn", "-w 1", "-b 0.0.0.0:8000", "mviewerstudio_backend.app:app"] From fa2b6fca9769ea26bec7d8c302d4002d10dc7697 Mon Sep 17 00:00:00 2001 From: Mathilde Marchand Date: Wed, 18 Feb 2026 16:24:54 +0100 Subject: [PATCH 14/14] prevent uncheck if only 1 bl checked --- lib/mv.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mv.js b/lib/mv.js index 7e02b9a3..04cd4279 100755 --- a/lib/mv.js +++ b/lib/mv.js @@ -321,6 +321,12 @@ var mv = (function () { $("#frm-bl").append(html); $("#frm-bl-visible").append(html2); $(".bl." + classe + " input").bind("change", function (e) { + var checkbox = $(e.currentTarget); + var allChecked = $(".bl." + classe + " input:checked"); + if (!checkbox.prop("checked") && allChecked.length === 0) { + checkbox.prop("checked", true); + return; + } var id = $(this).parent().parent().attr("data-layerid"); var value = $(e.currentTarget).prop("checked"); var select = $("#frm-bl-visible");