From 7c203784347c786b1b287e3845eff719e8f33e65 Mon Sep 17 00:00:00 2001 From: SJiB Date: Fri, 8 Sep 2023 17:07:03 +0200 Subject: [PATCH 01/13] Add option to select development2020 datamodel master branch https://github.com/teksi/wastewater/archive/refs/heads/master.zip --- qgepplugin/gui/qgepdatamodeldialog.py | 1 + qgepplugin/qgepqwat2ili | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 3a818e3..273776d 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -65,6 +65,7 @@ AVAILABLE_RELEASES.update( { "master": "https://github.com/QGEP/datamodel/archive/master.zip", + "development2020": "https://github.com/teksi/wastewater/archive/refs/heads/master.zip", } ) diff --git a/qgepplugin/qgepqwat2ili b/qgepplugin/qgepqwat2ili index df62333..110c8ef 160000 --- a/qgepplugin/qgepqwat2ili +++ b/qgepplugin/qgepqwat2ili @@ -1 +1 @@ -Subproject commit df62333539ec7197becb1ad0f604a35f29c8b98b +Subproject commit 110c8ef31139fc3cde005c34979c478aa0f4d99f From a64aba505bd50f93ac7690e034b0f01990d79f03 Mon Sep 17 00:00:00 2001 From: SJiB Date: Fri, 8 Sep 2023 17:13:07 +0200 Subject: [PATCH 02/13] copy of original version --- .../gui/qgepdatamodeldialog_standard.py | 923 ++++++++++++++++++ 1 file changed, 923 insertions(+) create mode 100644 qgepplugin/gui/qgepdatamodeldialog_standard.py diff --git a/qgepplugin/gui/qgepdatamodeldialog_standard.py b/qgepplugin/gui/qgepdatamodeldialog_standard.py new file mode 100644 index 0000000..3a818e3 --- /dev/null +++ b/qgepplugin/gui/qgepdatamodeldialog_standard.py @@ -0,0 +1,923 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# +# Profile +# Copyright (C) 2021 Olivier Dalang +# ----------------------------------------------------------- +# +# licensed under the terms of GNU GPL 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, print to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# --------------------------------------------------------------------- + +import configparser +import functools +import os +import subprocess +import tempfile +import zipfile + +import pkg_resources +import psycopg2 +from qgis.core import ( + Qgis, + QgsApplication, + QgsMessageLog, + QgsNetworkAccessManager, + QgsProject, +) +from qgis.PyQt.QtCore import QFile, QIODevice, QSettings, Qt, QUrl +from qgis.PyQt.QtNetwork import QNetworkReply, QNetworkRequest +from qgis.PyQt.QtWidgets import ( + QApplication, + QDialog, + QMessageBox, + QProgressDialog, + QPushButton, +) + +from ..utils import get_ui_class + +# Currently, the latest release is hard-coded in the plugin, meaning we need +# to publish a plugin update for each datamodel update. +# In the future, once plugin/datamodel versionning scheme clearly reflects +# compatibility, we could retrieve this dynamically, so datamodel bugfix +# releases don't require a plugin upgrade. + +# Allow to choose which releases can be installed +AVAILABLE_RELEASES = { + "1.6.0": f"https://github.com/QGEP/datamodel/archive/1.6.0.zip", +} +if QSettings().value("/QGEP/DeveloperMode", False, type=bool): + AVAILABLE_RELEASES.update( + { + "master": "https://github.com/QGEP/datamodel/archive/master.zip", + } + ) + +# Allows to pick which QGIS project matches the version (will take the biggest <= match) +DATAMODEL_QGEP_VERSIONS = { + "1.6.0": "https://github.com/QGEP/QGEP/releases/download/v10.0.0/qgep-v10.0.0.zip", + "1.5.5": "https://github.com/QGEP/QGEP/releases/download/v9.0.3/qgep-v9.0.3.zip", + "1.5.0": "https://github.com/QGEP/QGEP/releases/download/v8.0/qgep.zip", + "1.4.0": "https://github.com/QGEP/QGEP/releases/download/v7.0/qgep.zip", + "0": "https://github.com/QGEP/QGEP/releases/download/v6.2/qgep.zip", +} +TEMP_DIR = os.path.join(tempfile.gettempdir(), "QGEP", "datamodel-init") + +# Path for pg_service.conf +PG_CONFIG_PATH_KNOWN = True +if os.environ.get("PGSERVICEFILE"): + PG_CONFIG_PATH = os.environ.get("PGSERVICEFILE") +elif os.environ.get("PGSYSCONFDIR"): + PG_CONFIG_PATH = os.path.join(os.environ.get("PGSYSCONFDIR"), "pg_service.conf") +elif os.path.exists("~/.pg_service.conf"): + PG_CONFIG_PATH = "~/.pg_service.conf" +else: + PG_CONFIG_PATH_KNOWN = False + PG_CONFIG_PATH = os.path.join( + QgsApplication.qgisSettingsDirPath(), "pg_service.conf" + ) + +# Derived urls/paths, may require adaptations if release structure changes +DATAMODEL_URL_TEMPLATE = "https://github.com/QGEP/datamodel/archive/{}.zip" +REQUIREMENTS_PATH_TEMPLATE = os.path.join(TEMP_DIR, "datamodel-{}", "requirements.txt") +DELTAS_PATH_TEMPLATE = os.path.join(TEMP_DIR, "datamodel-{}", "delta") +INIT_SCRIPT_URL_TEMPLATE = "https://github.com/QGEP/datamodel/releases/download/{}/qgep_{}_structure_with_value_lists.sql" +QGEP_PROJECT_PATH_TEMPLATE = os.path.join(TEMP_DIR, "project", "qgep.qgs") + + +def qgep_datamodel_error_catcher(func): + """Display QGEPDatamodelError in error messages rather than normal exception dialog""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except QGEPDatamodelError as e: + args[0]._show_error(str(e)) + + return wrapper + + +class QGEPDatamodelError(Exception): + pass + + +class QgepPgserviceEditorDialog(QDialog, get_ui_class("qgeppgserviceeditordialog.ui")): + def __init__(self, cur_name, cur_config, taken_names): + super().__init__() + self.setupUi(self) + self.taken_names = taken_names + self.nameLineEdit.textChanged.connect(self.check_name) + self.pgconfigUserCheckBox.toggled.connect(self.pgconfigUserLineEdit.setEnabled) + self.pgconfigPasswordCheckBox.toggled.connect( + self.pgconfigPasswordLineEdit.setEnabled + ) + + self.nameLineEdit.setText(cur_name) + self.pgconfigHostLineEdit.setText(cur_config.get("host", "")) + self.pgconfigPortLineEdit.setText(cur_config.get("port", "")) + self.pgconfigDbLineEdit.setText(cur_config.get("dbname", "")) + self.pgconfigUserLineEdit.setText(cur_config.get("user", "")) + self.pgconfigPasswordLineEdit.setText(cur_config.get("password", "")) + + self.pgconfigUserCheckBox.setChecked(cur_config.get("user") is not None) + self.pgconfigPasswordCheckBox.setChecked(cur_config.get("password") is not None) + self.pgconfigUserLineEdit.setEnabled(cur_config.get("user") is not None) + self.pgconfigPasswordLineEdit.setEnabled(cur_config.get("password") is not None) + + self.check_name(cur_name) + + def check_name(self, new_text): + if new_text in self.taken_names: + self.nameCheckLabel.setText("will overwrite") + self.nameCheckLabel.setStyleSheet( + "color: rgb(170, 95, 0);\nfont-weight: bold;" + ) + else: + self.nameCheckLabel.setText("will be created") + self.nameCheckLabel.setStyleSheet( + "color: rgb(0, 170, 0);\nfont-weight: bold;" + ) + + def conf_name(self): + return self.nameLineEdit.text() + + def conf_dict(self): + retval = { + "host": self.pgconfigHostLineEdit.text(), + "port": self.pgconfigPortLineEdit.text(), + "dbname": self.pgconfigDbLineEdit.text(), + } + if self.pgconfigUserCheckBox.isChecked(): + retval.update( + { + "user": self.pgconfigUserLineEdit.text(), + } + ) + if self.pgconfigPasswordCheckBox.isChecked(): + retval.update( + { + "password": self.pgconfigPasswordLineEdit.text(), + } + ) + return retval + + +class QgepDatamodelInitToolDialog(QDialog, get_ui_class("qgepdatamodeldialog.ui")): + def __init__(self, parent=None): + QDialog.__init__(self, parent) + self.setupUi(self) + + self.progress_dialog = None + + # Populate the versions + self.releaseVersionComboBox.clear() + if len(AVAILABLE_RELEASES) > 1: + self.releaseVersionComboBox.addItem("- SELECT RELEASE VERSION -") + self.releaseVersionComboBox.model().item(0).setEnabled(False) + for version in sorted(list(AVAILABLE_RELEASES.keys()), reverse=True): + self.releaseVersionComboBox.addItem(version) + self.releaseVersionComboBox.setEnabled(len(AVAILABLE_RELEASES) > 1) + + # Show the pgconfig path + path_label = PG_CONFIG_PATH + if not PG_CONFIG_PATH_KNOWN: + self.pgservicePathLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-style: italic;" + ) + path_label += f"
Note: you must create a PGSYSCONFDIR variable for this configuration to work.More info here." + self.pgservicePathLabel.setTextFormat(Qt.RichText) + self.pgservicePathLabel.setTextInteractionFlags(Qt.TextBrowserInteraction) + self.pgservicePathLabel.setWordWrap(True) + self.pgservicePathLabel.setText(path_label) + + # Connect some signals + + self.releaseVersionComboBox.activated.connect(self.switch_datamodel) + + self.installDepsButton.pressed.connect(self.install_requirements) + + self.pgserviceComboBox.activated.connect(self.select_pgconfig) + self.pgserviceAddButton.pressed.connect(self.add_pgconfig) + + self.targetVersionComboBox.activated.connect(self.check_version) + self.versionUpgradeButton.pressed.connect(self.upgrade_version) + self.initializeButton.pressed.connect(self.initialize_version) + + self.loadProjectButton.pressed.connect(self.load_project) + + # Initialize the checks + self.checks = { + "datamodel": False, + "requirements": False, + "pgconfig": False, + "current_version": False, + "project": False, + } + + if len(AVAILABLE_RELEASES) == 1: + self.switch_datamodel() + + # Properties + + @property + def version(self): + return self.releaseVersionComboBox.currentText() + + @property + def target_version(self): + return self.targetVersionComboBox.currentText() + + @property + def conf(self): + return self.pgserviceComboBox.currentData() + + # Feedback helpers + + def _show_progress(self, message): + if self.progress_dialog is None: + self.progress_dialog = QProgressDialog( + self.tr("Starting..."), self.tr("Cancel"), 0, 0 + ) + cancel_button = QPushButton(self.tr("Cancel")) + cancel_button.setEnabled(False) + self.progress_dialog.setCancelButton(cancel_button) + self.progress_dialog.setLabelText(message) + self.progress_dialog.show() + QApplication.processEvents() + + def _done_progress(self): + self.progress_dialog.close() + self.progress_dialog.deleteLater() + self.progress_dialog = None + QApplication.processEvents() + + def _show_error(self, message): + self._done_progress() + err = QMessageBox() + err.setText(message) + err.setIcon(QMessageBox.Warning) + err.exec_() + + # Actions helpers + + def _run_sql( + self, + connection_string, + sql_command, + autocommit=False, + error_message="Psycopg error, see logs for more information", + ): + QgsMessageLog.logMessage( + f"Running query against {connection_string}: {sql_command}", "QGEP" + ) + try: + conn = psycopg2.connect(connection_string) + if autocommit: + conn.autocommit = True + cur = conn.cursor() + cur.execute(sql_command) + conn.commit() + cur.close() + conn.close() + except psycopg2.OperationalError as e: + message = f"{error_message}\nCommand :\n{sql_command}\n{e}" + raise QGEPDatamodelError(message) + + def _run_cmd( + self, + shell_command, + cwd=None, + error_message="Subprocess error, see logs for more information", + timeout=10, + ): + """ + Helper to run commands through subprocess + """ + QgsMessageLog.logMessage(f"Running command : {shell_command}", "QGEP") + result = subprocess.run( + shell_command, + cwd=cwd, + shell=True, + capture_output=True, + timeout=timeout, + ) + if result.stdout: + stdout = result.stdout.decode("utf-8", errors="replace") + QgsMessageLog.logMessage(stdout, "QGEP") + else: + stdout = None + if result.stderr: + stderr = result.stderr.decode("utf-8", errors="replace") + QgsMessageLog.logMessage(stderr, "QGEP", level=Qgis.Critical) + else: + stderr = None + if result.returncode: + message = f"{error_message}\nCommand :\n{shell_command}" + message += f"\n\nOutput :\n{stdout}" + message += f"\n\nError :\n{stderr}" + raise QGEPDatamodelError(message) + return stdout + + def _download(self, url, filename, error_message=None): + os.makedirs(TEMP_DIR, exist_ok=True) + + network_manager = QgsNetworkAccessManager.instance() + reply = network_manager.blockingGet(QNetworkRequest(QUrl(url))) + if reply.error() != QNetworkReply.NoError: + if error_message: + error_message = f"{error_message}\n{reply.errorString()}" + else: + error_message = reply.errorString() + raise QGEPDatamodelError(error_message) + download_path = os.path.join(TEMP_DIR, filename) + QgsMessageLog.logMessage(f"Downloading {url} to {download_path}", "QGEP") + download_file = QFile(download_path) + download_file.open(QIODevice.WriteOnly) + download_file.write(reply.content()) + download_file.close() + return download_file.fileName() + + def _read_pgservice(self): + config = configparser.ConfigParser() + if os.path.exists(PG_CONFIG_PATH): + config.read(PG_CONFIG_PATH) + return config + + def _write_pgservice_conf(self, service_name, config_dict): + config = self._read_pgservice() + config[service_name] = config_dict + + class EqualsSpaceRemover: + # see https://stackoverflow.com/a/25084055/13690651 + output_file = None + + def __init__(self, output_file): + self.output_file = output_file + + def write(self, content): + content = content.replace(" = ", "=", 1) + self.output_file.write(content.encode("utf-8")) + + config.write(EqualsSpaceRemover(open(PG_CONFIG_PATH, "wb"))) + + def _get_current_version(self): + # Dirty parsing of pum info + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + if not os.path.exists(deltas_dir): + return None + + pum_info = self._run_cmd( + f"python3 -m pum info -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir}", + error_message="Could not get current version, are you sure the database is accessible ?", + ) + version = None + for line in pum_info.splitlines(): + line = line.strip() + if not line: + continue + parts = line.split("|") + if len(parts) > 1: + version = parts[1].strip() + return version + + # Display + + def showEvent(self, event): + self.update_pgconfig_combobox() + self.check_datamodel() + self.check_requirements() + self.check_pgconfig() + self.check_version() + self.check_project() + super().showEvent(event) + + def enable_buttons_if_ready(self): + self.installDepsButton.setEnabled( + self.checks["datamodel"] and not self.checks["requirements"] + ) + self.versionUpgradeButton.setEnabled(all(self.checks.values())) + self.loadProjectButton.setEnabled(self.checks["project"]) + + # Datamodel + + def check_datamodel(self): + requirements_exists = os.path.exists( + REQUIREMENTS_PATH_TEMPLATE.format(self.version) + ) + deltas_exists = os.path.exists(DELTAS_PATH_TEMPLATE.format(self.version)) + + check = requirements_exists and deltas_exists + + if check: + if self.version == "master": + self.releaseCheckLabel.setText( + "DEV RELEASE - DO NOT USE FOR PRODUCTION" + ) + self.releaseCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + else: + self.releaseCheckLabel.setText("ok") + self.releaseCheckLabel.setStyleSheet( + "color: rgb(0, 170, 0);\nfont-weight: bold;" + ) + else: + self.releaseCheckLabel.setText("not found") + self.releaseCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + self.checks["datamodel"] = check + self.enable_buttons_if_ready() + + return check + + @qgep_datamodel_error_catcher + def switch_datamodel(self, _=None): + # Download the datamodel if it doesn't exist + + if not self.check_datamodel(): + + self._show_progress("Downloading the release") + + # Download files + datamodel_path = self._download( + AVAILABLE_RELEASES[self.version], "datamodel.zip" + ) + + # Unzip + datamodel_zip = zipfile.ZipFile(datamodel_path) + datamodel_zip.extractall(TEMP_DIR) + + # Cleanup + # os.unlink(datamodel_path) + + # Update UI + self.check_datamodel() + + self._done_progress() + + self.check_requirements() + self.check_version() + self.check_project() + + # Requirements + + def check_requirements(self): + + missing = [] + if not self.check_datamodel(): + missing.append(("unknown", "no datamodel")) + else: + requirements = pkg_resources.parse_requirements( + open(REQUIREMENTS_PATH_TEMPLATE.format(self.version)) + ) + for requirement in requirements: + try: + pkg_resources.require(str(requirement)) + except pkg_resources.DistributionNotFound: + missing.append((requirement, "missing")) + except pkg_resources.VersionConflict: + missing.append((requirement, "conflict")) + + check = len(missing) == 0 + + if check: + self.pythonCheckLabel.setText("ok") + self.pythonCheckLabel.setStyleSheet( + "color: rgb(0, 170, 0);\nfont-weight: bold;" + ) + else: + self.pythonCheckLabel.setText( + "\n".join(f"{dep}: {err}" for dep, err in missing) + ) + self.pythonCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + self.checks["requirements"] = check + self.enable_buttons_if_ready() + + return check + + @qgep_datamodel_error_catcher + def install_requirements(self): + + # TODO : Ideally, this should be done in a venv, as to avoid permission issues and/or modification + # of libraries versions that could affect other parts of the system. + # We could initialize a venv in the user's directory, and activate it. + # It's almost doable when only running commands from the command line (in which case we could + # just prepent something like `path/to/venv/Scripts/activate && ` to commands, /!\ syntax differs on Windows), + # but to be really useful, it would be best to then enable the virtualenv from within python directly. + # It seems venv doesn't provide a way to do so, while virtualenv does + # (see https://stackoverflow.com/a/33637378/13690651) + # but virtualenv isn't in the stdlib... So we'd have to install it globally ! Argh... + # Anyway, pip deps support should be done in QGIS one day so all plugins can benefit. + # In the mean time we just install globally and hope for the best. + + self._show_progress("Installing python dependencies with pip") + + # Install dependencies + requirements_file_path = REQUIREMENTS_PATH_TEMPLATE.format(self.version) + QgsMessageLog.logMessage( + f"Installing python dependencies from {requirements_file_path}", "QGEP" + ) + dependencies = " ".join( + [ + f'"{l.strip()}"' + for l in open(requirements_file_path, "r").read().splitlines() + if l.strip() + ] + ) + command_line = "the OSGeo4W shell" if os.name == "nt" else "the terminal" + self._run_cmd( + f"python3 -m pip install --user {dependencies}", + error_message=f"Could not install python dependencies. You can try to run the command manually from {command_line}.", + timeout=None, + ) + + self._done_progress() + + # Update UI + self.check_requirements() + + # Pgservice + + def check_pgconfig(self): + + check = bool(self.pgserviceComboBox.currentData()) + if check: + self.pgconfigCheckLabel.setText("ok") + self.pgconfigCheckLabel.setStyleSheet( + "color: rgb(0, 170, 0);\nfont-weight: bold;" + ) + else: + self.pgconfigCheckLabel.setText("not set") + self.pgconfigCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + self.checks["pgconfig"] = check + self.enable_buttons_if_ready() + + return check + + def add_pgconfig(self): + taken_names = self._read_pgservice().sections() + if self.conf in self._read_pgservice(): + cur_config = self._read_pgservice()[self.conf] + else: + cur_config = {} + + add_dialog = QgepPgserviceEditorDialog(self.conf, cur_config, taken_names) + if add_dialog.exec_() == QDialog.Accepted: + name = add_dialog.conf_name() + conf = add_dialog.conf_dict() + self._write_pgservice_conf(name, conf) + self.update_pgconfig_combobox() + self.pgserviceComboBox.setCurrentIndex( + self.pgserviceComboBox.findData(name) + ) + self.select_pgconfig() + + def update_pgconfig_combobox(self): + self.pgserviceComboBox.clear() + for config_name in self._read_pgservice().sections(): + self.pgserviceComboBox.addItem(config_name, config_name) + self.pgserviceComboBox.setCurrentIndex(0) + + def select_pgconfig(self, _=None): + config = self._read_pgservice() + if self.conf in config.sections(): + host = config.get(self.conf, "host", fallback="-") + port = config.get(self.conf, "port", fallback="-") + dbname = config.get(self.conf, "dbname", fallback="-") + user = config.get(self.conf, "user", fallback="-") + password = ( + len(config.get(self.conf, "password", fallback="")) * "*" + ) or "-" + self.pgserviceCurrentLabel.setText( + f"host: {host}:{port}\ndbname: {dbname}\nuser: {user}\npassword: {password}" + ) + else: + self.pgserviceCurrentLabel.setText("-") + self.check_pgconfig() + self.check_version() + self.check_project() + + # Version + + @qgep_datamodel_error_catcher + def check_version(self, _=None): + check = False + + # target version + + # (re)populate the combobox + prev = self.targetVersionComboBox.currentText() + self.targetVersionComboBox.clear() + available_versions = set() + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + if os.path.exists(deltas_dir): + for f in os.listdir(deltas_dir): + if f.startswith("delta_"): + available_versions.add(f.split("_")[1]) + for available_version in sorted(list(available_versions), reverse=True): + self.targetVersionComboBox.addItem(available_version) + self.targetVersionComboBox.setCurrentText(prev) # restore + + target_version = self.targetVersionComboBox.currentText() + + # current version + + self.initializeButton.setVisible(False) + self.targetVersionComboBox.setVisible(True) + self.versionUpgradeButton.setVisible(True) + + pgservice = self.pgserviceComboBox.currentData() + if not pgservice: + self.versionCheckLabel.setText("service not selected") + self.versionCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + elif not available_versions: + self.versionCheckLabel.setText("no delta in datamodel") + self.versionCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + else: + + error = None + current_version = None + connection_works = True + + try: + current_version = self._get_current_version() + except QGEPDatamodelError: + # Can happend if PUM is not initialized, unfortunately we can't really + # determine if this is a connection error or if PUM is not initailized + # see https://github.com/opengisch/pum/issues/96 + # We'll try to connect to see if it's a connection error + error = "qgep not initialized" + try: + self._run_sql( + f"service={self.conf}", + "SELECT 1;", + error_message="Errors when initializing the database.", + ) + except QGEPDatamodelError: + error = "database does not exist" + try: + self._run_sql( + f"service={self.conf} dbname=postgres", + "SELECT 1;", + error_message="Errors when initializing the database.", + ) + except QGEPDatamodelError: + error = "could not connect to database" + connection_works = False + + if not connection_works: + check = False + self.versionCheckLabel.setText(error) + self.versionCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + elif error is not None: + check = False + self.versionCheckLabel.setText(error) + self.versionCheckLabel.setStyleSheet( + "color: rgb(170, 95, 0);\nfont-weight: bold;" + ) + elif current_version <= target_version: + check = True + self.versionCheckLabel.setText(current_version) + self.versionCheckLabel.setStyleSheet( + "color: rgb(0, 170, 0);\nfont-weight: bold;" + ) + elif current_version > target_version: + check = False + self.versionCheckLabel.setText(f"{current_version} (cannot downgrade)") + self.versionCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + else: + check = False + self.versionCheckLabel.setText(f"{current_version} (invalid version)") + self.versionCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + self.initializeButton.setVisible( + current_version is None and connection_works + ) + self.targetVersionComboBox.setVisible(current_version is not None) + self.versionUpgradeButton.setVisible(current_version is not None) + + self.checks["current_version"] = check + self.enable_buttons_if_ready() + + return check + + @qgep_datamodel_error_catcher + def initialize_version(self): + + confirm = QMessageBox() + confirm.setText( + f"You are about to initialize the datamodel on {self.conf} to version {self.version}. " + ) + confirm.setInformativeText( + "Please confirm that you have a backup of your data as this operation can result in data loss." + ) + confirm.setStandardButtons(QMessageBox.Apply | QMessageBox.Cancel) + confirm.setIcon(QMessageBox.Warning) + + if confirm.exec_() == QMessageBox.Apply: + + self._show_progress("Initializing the datamodel") + + srid = self.sridLineEdit.text() + + # If we can't get current version, it's probably that the DB is not initialized + # (or maybe we can't connect, but we can't know easily with PUM) + + self._show_progress("Initializing the datamodel") + + # TODO : this should be done by PUM directly (see https://github.com/opengisch/pum/issues/94) + # also currently SRID doesn't work + try: + self._show_progress("Downloading the structure script") + url = INIT_SCRIPT_URL_TEMPLATE.format(self.version, self.version) + sql_path = self._download( + url, + f"structure_with_value_lists-{self.version}-{srid}.sql", + error_message=f"Initialization release file not found for version {self.version}", + ) + + # Dirty hack to customize SRID in a dump + if srid != "2056": + with open(sql_path, "r") as file: + contents = file.read() + contents = contents.replace("2056", srid) + with open(sql_path, "w") as file: + file.write(contents) + + try: + conn = psycopg2.connect(f"service={self.conf}") + except psycopg2.Error: + # It may be that the database doesn't exist yet + # in that case, we try to connect to the postgres database and to create it from there + self._show_progress("Creating the database") + dbname = self._read_pgservice()[self.conf]["dbname"] + self._run_sql( + f"service={self.conf} dbname=postgres", + f"CREATE DATABASE {dbname};", + autocommit=True, + error_message="Could not create a new database.", + ) + + self._show_progress("Running the initialization scripts") + self._run_sql( + f"service={self.conf}", + "CREATE EXTENSION IF NOT EXISTS postgis;", + error_message="Errors when initializing the database.", + ) + # we cannot use this, as it doesn't support COPY statements + # this means we'll run through psql without transaction :-/ + # cur.execute(open(sql_path, "r").read()) + self._run_cmd( + f'psql -f {sql_path} "service={self.conf}"', + error_message="Errors when initializing the database.", + timeout=300, + ) + # workaround until https://github.com/QGEP/QGEP/issues/612 is fixed + self._run_sql( + f"service={self.conf}", + "SELECT qgep_network.refresh_network_simple();", + error_message="Errors when initializing the database.", + ) + + except psycopg2.Error as e: + raise QGEPDatamodelError(str(e)) + + self.check_version() + self.check_project() + + self._done_progress() + + success = QMessageBox() + success.setText("Datamodel successfully initialized") + success.setIcon(QMessageBox.Information) + success.exec_() + + @qgep_datamodel_error_catcher + def upgrade_version(self): + + confirm = QMessageBox() + confirm.setText( + f"You are about to update the datamodel on {self.conf} to version {self.target_version}. " + ) + confirm.setInformativeText( + "Please confirm that you have a backup of your data as this operation can result in data loss." + ) + confirm.setStandardButtons(QMessageBox.Apply | QMessageBox.Cancel) + confirm.setIcon(QMessageBox.Warning) + + if confirm.exec_() == QMessageBox.Apply: + + self._show_progress("Upgrading the datamodel") + + srid = self.sridLineEdit.text() + + self._show_progress("Running pum upgrade") + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + self._run_cmd( + f"python3 -m pum upgrade -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", + cwd=os.path.dirname(deltas_dir), + error_message="Errors when upgrading the database.", + timeout=300, + ) + + self.check_version() + self.check_project() + + self._done_progress() + + success = QMessageBox() + success.setText("Datamodel successfully upgraded") + success.setIcon(QMessageBox.Information) + success.exec_() + + # Project + + @qgep_datamodel_error_catcher + def check_project(self): + + try: + current_version = self._get_current_version() + except QGEPDatamodelError: + # Can happend if PUM is not initialized, unfortunately we can't really + # determine if this is a connection error or if PUM is not initailized + # see https://github.com/opengisch/pum/issues/96 + current_version = None + + check = current_version is not None + + if check: + self.projectCheckLabel.setText("ok") + self.projectCheckLabel.setStyleSheet( + "color: rgb(0, 170, 0);\nfont-weight: bold;" + ) + else: + self.projectCheckLabel.setText("version not found") + self.projectCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + + self.checks["project"] = check + self.enable_buttons_if_ready() + + return check + + @qgep_datamodel_error_catcher + def load_project(self): + + current_version = self._get_current_version() + + url = None + for dm_vers in sorted(DATAMODEL_QGEP_VERSIONS): + if dm_vers <= current_version: + url = DATAMODEL_QGEP_VERSIONS[dm_vers] + + qgep_path = self._download(url, "qgep.zip") + qgep_zip = zipfile.ZipFile(qgep_path) + qgep_zip.extractall(TEMP_DIR) + + with open(QGEP_PROJECT_PATH_TEMPLATE, "r") as original_project: + contents = original_project.read() + + # replace the service name + contents = contents.replace("service='pg_qgep'", f"service='{self.conf}'") + + output_file = tempfile.NamedTemporaryFile(suffix=".qgs", delete=False) + output_file.write(contents.encode("utf8")) + + QgsProject.instance().read(output_file.name) From 57593b369be5195e8cfe7476b17ce8a5e73a55e0 Mon Sep 17 00:00:00 2001 From: SJiB Date: Fri, 8 Sep 2023 21:12:17 +0200 Subject: [PATCH 03/13] adjust PATH variables for datamodel2020 --- qgepplugin/gui/qgepdatamodeldialog.py | 87 ++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 273776d..56ce77d 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -65,7 +65,7 @@ AVAILABLE_RELEASES.update( { "master": "https://github.com/QGEP/datamodel/archive/master.zip", - "development2020": "https://github.com/teksi/wastewater/archive/refs/heads/master.zip", + "datamodel2020": "https://github.com/teksi/wastewater/archive/refs/heads/datamodel2020.zip", } ) @@ -95,6 +95,15 @@ # Derived urls/paths, may require adaptations if release structure changes DATAMODEL_URL_TEMPLATE = "https://github.com/QGEP/datamodel/archive/{}.zip" + +# add other path structure for datamodel2020 +REQUIREMENTS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\\requirements.txt") +DELTAS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\delta") +#https://raw.githubusercontent.com/teksi/wastewater/datamodel2020/datamodel/qgep_datamodel2020_structure_with_value_lists.sql +INIT_SCRIPT_URL_TEMPLATE2 = "https://raw.githubusercontent.com/teksi/wastewater/{}/datamodel/qgep_{}_structure_with_value_lists.sql" + + +# qgep paths REQUIREMENTS_PATH_TEMPLATE = os.path.join(TEMP_DIR, "datamodel-{}", "requirements.txt") DELTAS_PATH_TEMPLATE = os.path.join(TEMP_DIR, "datamodel-{}", "delta") INIT_SCRIPT_URL_TEMPLATE = "https://github.com/QGEP/datamodel/releases/download/{}/qgep_{}_structure_with_value_lists.sql" @@ -379,8 +388,15 @@ def write(self, content): def _get_current_version(self): # Dirty parsing of pum info - deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + # 8.9.2023 + if self.version == "datamodel2020": + deltas_dir = DELTAS_PATH_TEMPLATE2.format(self.version) + QgsMessageLog.logMessage(f"DELTAS_PATH_TEMPLATE2 {deltas_dir} does match with downloaded version {format(self.version)} !", "QGEP") + else: + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) if not os.path.exists(deltas_dir): + # 9.8.2023 + QgsMessageLog.logMessage(f"DELTAS_PATH_TEMPLATE {deltas_dir} does not match with downloaded version {format(self.version)} !", "QGEP") return None pum_info = self._run_cmd( @@ -418,13 +434,33 @@ def enable_buttons_if_ready(self): # Datamodel def check_datamodel(self): - requirements_exists = os.path.exists( + + if self.version == "datamodel2020": + requirements_exists = os.path.exists( + REQUIREMENTS_PATH_TEMPLATE2.format(self.version) + ) + else: + requirements_exists = os.path.exists( REQUIREMENTS_PATH_TEMPLATE.format(self.version) ) - deltas_exists = os.path.exists(DELTAS_PATH_TEMPLATE.format(self.version)) + if not requirements_exists: + QgsMessageLog.logMessage(f"requirements_exists not true {REQUIREMENTS_PATH_TEMPLATE} / {REQUIREMENTS_PATH_TEMPLATE2}", "QGEP") + if self.version == "datamodel2020": + deltas_exists = os.path.exists(DELTAS_PATH_TEMPLATE2.format(self.version)) + else: + deltas_exists = os.path.exists(DELTAS_PATH_TEMPLATE.format(self.version)) + + if not deltas_exists: + QgsMessageLog.logMessage(f"deltas_exists not true {DELTAS_PATH_TEMPLATE} / {DELTAS_PATH_TEMPLATE2}", "QGEP") + check = requirements_exists and deltas_exists + if check: + QgsMessageLog.logMessage(f"check OK {self.version}", "QGEP") + else: + QgsMessageLog.logMessage(f"check not ok {self.version} / requirements_exists {requirements_exists} / deltas_exists {deltas_exists}", "QGEP") + if check: if self.version == "master": self.releaseCheckLabel.setText( @@ -433,6 +469,20 @@ def check_datamodel(self): self.releaseCheckLabel.setStyleSheet( "color: rgb(170, 0, 0);\nfont-weight: bold;" ) + elif self.version == "datamodel2020": + self.releaseCheckLabel.setText( + "DEV 2020 RELEASE - DO NOT USE FOR PRODUCTION" + ) + self.releaseCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) + elif self.version == "1.7.0": + self.releaseCheckLabel.setText( + "DEV 2020 RELEASE - DO NOT USE FOR PRODUCTION" + ) + self.releaseCheckLabel.setStyleSheet( + "color: rgb(170, 0, 0);\nfont-weight: bold;" + ) else: self.releaseCheckLabel.setText("ok") self.releaseCheckLabel.setStyleSheet( @@ -486,9 +536,16 @@ def check_requirements(self): if not self.check_datamodel(): missing.append(("unknown", "no datamodel")) else: - requirements = pkg_resources.parse_requirements( - open(REQUIREMENTS_PATH_TEMPLATE.format(self.version)) - ) + #8.9.2023 + if self.version == "datamodel2020": + requirements = pkg_resources.parse_requirements( + open(REQUIREMENTS_PATH_TEMPLATE2.format(self.version)) + ) + else: + requirements = pkg_resources.parse_requirements( + open(REQUIREMENTS_PATH_TEMPLATE.format(self.version)) + ) + for requirement in requirements: try: pkg_resources.require(str(requirement)) @@ -634,7 +691,13 @@ def check_version(self, _=None): prev = self.targetVersionComboBox.currentText() self.targetVersionComboBox.clear() available_versions = set() - deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + + # 9.8.2023 + if self.version == "datamodel2020": + deltas_dir = DELTAS_PATH_TEMPLATE2.format(self.version) + else: + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + if os.path.exists(deltas_dir): for f in os.listdir(deltas_dir): if f.startswith("delta_"): @@ -766,7 +829,13 @@ def initialize_version(self): # also currently SRID doesn't work try: self._show_progress("Downloading the structure script") - url = INIT_SCRIPT_URL_TEMPLATE.format(self.version, self.version) + + # 9.8.2023 + if self.version == "datamodel2020": + url = INIT_SCRIPT_URL_TEMPLATE2.format(self.version, self.version) + else: + url = INIT_SCRIPT_URL_TEMPLATE.format(self.version, self.version) + sql_path = self._download( url, f"structure_with_value_lists-{self.version}-{srid}.sql", From 814906af09ce712cc451591377aa5cbf8f0b3429 Mon Sep 17 00:00:00 2001 From: SJiB Date: Fri, 8 Sep 2023 21:52:24 +0200 Subject: [PATCH 04/13] temporary take out refresh_network_simple --- qgepplugin/gui/qgepdatamodeldialog.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 56ce77d..a899539 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -878,12 +878,15 @@ def initialize_version(self): error_message="Errors when initializing the database.", timeout=300, ) + + #98.9.2023 + self._show_progress("Skip refresh_network_simple") # workaround until https://github.com/QGEP/QGEP/issues/612 is fixed - self._run_sql( - f"service={self.conf}", - "SELECT qgep_network.refresh_network_simple();", - error_message="Errors when initializing the database.", - ) +# self._run_sql( +# f"service={self.conf}", +# "SELECT qgep_network.refresh_network_simple();", +# error_message="Errors when initializing the database.", +# ) except psycopg2.Error as e: raise QGEPDatamodelError(str(e)) From 4628aa28812abe34aaefb8d5edc861aaaea56fb0 Mon Sep 17 00:00:00 2001 From: SJiB Date: Fri, 8 Sep 2023 22:03:09 +0200 Subject: [PATCH 05/13] DELTAS_PATH_TEMPLATE2 for datamodel2020 --- qgepplugin/gui/qgepdatamodeldialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index a899539..2f89faf 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -921,7 +921,12 @@ def upgrade_version(self): srid = self.sridLineEdit.text() self._show_progress("Running pum upgrade") - deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + # 8.9.2023 + if self.version == "datamodel2020": + deltas_dir = DELTAS_PATH_TEMPLATE2.format(self.version) + else: + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + self._run_cmd( f"python3 -m pum upgrade -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", cwd=os.path.dirname(deltas_dir), From 1c38dbd7d02991c97b0e8ebf80821d6989944825 Mon Sep 17 00:00:00 2001 From: SJiB Date: Wed, 13 Sep 2023 11:50:35 +0800 Subject: [PATCH 06/13] 20230909 temp zipped copy of qgepdatamodeldialog.zip --- qgepplugin/gui/20230909_qgepdatamodeldialog.zip | Bin 0 -> 8955 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 qgepplugin/gui/20230909_qgepdatamodeldialog.zip diff --git a/qgepplugin/gui/20230909_qgepdatamodeldialog.zip b/qgepplugin/gui/20230909_qgepdatamodeldialog.zip new file mode 100644 index 0000000000000000000000000000000000000000..1016e564942c59d5560a878616c884729fc160e6 GIT binary patch literal 8955 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 T|KAqszy1Cn3;$DG0D%7i$$BqL literal 0 HcmV?d00001 From 6dc51531279f6777013006d373c41da7c1b03d9d Mon Sep 17 00:00:00 2001 From: SJiB Date: Wed, 13 Sep 2023 12:03:13 +0800 Subject: [PATCH 07/13] also add self.version Installing python dependencies with pip --- qgepplugin/gui/qgepdatamodeldialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 2f89faf..1ccc164 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -592,7 +592,12 @@ def install_requirements(self): self._show_progress("Installing python dependencies with pip") # Install dependencies - requirements_file_path = REQUIREMENTS_PATH_TEMPLATE.format(self.version) + # 13.9.2023 also add self.version + if self.version == "datamodel2020": + requirements_file_path = REQUIREMENTS_PATH_TEMPLATE2.format(self.version) + else + requirements_file_path = REQUIREMENTS_PATH_TEMPLATE.format(self.version) + QgsMessageLog.logMessage( f"Installing python dependencies from {requirements_file_path}", "QGEP" ) From 0870be7711c9e737b647c161bfa3e8abea75c112 Mon Sep 17 00:00:00 2001 From: SJiB Date: Wed, 13 Sep 2023 12:03:44 +0800 Subject: [PATCH 08/13] correct comment --- qgepplugin/gui/qgepdatamodeldialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 1ccc164..810a681 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -884,7 +884,7 @@ def initialize_version(self): timeout=300, ) - #98.9.2023 + #8.9.2023 self._show_progress("Skip refresh_network_simple") # workaround until https://github.com/QGEP/QGEP/issues/612 is fixed # self._run_sql( From e6d9d2fd843cb66dd9ebcbb3689f65c64cf07c4f Mon Sep 17 00:00:00 2001 From: SJiB Date: Wed, 13 Sep 2023 14:27:55 +0800 Subject: [PATCH 09/13] adding script to set pum baseline - maybe take out again --- qgepplugin/gui/qgepdatamodeldialog.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 810a681..d7a3f7b 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -595,7 +595,7 @@ def install_requirements(self): # 13.9.2023 also add self.version if self.version == "datamodel2020": requirements_file_path = REQUIREMENTS_PATH_TEMPLATE2.format(self.version) - else + else: requirements_file_path = REQUIREMENTS_PATH_TEMPLATE.format(self.version) QgsMessageLog.logMessage( @@ -893,6 +893,19 @@ def initialize_version(self): # error_message="Errors when initializing the database.", # ) + #13.9.2023 new set baseline: pum baseline -p qgep_prod -t qgep_sys.pum_info -d ./delta/ -b 1.0.0 + + if self.version == "datamodel2020": + self._show_progress("Setting baseline with pum") + deltas_dir = DELTAS_PATH_TEMPLATE2.format(self.version) + + self._run_cmd( + #f"python3 -m pum upgrade -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", + f"python3 -m pum baseline -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -b 1.0.0", + cwd=os.path.dirname(deltas_dir), + error_message="Errors when setting baseline with pum in the database.", + timeout=300, + ) except psycopg2.Error as e: raise QGEPDatamodelError(str(e)) From ee46d55e7bf7c0ba3115088a707eba1a80e2b810 Mon Sep 17 00:00:00 2001 From: SJiB Date: Mon, 18 Sep 2023 10:23:19 +0800 Subject: [PATCH 10/13] backup qgepdatamodeldialog.zip --- qgepplugin/gui/20230913_qgepdatamodeldialog.zip | Bin 0 -> 9088 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 qgepplugin/gui/20230913_qgepdatamodeldialog.zip diff --git a/qgepplugin/gui/20230913_qgepdatamodeldialog.zip b/qgepplugin/gui/20230913_qgepdatamodeldialog.zip new file mode 100644 index 0000000000000000000000000000000000000000..14e6005f6b067e27ea103b376e33677005bd368f GIT binary patch literal 9088 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 Date: Thu, 5 Oct 2023 15:42:51 +0200 Subject: [PATCH 11/13] Adapt datamodel tool for tww_sys instead of qgep_sys --- qgepplugin/gui/qgepdatamodeldialog.py | 47 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index d7a3f7b..64591ed 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -399,10 +399,18 @@ def _get_current_version(self): QgsMessageLog.logMessage(f"DELTAS_PATH_TEMPLATE {deltas_dir} does not match with downloaded version {format(self.version)} !", "QGEP") return None - pum_info = self._run_cmd( - f"python3 -m pum info -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir}", - error_message="Could not get current version, are you sure the database is accessible ?", - ) + # 5.10.2023 adapt qgep_sys with tww_sys + if self.version == "datamodel2020": + pum_info = self._run_cmd( + f"python3 -m pum info -p {self.conf} -t tww_sys.pum_info -d {deltas_dir}", + error_message="Could not get current version, are you sure the database tww is accessible ?", + ) + else: + pum_info = self._run_cmd( + f"python3 -m pum info -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir}", + error_message="Could not get current version, are you sure the database qgep is accessible ?", + ) + version = None for line in pum_info.splitlines(): line = line.strip() @@ -893,7 +901,7 @@ def initialize_version(self): # error_message="Errors when initializing the database.", # ) - #13.9.2023 new set baseline: pum baseline -p qgep_prod -t qgep_sys.pum_info -d ./delta/ -b 1.0.0 + # 13.9.2023 new set baseline: pum baseline -p qgep_prod -t qgep_sys.pum_info -d ./delta/ -b 1.0.0 if self.version == "datamodel2020": self._show_progress("Setting baseline with pum") @@ -901,11 +909,15 @@ def initialize_version(self): self._run_cmd( #f"python3 -m pum upgrade -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", - f"python3 -m pum baseline -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -b 1.0.0", + # 5.10.2023 adapt to tww_sys + #f"python3 -m pum baseline -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -b 1.0.0", + f"python3 -m pum baseline -p {self.conf} -t tww_sys.pum_info -d {deltas_dir} -b 1.0.0", cwd=os.path.dirname(deltas_dir), - error_message="Errors when setting baseline with pum in the database.", + error_message="Errors when setting baseline with pum in the database tww.", timeout=300, ) + + except psycopg2.Error as e: raise QGEPDatamodelError(str(e)) @@ -942,15 +954,24 @@ def upgrade_version(self): # 8.9.2023 if self.version == "datamodel2020": deltas_dir = DELTAS_PATH_TEMPLATE2.format(self.version) - else: - deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) - - self._run_cmd( - f"python3 -m pum upgrade -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", + + # 5.10.2023 adapt qgep_sys to tww_sys + self._run_cmd( + f"python3 -m pum upgrade -p {self.conf} -t tww_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", cwd=os.path.dirname(deltas_dir), - error_message="Errors when upgrading the database.", + error_message="Errors when upgrading the database tww.", timeout=300, ) + else: + deltas_dir = DELTAS_PATH_TEMPLATE.format(self.version) + + # 5.10.2023 moved into else clause + self._run_cmd( + f"python3 -m pum upgrade -p {self.conf} -t qgep_sys.pum_info -d {deltas_dir} -u {self.target_version} -v int SRID {srid}", + cwd=os.path.dirname(deltas_dir), + error_message="Errors when upgrading the database qgep.", + timeout=300, + ) self.check_version() self.check_project() From e87d4746c1c4919980e66b8e7ba8d7efef536a8e Mon Sep 17 00:00:00 2001 From: SJiB Date: Thu, 5 Oct 2023 16:27:32 +0200 Subject: [PATCH 12/13] fix DELTA_PATH_TEMPLATE2 invalid escape --- qgepplugin/gui/qgepdatamodeldialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qgepplugin/gui/qgepdatamodeldialog.py b/qgepplugin/gui/qgepdatamodeldialog.py index 64591ed..ded42d9 100644 --- a/qgepplugin/gui/qgepdatamodeldialog.py +++ b/qgepplugin/gui/qgepdatamodeldialog.py @@ -98,7 +98,12 @@ # add other path structure for datamodel2020 REQUIREMENTS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\\requirements.txt") -DELTAS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\delta") + +# 5.10.2023 neu \\delta warning:C:\Users/Stefan/AppData/Roaming/QGIS/QGIS3\profiles\default/python/plugins\qgepplugin\gui\qgepdatamodeldialog.py:101: DeprecationWarning: invalid escape sequence \d +# DELTAS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\delta") + +#DELTAS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\delta") +DELTAS_PATH_TEMPLATE2 = os.path.join(TEMP_DIR, "wastewater-{}", "datamodel\\delta") #https://raw.githubusercontent.com/teksi/wastewater/datamodel2020/datamodel/qgep_datamodel2020_structure_with_value_lists.sql INIT_SCRIPT_URL_TEMPLATE2 = "https://raw.githubusercontent.com/teksi/wastewater/{}/datamodel/qgep_{}_structure_with_value_lists.sql" From e22462c3fdd7acb4fdefa4864173b527b69d4e22 Mon Sep 17 00:00:00 2001 From: SJiB Date: Fri, 6 Oct 2023 09:29:16 +0200 Subject: [PATCH 13/13] backup qgepdatamodeldialog temporarly --- qgepplugin/gui/20231004_qgepdatamodeldialog.zip | Bin 0 -> 9353 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 qgepplugin/gui/20231004_qgepdatamodeldialog.zip diff --git a/qgepplugin/gui/20231004_qgepdatamodeldialog.zip b/qgepplugin/gui/20231004_qgepdatamodeldialog.zip new file mode 100644 index 0000000000000000000000000000000000000000..4b4cd0b92bd64150bfa47babf77e94a4830208d8 GIT binary patch literal 9353 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%0DE17az{zo<}w$Ab@Xbt6hXT2nK5jZ@- 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