From dcc51d391c19c351119b69f739d7ee287b642586 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 24 Apr 2026 16:01:58 +0200 Subject: [PATCH 1/4] fix reading Mergin project settings saved from QGIS 4.0 to 3.x Co-authored-by: Copilot --- Mergin/project_settings_widget.py | 7 +++ Mergin/qgis_properties_version_4.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 Mergin/qgis_properties_version_4.py diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py index 4f499f9f..31c6470b 100644 --- a/Mergin/project_settings_widget.py +++ b/Mergin/project_settings_widget.py @@ -20,6 +20,7 @@ QgsMapLayer, QgsVectorLayer, QgsFieldProxyModel, + Qgis, ) from qgis.gui import ( QgsOptionsWidgetFactory, @@ -51,6 +52,7 @@ excluded_layers_list, get_fields_for_checkbox, ) +from .qgis_properties_version_4 import read_mergin_properties, is_qgis_version_4 ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_config.ui") ProjectConfigUiWidget, _ = uic.loadUiType(ui_file) @@ -82,6 +84,11 @@ def __init__(self, parent=None): QgsOptionsPageWidget.__init__(self, parent) self.setupUi(self) + if Qgis.versionInt() < 40000 and is_qgis_version_4(QgsProject.instance().fileName()): + props = read_mergin_properties(QgsProject.instance().fileName()) + for key, value in props.items(): + QgsProject.instance().writeEntry("Mergin", key, value) + self.cmb_photo_quality.addItem("Original", 0) self.cmb_photo_quality.addItem("High (approx. 2-4 Mb)", 1) self.cmb_photo_quality.addItem("Medium (approx. 1-2 Mb)", 2) diff --git a/Mergin/qgis_properties_version_4.py b/Mergin/qgis_properties_version_4.py new file mode 100644 index 00000000..05c14b4b --- /dev/null +++ b/Mergin/qgis_properties_version_4.py @@ -0,0 +1,67 @@ +import xml.etree.ElementTree as ET +from typing import Dict +import zipfile + + +def read_xml_tree_from_qgz(qgz_path: str) -> ET.Element: + qgs_filename = next( + (name for name in zipfile.ZipFile(qgz_path).namelist() if name.endswith(".qgs")), + None, + ) + + if qgs_filename is None: + raise ValueError(f"No .qgs file found inside {qgz_path}") + + with zipfile.ZipFile(qgz_path, "r") as input_zip_file: + entries = {name: input_zip_file.read(name) for name in input_zip_file.namelist()} + + return ET.fromstring(entries[qgs_filename]) + + +def is_qgis_version_4(qgz_file: str) -> bool: + root = read_xml_tree_from_qgz(qgz_file) + + version = root.attrib.get("version", "") + if not version.startswith("4."): + return False + + return True + + +def parse_properties(element, prefix="") -> Dict: + """Recursively parse nested elements into a flat dict with path keys.""" + result = {} + + for child in element: + if child.tag != "properties": + continue + + name = child.attrib.get("name", "") + key = f"{prefix}/{name}" if prefix else name + prop_type = child.attrib.get("type") + + if prop_type is not None: + if prop_type == "QStringList": + result[key] = [v.text for v in child.findall("value")] + else: + result[key] = child.text + else: + result.update(parse_properties(child, prefix=key)) + + return result + + +def read_mergin_properties(qgz_file: str) -> Dict: + root = read_xml_tree_from_qgz(qgz_file) + + version = root.attrib.get("version", "") + if not version.startswith("4."): + return {} + + mergin_elem = root.find(".//properties[@name='Mergin']") + if mergin_elem is None: + return {} + + props = parse_properties(mergin_elem) + + return props From 07d1f1a8791ec19e396a643e17bec52bf980917a Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 24 Apr 2026 16:11:38 +0200 Subject: [PATCH 2/4] fix bandit issue --- Mergin/qgis_properties_version_4.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Mergin/qgis_properties_version_4.py b/Mergin/qgis_properties_version_4.py index 05c14b4b..c574f2a4 100644 --- a/Mergin/qgis_properties_version_4.py +++ b/Mergin/qgis_properties_version_4.py @@ -1,9 +1,10 @@ -import xml.etree.ElementTree as ET +import defusedxml.ElementTree as ET +from xml.etree.ElementTree import Element from typing import Dict import zipfile -def read_xml_tree_from_qgz(qgz_path: str) -> ET.Element: +def read_xml_tree_from_qgz(qgz_path: str) -> Element: qgs_filename = next( (name for name in zipfile.ZipFile(qgz_path).namelist() if name.endswith(".qgs")), None, From 63d98576a4b0942526b8b81860e9e8f7b150fba6 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Sat, 25 Apr 2026 10:06:09 +0200 Subject: [PATCH 3/4] use normal xml functions (defusedxml not in QGIS Windows) and allow also qgs files --- Mergin/qgis_properties_version_4.py | 32 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Mergin/qgis_properties_version_4.py b/Mergin/qgis_properties_version_4.py index c574f2a4..b22e5108 100644 --- a/Mergin/qgis_properties_version_4.py +++ b/Mergin/qgis_properties_version_4.py @@ -1,10 +1,20 @@ -import defusedxml.ElementTree as ET -from xml.etree.ElementTree import Element +import xml.etree.ElementTree as ET # nosec B405 +from xml.etree.ElementTree import Element # nosec B405 from typing import Dict import zipfile -def read_xml_tree_from_qgz(qgz_path: str) -> Element: +def _read_xml_tree_from_project(project_path: str) -> Element: + if project_path.endswith(".qgs"): + with open(project_path, "r", encoding="utf-8") as f: + return ET.parse(f).getroot() # nosec B314 + elif project_path.endswith(".qgz"): + return _read_xml_tree_from_qgz(project_path) + else: + raise ValueError(f"Unsupported project file format: {project_path}") + + +def _read_xml_tree_from_qgz(qgz_path: str) -> Element: qgs_filename = next( (name for name in zipfile.ZipFile(qgz_path).namelist() if name.endswith(".qgs")), None, @@ -16,11 +26,11 @@ def read_xml_tree_from_qgz(qgz_path: str) -> Element: with zipfile.ZipFile(qgz_path, "r") as input_zip_file: entries = {name: input_zip_file.read(name) for name in input_zip_file.namelist()} - return ET.fromstring(entries[qgs_filename]) + return ET.fromstring(entries[qgs_filename]) # nosec B314 -def is_qgis_version_4(qgz_file: str) -> bool: - root = read_xml_tree_from_qgz(qgz_file) +def is_qgis_version_4(project_file: str) -> bool: + root = _read_xml_tree_from_project(project_file) version = root.attrib.get("version", "") if not version.startswith("4."): @@ -29,7 +39,7 @@ def is_qgis_version_4(qgz_file: str) -> bool: return True -def parse_properties(element, prefix="") -> Dict: +def _parse_properties(element, prefix="") -> Dict: """Recursively parse nested elements into a flat dict with path keys.""" result = {} @@ -47,13 +57,13 @@ def parse_properties(element, prefix="") -> Dict: else: result[key] = child.text else: - result.update(parse_properties(child, prefix=key)) + result.update(_parse_properties(child, prefix=key)) return result -def read_mergin_properties(qgz_file: str) -> Dict: - root = read_xml_tree_from_qgz(qgz_file) +def read_mergin_properties(project_file: str) -> Dict: + root = _read_xml_tree_from_project(project_file) version = root.attrib.get("version", "") if not version.startswith("4."): @@ -63,6 +73,6 @@ def read_mergin_properties(qgz_file: str) -> Dict: if mergin_elem is None: return {} - props = parse_properties(mergin_elem) + props = _parse_properties(mergin_elem) return props From caa1fda501243649c840e5d05fb1ec81a7c1aeae Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Sat, 25 Apr 2026 10:06:23 +0200 Subject: [PATCH 4/4] add test Co-authored-by: Copilot --- tests/conftest.py | 6 ++++++ tests/data/test_variables_4.0.qgz | Bin 0 -> 21662 bytes tests/test_qgis_properties_version_4.py | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/data/test_variables_4.0.qgz create mode 100644 tests/test_qgis_properties_version_4.py diff --git a/tests/conftest.py b/tests/conftest.py index f24af13d..bbd90feb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,3 +71,9 @@ def layer_field_filter(test_data_path: Path) -> QgsVectorLayer: layer = QgsVectorLayer(str(test_data_path / "data_field_filter.gpkg"), "field filter layer", "ogr") assert layer.isValid() return layer + + +@pytest.fixture +def qgis_project_4_path(test_data_path: Path) -> Path: + """Fixture for a QGIS project file created with QGIS 4.x.""" + return test_data_path / "test_variables_4.0.qgz" diff --git a/tests/data/test_variables_4.0.qgz b/tests/data/test_variables_4.0.qgz new file mode 100644 index 0000000000000000000000000000000000000000..2e1a09f70219420a3eacf14d4d74fbf292dc4231 GIT binary patch literal 21662 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®$SO$r|g~Zv#S(p3W`U62JfI^x^vD>dDE8)nZ%w zr=zA|WPBt8LP2P?n)aUbHv(UJDt=vb{2lzxqFtm&?Z7W@617VprovBsexTzDeyDTkvX(<2qoGD?=11&QilRMc%@r> zNEU25avyCm2!_278?^bVWsFkvX|L0J%Q8v3xF2&1tL#MM2r(_ruEJ=00}*4P{hi_k zJZ|65UduR3+a-#tp?PrW-wP+qLxPg`^QPHCsjkQl}o8NLkC zJ5ho3J$*gcAh97<3Hx}ry-?|%)dvG8+A-Y|)mg@-dAU_@9MRKxI&j0tqN9~xEN-X+Fj+jBYte6yM!>C zM}d-Dc^7H0k)KFhjH{j<+_a*dx3^ihas3BTDmAh2XSaVC%C2OlV5@2hgQk$0|4rFj z!s01({u}(K@f-YCnn1y|O7xrf_lA&w^0%>5)m2vZxAO4x`@{WvWxF#wqCM|G-|2=Z z^oFGBklpL2RFx{!El^MuWGa8U{V-e=)Bm>7sQh6XefH3q!pi&6JokAyQP3%9d>3cs zJy)zI<>_LX`zxY&mJMODn51#hQ9Xuhi#!#E_#yovqi?{6^$XyaRV@XtXbcTma*99t zs;CI3$JC$F-kdbFZ;U_a%Q-YSceOVRh8J2D2b*08=K(J3p1s&9hGS#T~toF~$dUW{3|qPKg;JO@{IG|Cr=vV@f9Gl7Ih8&B#xqcDt4aR6EM3hx}W) zAm-b7tK?v;38WN$$Sg5FC_f#8ir8sQm^Kxp@9vN0<}KjfE&I_UC#a@-F9UZ(F>W2zj6YXEbP1-()xs}rt`7`YchZP${f9)Bs+2N13yqL zHM76TGh||EG;#@Px7+tm5-oA+fKkdVMUC?vkMx}m4`#KW&u(ohE>E^P`YiI_z36h~ zc(U4eKDlg})NBIehwfu8plrhDih z5qd-eYeJ`Gd(f|5nq}`h?tF>Cl&rr!ZIV0RC4Nw3UU@|LyD$jk9lK5Oe=3E5(*Ngw z3dsML=sairvlGFmE7HHV0{{061lz%X-G4J3VuFWb7HP46V+cMZ>poJSZvKR5S4A9K zBbwWpSu62)gSNkSAP83rWMf2}b9Ewh0EE}%i2gkb^dGAc5Y&hMQU6K?{oncjS4;Vy c{IlPL(SL3*IsjslKM~h{56EvzX7I=R4|N~rYXATM literal 0 HcmV?d00001 diff --git a/tests/test_qgis_properties_version_4.py b/tests/test_qgis_properties_version_4.py new file mode 100644 index 00000000..ce9da9c6 --- /dev/null +++ b/tests/test_qgis_properties_version_4.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# GPLv3 license +# Copyright Lutra Consulting Limited + +from pathlib import Path + +from Mergin.qgis_properties_version_4 import is_qgis_version_4, read_mergin_properties + + +def test_is_qgis_version_4(qgis_project_4_path: Path): + assert is_qgis_version_4(str(qgis_project_4_path)) + + +def test_read_mergin_properties(qgis_project_4_path: Path): + + mergin_properties = read_mergin_properties(str(qgis_project_4_path)) + + assert isinstance(mergin_properties, dict) + assert len(mergin_properties) > 0