diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e428cd4..da82109 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,4 +64,3 @@ repos: ci: autofix_prs: true autoupdate_schedule: quarterly - diff --git a/pg_service_parser/config.py b/pg_service_parser/config.py index 65003ad..d294428 100644 --- a/pg_service_parser/config.py +++ b/pg_service_parser/config.py @@ -1,4 +1,3 @@ import os.path - DEFAULT_PG_SERVICE_PATH = os.path.expanduser("~/.pg_service.conf") diff --git a/pg_service_parser/core/__init__.py b/pg_service_parser/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pg_service_parser/core/item_models.py b/pg_service_parser/core/item_models.py new file mode 100644 index 0000000..1419205 --- /dev/null +++ b/pg_service_parser/core/item_models.py @@ -0,0 +1,72 @@ +from qgis.PyQt.QtCore import QAbstractTableModel, Qt +from qgis.PyQt.QtGui import QFont + + +class ServiceConfigModel(QAbstractTableModel): + KEY_COL = 0 + VALUE_COL = 1 + + def __init__(self, service_name, service_config): + super().__init__() + self.__service_name = service_name + self.__model_data = service_config + self.__dirty = False + + def rowCount(self, parent): + return len(self.__model_data) + + def columnCount(self, parent): + return 2 + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + key = list(self.__model_data.keys())[index.row()] + if role == Qt.DisplayRole: + if index.column() == self.KEY_COL: + return key + elif index.column() == self.VALUE_COL: + return self.__model_data[key] + elif role == Qt.EditRole and index.column() == self.VALUE_COL: + return self.__model_data[key] + elif role == Qt.FontRole and index.column() == self.KEY_COL: + font = QFont() + font.setBold(True) + return font + + return None + + def setData(self, index, value, role=Qt.EditRole) -> bool: + if not index.isValid(): + return False + + key = list(self.__model_data.keys())[index.row()] + if value != self.__model_data[key]: + self.__model_data[key] = value + self.__dirty = True + return True + + return False + + def flags(self, idx): + if not idx.isValid(): + return ~Qt.ItemIsSelectable & ~Qt.ItemIsEnabled + + _flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled + if idx.column() == self.KEY_COL: + return _flags + elif idx.column() == self.VALUE_COL: + return _flags | Qt.ItemIsEditable + + def is_dirty(self): + return self.__dirty + + def service_config(self): + return self.__model_data.copy() + + def service_name(self): + return self.__service_name + + def set_not_dirty(self): + self.__dirty = False diff --git a/pg_service_parser/pg_service_parser_wrapper.py b/pg_service_parser/core/pg_service_parser_wrapper.py similarity index 76% rename from pg_service_parser/pg_service_parser_wrapper.py rename to pg_service_parser/core/pg_service_parser_wrapper.py index 5b12bc7..66e3a74 100644 --- a/pg_service_parser/pg_service_parser_wrapper.py +++ b/pg_service_parser/core/pg_service_parser_wrapper.py @@ -1,14 +1,14 @@ import os.path +from typing import List, Optional import pgserviceparser -from typing import (List, - Optional) def conf_path() -> str: path = pgserviceparser.conf_path() return path if os.path.exists(path) else None + def service_names(conf_file_path: Optional[str] = None) -> List[str]: return pgserviceparser.service_names(conf_file_path) @@ -18,9 +18,9 @@ def service_config(service_name: str, conf_file_path: Optional[str] = None) -> d def write_service_settings( - service_name: str, - settings: dict, - conf_file_path: Optional[str] = None, + service_name: str, + settings: dict, + conf_file_path: Optional[str] = None, ) -> bool: """Returns true if it could write the settings to the file. @@ -39,13 +39,15 @@ def write_service_settings( return False -def create_service(service_name: str, settings: dict, conf_file_path: Optional[str] = None) -> bool: +def create_service( + service_name: str, settings: dict, conf_file_path: Optional[str] = None +) -> bool: config = pgserviceparser.full_config(conf_file_path) if service_name in config: return False config.add_section(service_name) - with open(conf_file_path or pgserviceparser.conf_path(), 'w') as f: + with open(conf_file_path or pgserviceparser.conf_path(), "w") as f: config.write(f) if service_name in config: @@ -54,7 +56,9 @@ def create_service(service_name: str, settings: dict, conf_file_path: Optional[s return False -def copy_service_settings(source_service_name: str, target_service_name: str, conf_file_path: Optional[str] = None) -> bool: +def copy_service_settings( + source_service_name: str, target_service_name: str, conf_file_path: Optional[str] = None +) -> bool: settings = pgserviceparser.service_config(source_service_name, conf_file_path) config = pgserviceparser.full_config(conf_file_path) @@ -67,12 +71,17 @@ def copy_service_settings(source_service_name: str, target_service_name: str, co return res -if __name__ == '__main__': +if __name__ == "__main__": assert service_names() == [] # Add new service - _settings = {'host': 'localhost', 'port': '5432', 'user': 'postgres', 'password': 'secret', - 'dbname': 'qgis_test_db'} + _settings = { + "host": "localhost", + "port": "5432", + "user": "postgres", + "password": "secret", + "dbname": "qgis_test_db", + } assert create_service("qgis-test", _settings) assert service_names() == ["qgis-test"] @@ -82,8 +91,13 @@ def copy_service_settings(source_service_name: str, target_service_name: str, co assert service_config("qgis-demo") == _settings # Add new service - _settings = {'host': 'localhost', 'port': '5433', 'user': 'admin', 'password': 'secret', - 'dbname': 'qgis_test_db2'} + _settings = { + "host": "localhost", + "port": "5433", + "user": "admin", + "password": "secret", + "dbname": "qgis_test_db2", + } assert create_service("qgis-new-test", _settings) assert service_names() == ["qgis-test", "qgis-demo", "qgis-new-test"] assert service_config("qgis-new-test") == _settings diff --git a/pg_service_parser/gui/dlg_pg_service.py b/pg_service_parser/gui/dlg_pg_service.py index 7395522..484924c 100644 --- a/pg_service_parser/gui/dlg_pg_service.py +++ b/pg_service_parser/gui/dlg_pg_service.py @@ -1,15 +1,18 @@ -from qgis.PyQt.QtCore import (Qt, - pyqtSlot) -from qgis.PyQt.QtWidgets import (QDialog, - QSizePolicy) from qgis.gui import QgsMessageBar - -from pg_service_parser.pg_service_parser_wrapper import (conf_path, - copy_service_settings, - service_names) +from qgis.PyQt.QtCore import Qt, pyqtSlot +from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QSizePolicy + +from pg_service_parser.core.item_models import ServiceConfigModel +from pg_service_parser.core.pg_service_parser_wrapper import ( + conf_path, + copy_service_settings, + service_config, + service_names, + write_service_settings, +) from pg_service_parser.utils import get_ui_class -DIALOG_UI = get_ui_class('pg_service_dialog.ui') +DIALOG_UI = get_ui_class("pg_service_dialog.ui") COPY_TAB_INDEX = 0 EDIT_TAB_INDEX = 1 @@ -23,18 +26,24 @@ def __init__(self, parent): conf_file_path = conf_path() if not conf_file_path: self.lblConfFile.setText("Config file not found!") - self.lblConfFile.setToolTip("Set your PGSERVICEFILE environment variable and reopen the dialog.") + self.lblConfFile.setToolTip( + "Set your PGSERVICEFILE environment variable and reopen the dialog." + ) self.txtConfFile.setVisible(False) self.tabWidget.setEnabled(False) return + self.__edit_model = None + self.txtConfFile.setText(conf_file_path) self.lblWarning.setVisible(False) - self.tabWidget.setTabEnabled(EDIT_TAB_INDEX, False) # Not yet implemented self.radOverwrite.toggled.connect(self.__update_target_controls) self.btnCopyService.clicked.connect(self.__copy_service) self.cboSourceService.currentIndexChanged.connect(self.__source_service_changed) + self.tabWidget.currentChanged.connect(self.__current_tab_changed) + self.cboEditService.currentIndexChanged.connect(self.__edit_service_changed) + self.btnUpdateService.clicked.connect(self.__update_service_clicked) self.__initialize_copy_services() self.__update_target_controls(True) @@ -72,6 +81,16 @@ def __initialize_copy_services(self): self.cboSourceService.addItems(service_names()) self.cboSourceService.setCurrentText(current_text) + def __initialize_edit_services(self): + self.__edit_model = None + current_text = self.cboEditService.currentText() # Remember latest currentText + self.cboEditService.blockSignals(True) # Avoid triggering custom slot while clearing + self.cboEditService.clear() + self.cboEditService.blockSignals(False) + self.cboEditService.addItems(service_names()) + self.cboEditService.setCurrentText(current_text) + + @pyqtSlot() def __copy_service(self): # Validations if self.radCreate.isChecked(): @@ -79,14 +98,20 @@ def __copy_service(self): self.bar.pushInfo("PG service", "Enter a service name and try again.") return elif self.txtNewService.text().strip() in service_names(): - self.bar.pushWarning("PG service", "Service name already exists! Change it and try again.") + self.bar.pushWarning( + "PG service", "Service name already exists! Change it and try again." + ) return elif self.radOverwrite.isChecked(): if not self.cboTargetService.currentText(): self.bar.pushInfo("PG service", "Select a valid target service and try again.") return - target_service = self.cboTargetService.currentText() if self.radOverwrite.isChecked() else self.txtNewService.text().strip() + target_service = ( + self.cboTargetService.currentText() + if self.radOverwrite.isChecked() + else self.txtNewService.text().strip() + ) if copy_service_settings(self.cboSourceService.currentText(), target_service): self.bar.pushSuccess("PG service", f"PG service copied to '{target_service}'!") @@ -94,3 +119,49 @@ def __copy_service(self): self.__initialize_copy_services() # Reflect the newly added service else: self.bar.pushWarning("PG service", "There was a problem copying the service!") + + @pyqtSlot(int) + def __current_tab_changed(self, index): + if index == COPY_TAB_INDEX: + # self.__initialize_copy_services() + pass # For now, services to be copied won't be altered in other tabs + elif index == EDIT_TAB_INDEX: + self.__initialize_edit_services() + + @pyqtSlot(int) + def __edit_service_changed(self, index): + target_service = self.cboEditService.currentText() + if self.__edit_model and self.__edit_model.is_dirty(): + if ( + not QMessageBox.question( + self, + "Pending edits", + "There are pending edits for service '{}'. Are you sure you want to discard them?".format( + self.__edit_model.service_name() + ), + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + == QMessageBox.Yes + ): + + self.cboEditService.blockSignals(True) + self.cboEditService.setCurrentText(self.__edit_model.service_name()) + self.cboEditService.blockSignals(False) + return + + self.__edit_model = ServiceConfigModel(target_service, service_config(target_service)) + self.tblServiceConfig.setModel(self.__edit_model) + + @pyqtSlot() + def __update_service_clicked(self): + if self.__edit_model and self.__edit_model.is_dirty(): + target_service = self.cboEditService.currentText() + res = write_service_settings(target_service, self.__edit_model.service_config()) + if res: + self.bar.pushSuccess("PG service", f"PG service '{target_service}' updated!") + self.__edit_model.set_not_dirty() + else: + self.bar.pushWarning("PG service", "There was a problem updating the service!") + else: + self.bar.pushInfo("PG service", "Edit the service configuration and try again.") diff --git a/pg_service_parser/imgs/warning.svg b/pg_service_parser/imgs/warning.svg index c6b8c23..539b63e 100644 --- a/pg_service_parser/imgs/warning.svg +++ b/pg_service_parser/imgs/warning.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/pg_service_parser/metadata.txt b/pg_service_parser/metadata.txt index 366d50f..3e6e196 100644 --- a/pg_service_parser/metadata.txt +++ b/pg_service_parser/metadata.txt @@ -14,4 +14,4 @@ tracker=https://github.com/opengisch/qgis-pg-service-parser-plugin/issues repository=https://github.com/opengisch/qgis-pg-service-parser-plugin icon=images/icon.png experimental=False -deprecated=False \ No newline at end of file +deprecated=False diff --git a/pg_service_parser/pg_service_parser_plugin.py b/pg_service_parser/pg_service_parser_plugin.py index df0d78f..126586f 100644 --- a/pg_service_parser/pg_service_parser_plugin.py +++ b/pg_service_parser/pg_service_parser_plugin.py @@ -3,13 +3,13 @@ from pg_service_parser.gui.dlg_pg_service import PgServiceDialog -class PgServiceParserPlugin(): +class PgServiceParserPlugin: def __init__(self, iface): self.iface = iface self.action = None def initGui(self): - self.action = QAction('Go!', self.iface.mainWindow()) + self.action = QAction("Go!", self.iface.mainWindow()) self.action.triggered.connect(self.run) self.iface.addToolBarIcon(self.action) diff --git a/pg_service_parser/ui/pg_service_dialog.ui b/pg_service_parser/ui/pg_service_dialog.ui index 83f617a..a9901c7 100644 --- a/pg_service_parser/ui/pg_service_dialog.ui +++ b/pg_service_parser/ui/pg_service_dialog.ui @@ -205,13 +205,31 @@ - + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + true - - 0 + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + Qt::DotLine + + + false + + + true + + + false + @@ -230,7 +248,7 @@ - + Update service diff --git a/pg_service_parser/utils.py b/pg_service_parser/utils.py index 02a9388..9c7f47f 100644 --- a/pg_service_parser/utils.py +++ b/pg_service_parser/utils.py @@ -1,6 +1,6 @@ import os -from qgis.PyQt.uic import (loadUiType, - loadUi) + +from qgis.PyQt.uic import loadUiType from pg_service_parser.config import DEFAULT_PG_SERVICE_PATH @@ -16,14 +16,8 @@ def get_ui_class(ui_file): def get_ui_file_path(ui_file) -> str: - os.path.sep.join(ui_file.split('/')) - ui_file_path = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - 'ui', - ui_file - ) - ) + os.path.sep.join(ui_file.split("/")) + ui_file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "ui", ui_file)) return ui_file_path