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..b22e5108 --- /dev/null +++ b/Mergin/qgis_properties_version_4.py @@ -0,0 +1,78 @@ +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_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, + ) + + 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]) # nosec B314 + + +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."): + 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(project_file: str) -> Dict: + root = _read_xml_tree_from_project(project_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 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 00000000..2e1a09f7 Binary files /dev/null and b/tests/data/test_variables_4.0.qgz differ 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