diff --git a/pg_service_parser/conf/service_settings.py b/pg_service_parser/conf/service_settings.py new file mode 100644 index 0000000..8511c56 --- /dev/null +++ b/pg_service_parser/conf/service_settings.py @@ -0,0 +1,17 @@ +# Settings available for manual addition +# See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS +SERVICE_SETTINGS = { + "host": "localhost", + "port": "5432", + "dbname": "test", + "user": "", + "password": "", + "passfile": "", +} + +# Settings to initialize new files +SETTINGS_TEMPLATE = { + "host": "localhost", + "port": "5432", + "dbname": "test", +} diff --git a/pg_service_parser/core/item_models.py b/pg_service_parser/core/item_models.py index 4c72be8..cc05d0a 100644 --- a/pg_service_parser/core/item_models.py +++ b/pg_service_parser/core/item_models.py @@ -1,4 +1,4 @@ -from qgis.PyQt.QtCore import QAbstractTableModel, Qt, pyqtSignal +from qgis.PyQt.QtCore import QAbstractTableModel, QModelIndex, Qt, pyqtSignal from qgis.PyQt.QtGui import QColorConstants, QFont @@ -8,19 +8,43 @@ class ServiceConfigModel(QAbstractTableModel): is_dirty_changed = pyqtSignal(bool) # Whether the model gets dirty or not - def __init__(self, service_name, service_config): + def __init__(self, service_name: str, service_config: dict): super().__init__() self.__service_name = service_name self.__model_data = service_config self.__original_data = service_config.copy() self.__dirty = False - def rowCount(self, parent): + def rowCount(self, parent=QModelIndex()): return len(self.__model_data) - def columnCount(self, parent): + def columnCount(self, parent=QModelIndex()): return 2 + def index_to_setting_key(self, index): + return list(self.__model_data.keys())[index.row()] + + def add_settings(self, settings: dict): + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount() + len(settings) - 1) + self.__model_data.update(settings) + self.__set_dirty_status(True) + self.endInsertRows() + + if self.__model_data == self.__original_data: + self.__set_dirty_status(False) + + def remove_setting(self, index: QModelIndex): + if not index.isValid(): + return + + self.beginRemoveRows(QModelIndex(), index.row(), index.row()) + del self.__model_data[list(self.__model_data.keys())[index.row()]] + self.__set_dirty_status(True) + self.endRemoveRows() + + if self.__model_data == self.__original_data: + self.__set_dirty_status(False) + def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None @@ -38,15 +62,18 @@ def data(self, index, role=Qt.DisplayRole): font = QFont() font.setBold(True) return font - elif ( - index.column() == self.VALUE_COL - and self.__model_data[key] != self.__original_data[key] + elif index.column() == self.VALUE_COL and ( + key not in self.__original_data + or self.__model_data[key] != self.__original_data[key] ): font = QFont() font.setItalic(True) return font elif role == Qt.ForegroundRole and index.column() == self.VALUE_COL: - if self.__model_data[key] != self.__original_data[key]: + if ( + key not in self.__original_data + or self.__model_data[key] != self.__original_data[key] + ): return QColorConstants.DarkGreen return None @@ -59,13 +86,11 @@ def setData(self, index, value, role=Qt.EditRole) -> bool: if value != self.__model_data[key]: self.__model_data[key] = value - if value != self.__original_data[key]: - self.__dirty = True - self.is_dirty_changed.emit(True) + if key not in self.__original_data or value != self.__original_data[key]: + self.__set_dirty_status(True) else: if self.__model_data == self.__original_data: - self.__dirty = False - self.is_dirty_changed.emit(False) + self.__set_dirty_status(False) return True @@ -84,6 +109,13 @@ def flags(self, idx): def is_dirty(self): return self.__dirty + def __set_dirty_status(self, status: bool): + self.__dirty = status + self.is_dirty_changed.emit(status) + + def current_setting_keys(self) -> list[str]: + return list(self.__model_data.keys()) + def service_config(self): return self.__model_data.copy() @@ -93,5 +125,12 @@ def service_name(self): def set_not_dirty(self): # Data saved in the provider self.__original_data = self.__model_data.copy() - self.__dirty = False - self.is_dirty_changed.emit(False) + self.__set_dirty_status(False) + + def invalid_settings(self): + """ + Validation for service entries. + + :return: List of invalid settings. + """ + return [k for k, v in self.__model_data.items() if v.strip() == ""] diff --git a/pg_service_parser/core/pg_service_parser_wrapper.py b/pg_service_parser/core/pg_service_parser_wrapper.py index 9895c45..66be49d 100644 --- a/pg_service_parser/core/pg_service_parser_wrapper.py +++ b/pg_service_parser/core/pg_service_parser_wrapper.py @@ -5,14 +5,17 @@ def conf_path() -> Path: - path = pgserviceparser.conf_path() - return path if path.exists() else None + return pgserviceparser.conf_path() def service_names(conf_file_path: Optional[str] = None) -> List[str]: return pgserviceparser.service_names(conf_file_path) +def add_new_service(service_name: str, conf_file_path: Optional[str] = None) -> bool: + return create_service(service_name, {}, conf_file_path) + + def service_config(service_name: str, conf_file_path: Optional[str] = None) -> dict: return pgserviceparser.service_config(service_name, conf_file_path) @@ -37,7 +40,8 @@ def create_service( config.write(f) if service_name in config: - pgserviceparser.write_service(service_name, settings) + if settings: + pgserviceparser.write_service(service_name, settings) return True return False diff --git a/pg_service_parser/gui/dlg_pg_service.py b/pg_service_parser/gui/dlg_pg_service.py index d502922..689df51 100644 --- a/pg_service_parser/gui/dlg_pg_service.py +++ b/pg_service_parser/gui/dlg_pg_service.py @@ -1,15 +1,22 @@ +from pathlib import Path + +from qgis.core import QgsApplication from qgis.gui import QgsMessageBar from qgis.PyQt.QtCore import Qt, pyqtSlot from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QSizePolicy +from pg_service_parser.conf.service_settings import SERVICE_SETTINGS, SETTINGS_TEMPLATE from pg_service_parser.core.item_models import ServiceConfigModel from pg_service_parser.core.pg_service_parser_wrapper import ( + add_new_service, conf_path, copy_service_settings, service_config, service_names, write_service, ) +from pg_service_parser.gui.dlg_service_name import ServiceNameDialog +from pg_service_parser.gui.dlg_service_settings import ServiceSettingsDialog from pg_service_parser.utils import get_ui_class DIALOG_UI = get_ui_class("pg_service_dialog.ui") @@ -23,26 +30,46 @@ def __init__(self, parent): QDialog.__init__(self, parent) self.setupUi(self) + # Flag to handle initialization of new files + self.__new_empty_file = False + conf_file_path = conf_path() - if not conf_file_path: + self.__initialize_dialog(conf_file_path) + + def __initialize_dialog(self, conf_file_path): + if not conf_file_path.exists(): + self.btnCreateServiceFile.setIcon(QgsApplication.getThemeIcon("/mActionNewPage.svg")) + self.btnCreateServiceFile.clicked.connect(self.__create_file_clicked) self.lblConfFile.setText("Config file not found!") - self.lblConfFile.setToolTip( - "Set your PGSERVICEFILE environment variable and reopen the dialog." + not_found_tooltip = ( + "Create a config file at a default location or\n" + "set your PGSERVICEFILE environment variable and reopen the dialog." ) + self.lblConfFile.setToolTip(not_found_tooltip) + self.lblWarning.setToolTip(not_found_tooltip) self.txtConfFile.setVisible(False) self.tabWidget.setEnabled(False) return self.__edit_model = None + self.btnAddSettings.setIcon(QgsApplication.getThemeIcon("/symbologyAdd.svg")) + self.btnRemoveSetting.setIcon(QgsApplication.getThemeIcon("/symbologyRemove.svg")) self.txtConfFile.setText(str(conf_file_path)) self.lblWarning.setVisible(False) + self.lblConfFile.setText("Config file path found at ") + self.lblConfFile.setToolTip("") + self.txtConfFile.setVisible(True) + self.tabWidget.setEnabled(True) + self.btnCreateServiceFile.setVisible(False) 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.btnAddSettings.clicked.connect(self.__add_settings_clicked) + self.btnRemoveSetting.clicked.connect(self.__remove_setting_clicked) self.btnUpdateService.clicked.connect(self.__update_service_clicked) self.__initialize_edit_services() @@ -53,6 +80,19 @@ def __init__(self, parent): self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.layout().insertWidget(0, self.bar) + @pyqtSlot() + def __create_file_clicked(self): + dlg = ServiceNameDialog(self) + dlg.exec_() + if dlg.result() == QDialog.Accepted: + path = conf_path() + Path.touch(path) + add_new_service(dlg.service_name) + + # Set flag to get a template after some initialization + self.__new_empty_file = True + self.__initialize_dialog(path) + @pyqtSlot(bool) def __update_target_controls(self, checked): self.cboTargetService.setEnabled(self.radOverwrite.isChecked()) @@ -157,9 +197,50 @@ def __edit_service_changed(self, index): self.__edit_model.is_dirty_changed.connect(self.btnUpdateService.setEnabled) self.btnUpdateService.setDisabled(True) + if self.__new_empty_file: + # Add service template + self.__edit_model.add_settings(SETTINGS_TEMPLATE) + self.__new_empty_file = False + + @pyqtSlot() + def __add_settings_clicked(self): + dlg = ServiceSettingsDialog(self, self.__edit_model.current_setting_keys()) + dlg.exec_() + + if dlg.settings_to_add: + settings = {k: v for k, v in SERVICE_SETTINGS.items() if k in dlg.settings_to_add} + self.__edit_model.add_settings(settings) + + @pyqtSlot() + def __remove_setting_clicked(self): + selected_indexes = self.tblServiceConfig.selectedIndexes() + if selected_indexes: + setting_key = self.__edit_model.index_to_setting_key(selected_indexes[0]) + if ( + QMessageBox.question( + self, + "Remove service setting", + f"Are you sure you want to remove the '{setting_key}' setting?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + == QMessageBox.Yes + ): + self.__edit_model.remove_setting(selected_indexes[0]) + @pyqtSlot() def __update_service_clicked(self): if self.__edit_model and self.__edit_model.is_dirty(): + invalid = self.__edit_model.invalid_settings() + if invalid: + self.bar.pushWarning( + "PG service", + "Settings '{}' have invalid values. Adjust them and try again.".format( + "', '".join(invalid) + ), + ) + return + target_service = self.cboEditService.currentText() write_service(target_service, self.__edit_model.service_config()) self.bar.pushSuccess("PG service", f"PG service '{target_service}' updated!") diff --git a/pg_service_parser/gui/dlg_service_name.py b/pg_service_parser/gui/dlg_service_name.py new file mode 100644 index 0000000..edb7798 --- /dev/null +++ b/pg_service_parser/gui/dlg_service_name.py @@ -0,0 +1,21 @@ +from qgis.PyQt.QtCore import pyqtSlot +from qgis.PyQt.QtWidgets import QDialog + +from pg_service_parser.utils import get_ui_class + +DIALOG_UI = get_ui_class("service_name_dialog.ui") + + +class ServiceNameDialog(QDialog, DIALOG_UI): + + def __init__(self, parent): + QDialog.__init__(self, parent) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.__accepted) + self.service_name = "my-service" + + @pyqtSlot() + def __accepted(self): + if self.txtServiceName.text().strip(): + self.service_name = self.txtServiceName.text().replace(" ", "-") diff --git a/pg_service_parser/gui/dlg_service_settings.py b/pg_service_parser/gui/dlg_service_settings.py new file mode 100644 index 0000000..e74d591 --- /dev/null +++ b/pg_service_parser/gui/dlg_service_settings.py @@ -0,0 +1,24 @@ +from qgis.PyQt.QtCore import pyqtSlot +from qgis.PyQt.QtWidgets import QDialog + +from pg_service_parser.conf.service_settings import SERVICE_SETTINGS +from pg_service_parser.utils import get_ui_class + +DIALOG_UI = get_ui_class("service_settings_dialog.ui") + + +class ServiceSettingsDialog(QDialog, DIALOG_UI): + + def __init__(self, parent, settings_to_hide: list[str]): + QDialog.__init__(self, parent) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.__accepted) + + settings = set(SERVICE_SETTINGS.keys()) - set(settings_to_hide) + self.lstSettings.addItems(settings) + self.settings_to_add = [] + + @pyqtSlot() + def __accepted(self): + self.settings_to_add = [item.text() for item in self.lstSettings.selectedItems()] diff --git a/pg_service_parser/ui/pg_service_dialog.ui b/pg_service_parser/ui/pg_service_dialog.ui index 6ec4fa8..c43e301 100644 --- a/pg_service_parser/ui/pg_service_dialog.ui +++ b/pg_service_parser/ui/pg_service_dialog.ui @@ -67,6 +67,13 @@ + + + + Create file at default location + + + @@ -81,42 +88,8 @@ Edit - - - - - - - Edit service - - - - - - - - 0 - 0 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed @@ -144,7 +117,56 @@ - + + + + + + + 25 + 25 + + + + Add settings to current service + + + + + + + + + + + 25 + 25 + + + + Remove setting from current service + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + @@ -168,6 +190,40 @@ + + + + + + Edit service + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/pg_service_parser/ui/service_name_dialog.ui b/pg_service_parser/ui/service_name_dialog.ui new file mode 100644 index 0000000..69a0c96 --- /dev/null +++ b/pg_service_parser/ui/service_name_dialog.ui @@ -0,0 +1,99 @@ + + + dlgServiceName + + + + 0 + 0 + 260 + 164 + + + + Service name + + + + + + Enter a service name + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + e.g., my-service + + + true + + + + + + + + true + + + + color: rgb(154, 153, 150); + + + Leave empty to use a default name + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + dlgServiceName + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + dlgServiceName + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/pg_service_parser/ui/service_settings_dialog.ui b/pg_service_parser/ui/service_settings_dialog.ui new file mode 100644 index 0000000..033691e --- /dev/null +++ b/pg_service_parser/ui/service_settings_dialog.ui @@ -0,0 +1,74 @@ + + + dlgServiceSettings + + + + 0 + 0 + 226 + 205 + + + + Add service settings + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + + + + + + + buttonBox + accepted() + dlgServiceSettings + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + dlgServiceSettings + reject() + + + 316 + 260 + + + 286 + 274 + + + + +