From df03437efb76695809179b6ff764dd08af5e9918 Mon Sep 17 00:00:00 2001 From: Miikka Kallio Date: Fri, 25 Oct 2024 12:32:14 +0300 Subject: [PATCH 1/2] Added functionality to load existing plan. Added dialog for loading plan. Added database connection selection, searchable listing of all plans in the database. Filtering of layers based on id of selected plan. --- arho_feature_template/core/exceptions.py | 5 + arho_feature_template/gui/ask_credentials.py | 36 ++++ arho_feature_template/gui/ask_credentials.ui | 66 +++++++ arho_feature_template/gui/load_plan_dialog.py | 178 ++++++++++++++++++ arho_feature_template/gui/load_plan_dialog.ui | 126 +++++++++++++ arho_feature_template/plugin.py | 32 +++- arho_feature_template/utils/db_utils.py | 116 ++++++++++++ 7 files changed, 554 insertions(+), 5 deletions(-) create mode 100644 arho_feature_template/core/exceptions.py create mode 100644 arho_feature_template/gui/ask_credentials.py create mode 100644 arho_feature_template/gui/ask_credentials.ui create mode 100644 arho_feature_template/gui/load_plan_dialog.py create mode 100644 arho_feature_template/gui/load_plan_dialog.ui create mode 100644 arho_feature_template/utils/db_utils.py diff --git a/arho_feature_template/core/exceptions.py b/arho_feature_template/core/exceptions.py new file mode 100644 index 0000000..7b77bfb --- /dev/null +++ b/arho_feature_template/core/exceptions.py @@ -0,0 +1,5 @@ +from arho_feature_template.qgis_plugin_tools.tools.exceptions import QgsPluginException + + +class AuthConfigException(QgsPluginException): + pass diff --git a/arho_feature_template/gui/ask_credentials.py b/arho_feature_template/gui/ask_credentials.py new file mode 100644 index 0000000..5233949 --- /dev/null +++ b/arho_feature_template/gui/ask_credentials.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from importlib import resources + +from qgis.PyQt import uic +from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QLineEdit + +# Load the .ui file path using importlib resources +ui_path = resources.files(__package__) / "ask_credentials.ui" + +# Use uic.loadUiType to load the UI definition from the .ui file +DbAskCredentialsDialogBase, _ = uic.loadUiType(ui_path) + + +class DbAskCredentialsDialog(QDialog, DbAskCredentialsDialogBase): # type: ignore + def __init__(self, parent: QDialog = None): + super().__init__(parent) + + # Set up the UI from the loaded .ui file + self.setupUi(self) + + # The UI elements defined in the .ui file + self.userLineEdit: QLineEdit = self.findChild(QLineEdit, "userLineEdit") + self.pwdLineEdit: QLineEdit = self.findChild(QLineEdit, "pwdLineEdit") + self.buttonBox: QDialogButtonBox = self.findChild(QDialogButtonBox, "buttonBox") + + # Connect the OK and Cancel buttons to their respective functions + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + def get_credentials(self) -> tuple[str, str]: + """ + Returns the entered username and password. + :return: Tuple (username, password) + """ + return self.userLineEdit.text(), self.pwdLineEdit.text() diff --git a/arho_feature_template/gui/ask_credentials.ui b/arho_feature_template/gui/ask_credentials.ui new file mode 100644 index 0000000..cf42490 --- /dev/null +++ b/arho_feature_template/gui/ask_credentials.ui @@ -0,0 +1,66 @@ + + + DbAskCredentialsDialogBase + + + + 0 + 0 + 400 + 200 + + + + Käyttäjän autentikaatio + + + + + + + + Käyttäjä: + + + + + + + Käyttäjä... + + + + + + + Salasana: + + + + + + + Salasana... + + + QLineEdit::Password + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/arho_feature_template/gui/load_plan_dialog.py b/arho_feature_template/gui/load_plan_dialog.py new file mode 100644 index 0000000..250d74b --- /dev/null +++ b/arho_feature_template/gui/load_plan_dialog.py @@ -0,0 +1,178 @@ +from importlib import resources + +import psycopg2 +from qgis.PyQt import uic +from qgis.PyQt.QtCore import QRegularExpression, QSortFilterProxyModel, Qt +from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel +from qgis.PyQt.QtWidgets import QComboBox, QDialog, QLineEdit, QMessageBox, QPushButton, QTableView + +from arho_feature_template.gui.ask_credentials import DbAskCredentialsDialog +from arho_feature_template.utils.db_utils import get_db_connection_params + +ui_path = resources.files(__package__) / "load_plan_dialog.ui" + +LoadPlanDialogBase, _ = uic.loadUiType(ui_path) + + +class PlanFilterProxyModel(QSortFilterProxyModel): + def filterAcceptsRow(self, source_row, source_parent): # noqa: N802 + model = self.sourceModel() + if not model: + return False + + filter_text = self.filterRegularExpression().pattern() + if not filter_text: + return True + + for column in range(5): + index = model.index(source_row, column, source_parent) + data = model.data(index) + if data and filter_text.lower() in data.lower(): + return True + + return False + + +class LoadPlanDialog(QDialog, LoadPlanDialogBase): # type: ignore + def __init__(self, parent, connections): + super().__init__(parent) + + uic.loadUi(ui_path, self) + + self.connectionComboBox: QComboBox = self.findChild(QComboBox, "connectionComboBox") + self.planTableView: QTableView = self.findChild(QTableView, "planTableView") + self.okButton: QPushButton = self.findChild(QPushButton, "okButton") + + self.searchLineEdit: QLineEdit = self.findChild(QLineEdit, "searchLineEdit") + self.searchLineEdit.setPlaceholderText("Etsi kaavoja...") + + self.connectionComboBox.addItems(connections) + + self.connectionComboBox.currentIndexChanged.connect(self.load_plans) + self.okButton.clicked.connect(self.accept) + + self.okButton.setEnabled(False) + + self.filterProxyModel = PlanFilterProxyModel(self) + self.filterProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) + + self.planTableView.setModel(self.filterProxyModel) + self.searchLineEdit.textChanged.connect(self.filter_plans) + + self.selected_plan_id = None + + def load_plans(self): + selected_connection = self.connectionComboBox.currentText() + if not selected_connection: + self.planTableView.setModel(QStandardItemModel()) + return + + cursor = None + conn = None + + try: + conn_params = get_db_connection_params(selected_connection) + + if not conn_params.get("user") or not conn_params.get("password"): + # Trigger dialog to ask for missing credentials + dialog = DbAskCredentialsDialog(self) + dialog.rejected.connect(self.reject) + if dialog.exec() == QDialog.Accepted: + user, password = dialog.get_credentials() + conn_params["user"] = user + conn_params["password"] = password + + conn = psycopg2.connect( + host=conn_params["host"], + port=conn_params["port"], + dbname=conn_params["dbname"], + user=conn_params["user"], + password=conn_params["password"], + sslmode=conn_params["sslmode"], + ) + + cursor = conn.cursor() + + cursor.execute(""" + SELECT + p.id, + p.producers_plan_identifier, + p.name ->> 'fin' AS name_fin, + l.name ->> 'fin' AS lifecycle_status_fin, + pt.name ->> 'fin' AS plan_type_fin + FROM + hame.plan p + LEFT JOIN + codes.lifecycle_status l + ON + p.lifecycle_status_id = l.id + LEFT JOIN + codes.plan_type pt + ON + p.plan_type_id = pt.id; + """) + plans = cursor.fetchall() + + model = QStandardItemModel(len(plans), 5) + model.setHorizontalHeaderLabels( + [ + "ID", + "Tuottajan kaavatunnus", + "Nimi", + "Kaavan elinkaaren tila", + "Kaavalaji", + ] + ) + + for row_idx, plan in enumerate(plans): + model.setItem(row_idx, 0, QStandardItem(str(plan[0]))) # id + model.setItem(row_idx, 1, QStandardItem(str(plan[1]))) # producer_plan_identifier + model.setItem(row_idx, 2, QStandardItem(str(plan[2]))) # name_fin + model.setItem(row_idx, 3, QStandardItem(str(plan[3]))) # lifecycle_status_fin + model.setItem(row_idx, 4, QStandardItem(str(plan[4]))) # plan_type_fin + + self.filterProxyModel.setSourceModel(model) + + self.planTableView.setSelectionMode(QTableView.SingleSelection) + self.planTableView.setSelectionBehavior(QTableView.SelectRows) + + self.planTableView.selectionModel().selectionChanged.connect(self.on_selection_changed) + + except ValueError as ve: + QMessageBox.critical(self, "Connection Error", str(ve)) + self.planTableView.setModel(QStandardItemModel()) + + except Exception as e: # noqa: BLE001 + QMessageBox.critical(self, "Error", f"Failed to load plans: {e}") + self.planTableView.setModel(QStandardItemModel()) + + finally: + if cursor: + cursor.close() + if conn: + conn.close() + + def filter_plans(self): + search_text = self.searchLineEdit.text() + if search_text: + search_regex = QRegularExpression(search_text) + self.filterProxyModel.setFilterRegularExpression(search_regex) + else: + self.filterProxyModel.setFilterRegularExpression("") + + def on_selection_changed(self): + # Enable the OK button only if a row is selected + selection = self.planTableView.selectionModel().selectedRows() + if selection: + selected_row = selection[0].row() + self.selected_plan_id = self.planTableView.model().index(selected_row, 0).data() + self.okButton.setEnabled(True) + else: + self.selected_plan_id = None + self.okButton.setEnabled(False) + + def get_selected_connection(self): + return self.connectionComboBox.currentText() + + def get_selected_plan_id(self): + return self.selected_plan_id diff --git a/arho_feature_template/gui/load_plan_dialog.ui b/arho_feature_template/gui/load_plan_dialog.ui new file mode 100644 index 0000000..d0e1a8f --- /dev/null +++ b/arho_feature_template/gui/load_plan_dialog.ui @@ -0,0 +1,126 @@ + + + LoadPlanDialog + + + Avaa kaava + + + + 800 + 600 + + + + + + + + + Valitse tietokantayhteys: + + + + + + + + 0 + 0 + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + + + + + Etsi kaavoja: + + + + + + + Etsi kaavoja... + + + + 0 + 0 + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + + + + + + Kaavat: + + + + + + + + 1 + 1 + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + + + + + + OK + + + + 0 + 0 + + + + + + + + + diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index e60d754..295290b 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -5,14 +5,17 @@ from qgis.PyQt.QtCore import QCoreApplication, Qt, QTranslator from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction, QWidget +from qgis.PyQt.QtWidgets import QAction, QDialog, QMessageBox, QWidget from qgis.utils import iface from arho_feature_template.core.feature_template_library import FeatureTemplater, TemplateGeometryDigitizeMapTool from arho_feature_template.core.new_plan import NewPlan +from arho_feature_template.core.update_plan import LandUsePlan, update_selected_plan +from arho_feature_template.gui.load_plan_dialog import LoadPlanDialog from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name +from arho_feature_template.utils.db_utils import get_existing_database_connections from arho_feature_template.utils.misc_utils import PLUGIN_PATH if TYPE_CHECKING: @@ -134,6 +137,7 @@ def initGui(self) -> None: # noqa N802 iface.mapCanvas().mapToolSet.connect(self.templater.digitize_map_tool.deactivate) + # Add main plugin action to the toolbar self.template_dock_action = self.add_action( "", "Feature Templates", @@ -146,11 +150,11 @@ def initGui(self) -> None: # noqa N802 self.new_land_use_plan_action = self.add_action( plan_icon_path, - "Create New Plan", - self.digitize_new_plan, + "Create New Land Use Plan", + self.add_new_plan, add_to_menu=True, add_to_toolbar=True, - status_tip="Create a new plan", + status_tip="Create a new land use plan", ) self.load_land_use_plan_action = self.add_action( @@ -165,11 +169,29 @@ def on_map_tool_changed(self, new_tool: QgsMapTool, old_tool: QgsMapTool) -> Non if not isinstance(new_tool, TemplateGeometryDigitizeMapTool): self.template_dock_action.setChecked(False) - def digitize_new_plan(self): + def add_new_plan(self): self.new_plan.add_new_plan() def load_existing_land_use_plan(self) -> None: """Open existing land use plan.""" + connections = get_existing_database_connections() + + if not connections: + QMessageBox.critical(None, "Error", "No database connections found.") + return + + dialog = LoadPlanDialog(None, connections) + + if dialog.exec_() == QDialog.Accepted: + selected_plan_id = dialog.get_selected_plan_id() + + if not selected_plan_id: + QMessageBox.critical(None, "Error", "No plan was selected.") + return + + plan = LandUsePlan(selected_plan_id) + + update_selected_plan(plan) def unload(self) -> None: """Removes the plugin menu item and icon from QGIS GUI.""" diff --git a/arho_feature_template/utils/db_utils.py b/arho_feature_template/utils/db_utils.py new file mode 100644 index 0000000..e28d081 --- /dev/null +++ b/arho_feature_template/utils/db_utils.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import logging + +from qgis.core import QgsApplication, QgsAuthMethodConfig +from qgis.PyQt.QtCore import QSettings +from qgis.PyQt.QtWidgets import QDialog, QMessageBox + +from arho_feature_template.core.exceptions import AuthConfigException +from arho_feature_template.gui.ask_credentials import DbAskCredentialsDialog +from arho_feature_template.qgis_plugin_tools.tools.custom_logging import bar_msg +from arho_feature_template.qgis_plugin_tools.tools.settings import parse_value + +LOGGER = logging.getLogger("LandUsePlugin") + +PG_CONNECTIONS = "PostgreSQL/connections" +QGS_SETTINGS_PSYCOPG2_PARAM_MAP = { + "database": "dbname", + "host": "host", + "password": "password", + "port": "port", + "username": "user", + "sslmode": "sslmode", +} +QGS_SETTINGS_SSL_MODE_TO_POSTGRES = { + "SslDisable": "disable", + "SslAllow": "allow", + "SslPrefer": "prefer", + "SslRequire": "require", + "SslVerifyCa": "verify-ca", + "SslVerifyFull": "verify-full", +} + + +def get_existing_database_connections() -> set: + """ + Retrieve the list of existing database connections from QGIS settings. + + :return: A set of available PostgreSQL connection names. + """ + s = QSettings() + s.beginGroup(PG_CONNECTIONS) + keys = s.allKeys() + s.endGroup() + + connections = {key.split("/")[0] for key in keys if "/" in key} + LOGGER.debug(f"Available database connections: {connections}") # noqa: G004 + + return connections + + +def get_db_connection_params(con_name) -> dict: + s = QSettings() + s.beginGroup(f"{PG_CONNECTIONS}/{con_name}") + + auth_cfg_id = parse_value(s.value("authcfg")) + username_saved = parse_value(s.value("saveUsername")) + pwd_saved = parse_value(s.value("savePassword")) + # sslmode = parse_value(s.value("sslmode")) + + params = {} + + for qgs_key, psyc_key in QGS_SETTINGS_PSYCOPG2_PARAM_MAP.items(): + if psyc_key != "sslmode": + params[psyc_key] = parse_value(s.value(qgs_key)) + else: + params[psyc_key] = QGS_SETTINGS_SSL_MODE_TO_POSTGRES[parse_value(s.value(qgs_key))] + + s.endGroup() + # username or password might have to be asked separately + if not username_saved: + params["user"] = None + + if not pwd_saved: + params["password"] = None + + if auth_cfg_id is not None and auth_cfg_id != "": + # Auth config is being used to store the username and password + auth_config = QgsAuthMethodConfig() + # noinspection PyArgumentList + QgsApplication.authManager().loadAuthenticationConfig(auth_cfg_id, auth_config, True) + + if auth_config.isValid(): + params["user"] = auth_config.configMap().get("username") + params["password"] = auth_config.configMap().get("password") + else: + msg = "Auth config error occurred while fetching database connection parameters" + raise AuthConfigException( + msg, + bar_msg=bar_msg(f"Check auth config with id: {auth_cfg_id}"), + ) + + return params + + +def check_credentials(conn_params: dict) -> None: + """ + Checks whether the username and password are present in the connection parameters. + If not, prompt the user to enter the credentials via a dialog. + + :param conn_params: Connection parameters (dictionary). + """ + if not conn_params["user"] or not conn_params["password"]: + LOGGER.info("No username and/or password found. Asking user for credentials.") + + # Show dialog to ask for user credentials + ask_credentials_dlg = DbAskCredentialsDialog() + result = ask_credentials_dlg.exec_() + + if result == QDialog.Accepted: + conn_params["user"] = ask_credentials_dlg.userLineEdit.text() + conn_params["password"] = ask_credentials_dlg.pwdLineEdit.text() + else: + ask_credentials_dlg.close() + QMessageBox.warning(None, "Authentication Required", "Cannot connect without username or password.") + return None From 2bd9504f29c09d1b862e62506dc4263646bcfbf2 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Mon, 28 Oct 2024 21:48:14 +0200 Subject: [PATCH 2/2] Change to use ProviderRegistry --- arho_feature_template/core/exceptions.py | 7 +- arho_feature_template/gui/ask_credentials.py | 36 ----- arho_feature_template/gui/ask_credentials.ui | 66 --------- arho_feature_template/gui/load_plan_dialog.py | 139 +++++++----------- arho_feature_template/gui/load_plan_dialog.ui | 87 ++++++----- arho_feature_template/plugin.py | 5 +- arho_feature_template/utils/db_utils.py | 110 ++------------ 7 files changed, 122 insertions(+), 328 deletions(-) delete mode 100644 arho_feature_template/gui/ask_credentials.py delete mode 100644 arho_feature_template/gui/ask_credentials.ui diff --git a/arho_feature_template/core/exceptions.py b/arho_feature_template/core/exceptions.py index 7b77bfb..50b61b5 100644 --- a/arho_feature_template/core/exceptions.py +++ b/arho_feature_template/core/exceptions.py @@ -1,5 +1,2 @@ -from arho_feature_template.qgis_plugin_tools.tools.exceptions import QgsPluginException - - -class AuthConfigException(QgsPluginException): - pass +class UnexpectedNoneError(Exception): + """Internal QGIS errors that should not be happened""" diff --git a/arho_feature_template/gui/ask_credentials.py b/arho_feature_template/gui/ask_credentials.py deleted file mode 100644 index 5233949..0000000 --- a/arho_feature_template/gui/ask_credentials.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -from importlib import resources - -from qgis.PyQt import uic -from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QLineEdit - -# Load the .ui file path using importlib resources -ui_path = resources.files(__package__) / "ask_credentials.ui" - -# Use uic.loadUiType to load the UI definition from the .ui file -DbAskCredentialsDialogBase, _ = uic.loadUiType(ui_path) - - -class DbAskCredentialsDialog(QDialog, DbAskCredentialsDialogBase): # type: ignore - def __init__(self, parent: QDialog = None): - super().__init__(parent) - - # Set up the UI from the loaded .ui file - self.setupUi(self) - - # The UI elements defined in the .ui file - self.userLineEdit: QLineEdit = self.findChild(QLineEdit, "userLineEdit") - self.pwdLineEdit: QLineEdit = self.findChild(QLineEdit, "pwdLineEdit") - self.buttonBox: QDialogButtonBox = self.findChild(QDialogButtonBox, "buttonBox") - - # Connect the OK and Cancel buttons to their respective functions - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - def get_credentials(self) -> tuple[str, str]: - """ - Returns the entered username and password. - :return: Tuple (username, password) - """ - return self.userLineEdit.text(), self.pwdLineEdit.text() diff --git a/arho_feature_template/gui/ask_credentials.ui b/arho_feature_template/gui/ask_credentials.ui deleted file mode 100644 index cf42490..0000000 --- a/arho_feature_template/gui/ask_credentials.ui +++ /dev/null @@ -1,66 +0,0 @@ - - - DbAskCredentialsDialogBase - - - - 0 - 0 - 400 - 200 - - - - Käyttäjän autentikaatio - - - - - - - - Käyttäjä: - - - - - - - Käyttäjä... - - - - - - - Salasana: - - - - - - - Salasana... - - - QLineEdit::Password - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/arho_feature_template/gui/load_plan_dialog.py b/arho_feature_template/gui/load_plan_dialog.py index 250d74b..8cbf984 100644 --- a/arho_feature_template/gui/load_plan_dialog.py +++ b/arho_feature_template/gui/load_plan_dialog.py @@ -1,13 +1,12 @@ from importlib import resources -import psycopg2 +from qgis.core import QgsProviderRegistry from qgis.PyQt import uic from qgis.PyQt.QtCore import QRegularExpression, QSortFilterProxyModel, Qt from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel -from qgis.PyQt.QtWidgets import QComboBox, QDialog, QLineEdit, QMessageBox, QPushButton, QTableView +from qgis.PyQt.QtWidgets import QComboBox, QDialog, QDialogButtonBox, QLineEdit, QMessageBox, QPushButton, QTableView -from arho_feature_template.gui.ask_credentials import DbAskCredentialsDialog -from arho_feature_template.utils.db_utils import get_db_connection_params +from arho_feature_template.core.exceptions import UnexpectedNoneError ui_path = resources.files(__package__) / "load_plan_dialog.ui" @@ -34,66 +33,67 @@ def filterAcceptsRow(self, source_row, source_parent): # noqa: N802 class LoadPlanDialog(QDialog, LoadPlanDialogBase): # type: ignore + connectionComboBox: QComboBox # noqa: N815 + push_button_load: QPushButton + planTableView: QTableView # noqa: N815 + searchLineEdit: QLineEdit # noqa: N815 + buttonBox: QDialogButtonBox # noqa: N815 + def __init__(self, parent, connections): super().__init__(parent) + self.setupUi(self) - uic.loadUi(ui_path, self) + self._selected_plan_id = None - self.connectionComboBox: QComboBox = self.findChild(QComboBox, "connectionComboBox") - self.planTableView: QTableView = self.findChild(QTableView, "planTableView") - self.okButton: QPushButton = self.findChild(QPushButton, "okButton") + self.buttonBox.rejected.connect(self.reject) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) - self.searchLineEdit: QLineEdit = self.findChild(QLineEdit, "searchLineEdit") - self.searchLineEdit.setPlaceholderText("Etsi kaavoja...") + self.push_button_load.clicked.connect(self.load_plans) + self.searchLineEdit.textChanged.connect(self.filter_plans) self.connectionComboBox.addItems(connections) - self.connectionComboBox.currentIndexChanged.connect(self.load_plans) - self.okButton.clicked.connect(self.accept) - - self.okButton.setEnabled(False) - - self.filterProxyModel = PlanFilterProxyModel(self) + self.planTableView.setSelectionMode(QTableView.SingleSelection) + self.planTableView.setSelectionBehavior(QTableView.SelectRows) + self.planTableView.selectionModel().selectionChanged.connect(self.on_selection_changed) + + self.model = QStandardItemModel() + self.model.setColumnCount(5) + self.model.setHorizontalHeaderLabels( + [ + "ID", + "Tuottajan kaavatunnus", + "Nimi", + "Kaavan elinkaaren tila", + "Kaavalaji", + ] + ) + + self.filterProxyModel = PlanFilterProxyModel() + self.filterProxyModel.setSourceModel(self.model) self.filterProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.planTableView.setModel(self.filterProxyModel) - self.searchLineEdit.textChanged.connect(self.filter_plans) - - self.selected_plan_id = None def load_plans(self): + self.model.removeRows(0, self.model.rowCount()) + selected_connection = self.connectionComboBox.currentText() if not selected_connection: self.planTableView.setModel(QStandardItemModel()) return - cursor = None - conn = None + provider_registry = QgsProviderRegistry.instance() + if provider_registry is None: + raise UnexpectedNoneError + postgres_provider_metadata = provider_registry.providerMetadata("postgres") + if postgres_provider_metadata is None: + raise UnexpectedNoneError try: - conn_params = get_db_connection_params(selected_connection) - - if not conn_params.get("user") or not conn_params.get("password"): - # Trigger dialog to ask for missing credentials - dialog = DbAskCredentialsDialog(self) - dialog.rejected.connect(self.reject) - if dialog.exec() == QDialog.Accepted: - user, password = dialog.get_credentials() - conn_params["user"] = user - conn_params["password"] = password - - conn = psycopg2.connect( - host=conn_params["host"], - port=conn_params["port"], - dbname=conn_params["dbname"], - user=conn_params["user"], - password=conn_params["password"], - sslmode=conn_params["sslmode"], - ) - - cursor = conn.cursor() - - cursor.execute(""" + connection = postgres_provider_metadata.createConnection(selected_connection) + plans = connection.executeSql(""" SELECT p.id, p.producers_plan_identifier, @@ -111,46 +111,12 @@ def load_plans(self): ON p.plan_type_id = pt.id; """) - plans = cursor.fetchall() - - model = QStandardItemModel(len(plans), 5) - model.setHorizontalHeaderLabels( - [ - "ID", - "Tuottajan kaavatunnus", - "Nimi", - "Kaavan elinkaaren tila", - "Kaavalaji", - ] - ) - - for row_idx, plan in enumerate(plans): - model.setItem(row_idx, 0, QStandardItem(str(plan[0]))) # id - model.setItem(row_idx, 1, QStandardItem(str(plan[1]))) # producer_plan_identifier - model.setItem(row_idx, 2, QStandardItem(str(plan[2]))) # name_fin - model.setItem(row_idx, 3, QStandardItem(str(plan[3]))) # lifecycle_status_fin - model.setItem(row_idx, 4, QStandardItem(str(plan[4]))) # plan_type_fin - - self.filterProxyModel.setSourceModel(model) - - self.planTableView.setSelectionMode(QTableView.SingleSelection) - self.planTableView.setSelectionBehavior(QTableView.SelectRows) - - self.planTableView.selectionModel().selectionChanged.connect(self.on_selection_changed) - - except ValueError as ve: - QMessageBox.critical(self, "Connection Error", str(ve)) - self.planTableView.setModel(QStandardItemModel()) + for plan in plans: + self.model.appendRow([QStandardItem(column) for column in plan]) except Exception as e: # noqa: BLE001 QMessageBox.critical(self, "Error", f"Failed to load plans: {e}") - self.planTableView.setModel(QStandardItemModel()) - - finally: - if cursor: - cursor.close() - if conn: - conn.close() + self.model.removeRows(0, self.model.rowCount()) def filter_plans(self): search_text = self.searchLineEdit.text() @@ -163,16 +129,17 @@ def filter_plans(self): def on_selection_changed(self): # Enable the OK button only if a row is selected selection = self.planTableView.selectionModel().selectedRows() + ok_button = self.buttonBox.button(QDialogButtonBox.Ok) if selection: selected_row = selection[0].row() - self.selected_plan_id = self.planTableView.model().index(selected_row, 0).data() - self.okButton.setEnabled(True) + self._selected_plan_id = self.planTableView.model().index(selected_row, 0).data() + ok_button.setEnabled(True) else: - self.selected_plan_id = None - self.okButton.setEnabled(False) + self._selected_plan_id = None + ok_button.setEnabled(False) def get_selected_connection(self): return self.connectionComboBox.currentText() def get_selected_plan_id(self): - return self.selected_plan_id + return self._selected_plan_id diff --git a/arho_feature_template/gui/load_plan_dialog.ui b/arho_feature_template/gui/load_plan_dialog.ui index d0e1a8f..0a0c82d 100644 --- a/arho_feature_template/gui/load_plan_dialog.ui +++ b/arho_feature_template/gui/load_plan_dialog.ui @@ -2,8 +2,13 @@ LoadPlanDialog - - Avaa kaava + + + 0 + 0 + 800 + 600 + @@ -11,9 +16,10 @@ 600 + + Avaa kaava + - - @@ -22,16 +28,26 @@ - - - - 0 - 0 - - - + + + + + + 0 + 0 + + + + + + + + Lataa kaavat + + + + - @@ -40,9 +56,14 @@ QSizePolicy::Expanding + + + 0 + 0 + + - @@ -52,18 +73,17 @@ - - Etsi kaavoja... - 0 0 + + Etsi kaavoja... + - @@ -72,10 +92,14 @@ QSizePolicy::Expanding + + + 0 + 0 + + - - @@ -91,9 +115,8 @@ 1 - + - @@ -102,20 +125,18 @@ QSizePolicy::Expanding + + + 0 + 0 + + - - - - - OK - - - - 0 - 0 - + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index 295290b..fc34cc2 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -15,7 +15,7 @@ from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name -from arho_feature_template.utils.db_utils import get_existing_database_connections +from arho_feature_template.utils.db_utils import get_existing_database_connection_names from arho_feature_template.utils.misc_utils import PLUGIN_PATH if TYPE_CHECKING: @@ -174,7 +174,8 @@ def add_new_plan(self): def load_existing_land_use_plan(self) -> None: """Open existing land use plan.""" - connections = get_existing_database_connections() + + connections = get_existing_database_connection_names() if not connections: QMessageBox.critical(None, "Error", "No database connections found.") diff --git a/arho_feature_template/utils/db_utils.py b/arho_feature_template/utils/db_utils.py index e28d081..b8f67a0 100644 --- a/arho_feature_template/utils/db_utils.py +++ b/arho_feature_template/utils/db_utils.py @@ -2,115 +2,25 @@ import logging -from qgis.core import QgsApplication, QgsAuthMethodConfig -from qgis.PyQt.QtCore import QSettings -from qgis.PyQt.QtWidgets import QDialog, QMessageBox +from qgis.core import QgsProviderRegistry -from arho_feature_template.core.exceptions import AuthConfigException -from arho_feature_template.gui.ask_credentials import DbAskCredentialsDialog -from arho_feature_template.qgis_plugin_tools.tools.custom_logging import bar_msg -from arho_feature_template.qgis_plugin_tools.tools.settings import parse_value +from arho_feature_template.core.exceptions import UnexpectedNoneError LOGGER = logging.getLogger("LandUsePlugin") -PG_CONNECTIONS = "PostgreSQL/connections" -QGS_SETTINGS_PSYCOPG2_PARAM_MAP = { - "database": "dbname", - "host": "host", - "password": "password", - "port": "port", - "username": "user", - "sslmode": "sslmode", -} -QGS_SETTINGS_SSL_MODE_TO_POSTGRES = { - "SslDisable": "disable", - "SslAllow": "allow", - "SslPrefer": "prefer", - "SslRequire": "require", - "SslVerifyCa": "verify-ca", - "SslVerifyFull": "verify-full", -} - -def get_existing_database_connections() -> set: +def get_existing_database_connection_names() -> list[str]: """ Retrieve the list of existing database connections from QGIS settings. :return: A set of available PostgreSQL connection names. """ - s = QSettings() - s.beginGroup(PG_CONNECTIONS) - keys = s.allKeys() - s.endGroup() - - connections = {key.split("/")[0] for key in keys if "/" in key} - LOGGER.debug(f"Available database connections: {connections}") # noqa: G004 - - return connections - - -def get_db_connection_params(con_name) -> dict: - s = QSettings() - s.beginGroup(f"{PG_CONNECTIONS}/{con_name}") - - auth_cfg_id = parse_value(s.value("authcfg")) - username_saved = parse_value(s.value("saveUsername")) - pwd_saved = parse_value(s.value("savePassword")) - # sslmode = parse_value(s.value("sslmode")) - - params = {} - - for qgs_key, psyc_key in QGS_SETTINGS_PSYCOPG2_PARAM_MAP.items(): - if psyc_key != "sslmode": - params[psyc_key] = parse_value(s.value(qgs_key)) - else: - params[psyc_key] = QGS_SETTINGS_SSL_MODE_TO_POSTGRES[parse_value(s.value(qgs_key))] - - s.endGroup() - # username or password might have to be asked separately - if not username_saved: - params["user"] = None - - if not pwd_saved: - params["password"] = None - - if auth_cfg_id is not None and auth_cfg_id != "": - # Auth config is being used to store the username and password - auth_config = QgsAuthMethodConfig() - # noinspection PyArgumentList - QgsApplication.authManager().loadAuthenticationConfig(auth_cfg_id, auth_config, True) - - if auth_config.isValid(): - params["user"] = auth_config.configMap().get("username") - params["password"] = auth_config.configMap().get("password") - else: - msg = "Auth config error occurred while fetching database connection parameters" - raise AuthConfigException( - msg, - bar_msg=bar_msg(f"Check auth config with id: {auth_cfg_id}"), - ) - - return params - - -def check_credentials(conn_params: dict) -> None: - """ - Checks whether the username and password are present in the connection parameters. - If not, prompt the user to enter the credentials via a dialog. - - :param conn_params: Connection parameters (dictionary). - """ - if not conn_params["user"] or not conn_params["password"]: - LOGGER.info("No username and/or password found. Asking user for credentials.") - # Show dialog to ask for user credentials - ask_credentials_dlg = DbAskCredentialsDialog() - result = ask_credentials_dlg.exec_() + provider_registry = QgsProviderRegistry.instance() + if provider_registry is None: + raise UnexpectedNoneError + postgres_provider_metadata = provider_registry.providerMetadata("postgres") + if postgres_provider_metadata is None: + raise UnexpectedNoneError - if result == QDialog.Accepted: - conn_params["user"] = ask_credentials_dlg.userLineEdit.text() - conn_params["password"] = ask_credentials_dlg.pwdLineEdit.text() - else: - ask_credentials_dlg.close() - QMessageBox.warning(None, "Authentication Required", "Cannot connect without username or password.") - return None + return list(postgres_provider_metadata.dbConnections(False))