Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Connections tab to list and manage (add/edit/remove) connections to a particular service #39

Merged
merged 5 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions pg_service_parser/core/connection_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from qgis.core import QgsAbstractDatabaseProviderConnection
from qgis.PyQt.QtCore import QAbstractTableModel, QModelIndex, Qt
from qgis.PyQt.QtGui import QFont


class ServiceConnectionModel(QAbstractTableModel):
KEY_COL = 0
VALUE_COL = 1

def __init__(
self, service_name: str, connections: dict[str, QgsAbstractDatabaseProviderConnection]
) -> None:
super().__init__()
self.__service_name = service_name
self.__model_data = connections

def rowCount(self, parent=QModelIndex()):
return len(self.__model_data)

def columnCount(self, parent=QModelIndex()):
return 2

def index_to_connection_key(self, index):
return list(self.__model_data.keys())[index.row()]

def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return None

key = list(self.__model_data.keys())[index.row()]
if role == Qt.ItemDataRole.DisplayRole:
if index.column() == self.KEY_COL:
return key
elif index.column() == self.VALUE_COL:
return self.__model_data[key].uri()
elif role == Qt.ItemDataRole.FontRole:
if index.column() == self.KEY_COL:
font = QFont()
font.setBold(True)
return font
elif index.column() == self.VALUE_COL:
font = QFont()
font.setItalic(True)
return font
elif role == Qt.ItemDataRole.ToolTipRole:
if index.column() == self.VALUE_COL:
return self.__model_data[key].uri()

return None

def headerData(self, section, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
if section == self.KEY_COL:
return "Connection name"
elif section == self.VALUE_COL:
return "URI"

return QAbstractTableModel.headerData(self, section, orientation, role)

def flags(self, idx):
if not idx.isValid():
return ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEnabled

return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled

def service_name(self):
return self.__service_name
48 changes: 48 additions & 0 deletions pg_service_parser/core/service_connections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from qgis.core import (
QgsAbstractDatabaseProviderConnection,
QgsDataSourceUri,
QgsProviderRegistry,
)
from qgis.gui import QgsGui
from qgis.PyQt.QtCore import QSettings
from qgis.PyQt.QtWidgets import QWidget


def get_connections(service: str) -> dict[str, QgsAbstractDatabaseProviderConnection]:
res = {}
provider = QgsProviderRegistry.instance().providerMetadata("postgres")
conns = provider.connections()
for key, pg_conn in conns.items():
if QgsDataSourceUri(pg_conn.uri()).service() == service:
res[key] = pg_conn

return res


def create_connection(service: str, connection_name: str) -> None:
config = {}
uri = f"service='{service}'"
provider = QgsProviderRegistry.instance().providerMetadata("postgres")
conn = provider.createConnection(uri, config)
provider.saveConnection(conn, connection_name)


def remove_connection(connection_name: str) -> None:
provider = QgsProviderRegistry.instance().providerMetadata("postgres")
provider.deleteConnection(connection_name)


def edit_connection(connection_name: str, parent: QWidget) -> None:
provider = QgsProviderRegistry.instance().providerMetadata("postgres")

if connection_name in provider.dbConnections():
pg = QgsGui.sourceSelectProviderRegistry().providerByName("postgres")
widget = pg.createDataSourceWidget(
parent, widgetMode=QgsProviderRegistry.WidgetMode.Standalone
)

settings = QSettings()
settings.setValue("PostgreSQL/connections/selected", connection_name)

widget.refresh() # To reflect the newly selected connection
widget.btnEdit_clicked()
42 changes: 42 additions & 0 deletions pg_service_parser/gui/dlg_new_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from enum import Enum

from qgis.PyQt.QtCore import pyqtSlot
from qgis.PyQt.QtWidgets import QDialog, QWidget

from pg_service_parser.utils import get_ui_class

DIALOG_UI = get_ui_class("new_name_dialog.ui")


class EnumNewName(Enum):
SERVICE = 0
CONNECTION = 1


class NewNameDialog(QDialog, DIALOG_UI):

def __init__(self, mode: EnumNewName, parent: QWidget, data: str = "") -> None:
QDialog.__init__(self, parent)
self.setupUi(self)
self.__mode = mode

self.buttonBox.accepted.connect(self.__accepted)

if self.__mode == EnumNewName.SERVICE:
self.setWindowTitle("Service name")
self.label.setText("Enter a service name")
self.txtNewName.setPlaceholderText("e.g., my-service")
self.new_name = "my-service"
elif self.__mode == EnumNewName.CONNECTION:
self.setWindowTitle("Connection name")
self.label.setText("Enter a connection name")
self.txtNewName.setPlaceholderText("e.g., My Service Connection")
self.new_name = f"{data} connection"

@pyqtSlot()
def __accepted(self):
if self.txtNewName.text().strip():
if self.__mode == EnumNewName.SERVICE:
self.new_name = self.txtNewName.text().strip().replace(" ", "-")
elif self.__mode == EnumNewName.CONNECTION:
self.new_name = self.txtNewName.text().strip()
111 changes: 105 additions & 6 deletions pg_service_parser/gui/dlg_pg_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from qgis.core import QgsApplication
from qgis.gui import QgsMessageBar
from qgis.PyQt.QtCore import QItemSelection, Qt, pyqtSlot
from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QSizePolicy
from qgis.PyQt.QtCore import QItemSelection, QModelIndex, Qt, pyqtSlot
from qgis.PyQt.QtWidgets import QDialog, QHeaderView, 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.connection_model import ServiceConnectionModel
from pg_service_parser.core.pg_service_parser_wrapper import (
add_new_service,
conf_path,
Expand All @@ -15,14 +15,22 @@
service_names,
write_service,
)
from pg_service_parser.gui.dlg_service_name import ServiceNameDialog
from pg_service_parser.core.service_connections import (
create_connection,
edit_connection,
get_connections,
remove_connection,
)
from pg_service_parser.core.setting_model import ServiceConfigModel
from pg_service_parser.gui.dlg_new_name import EnumNewName, NewNameDialog
from pg_service_parser.gui.dlg_service_settings import ServiceSettingsDialog
from pg_service_parser.gui.item_delegates import ServiceConfigDelegate
from pg_service_parser.utils import get_ui_class

DIALOG_UI = get_ui_class("pg_service_dialog.ui")
EDIT_TAB_INDEX = 0
COPY_TAB_INDEX = 1
CONNECTION_TAB_INDEX = 2


class PgServiceDialog(QDialog, DIALOG_UI):
Expand Down Expand Up @@ -53,16 +61,21 @@ def __initialize_dialog(self):
return

self.__edit_model = None
self.__connection_model = None

self.btnAddSettings.setIcon(QgsApplication.getThemeIcon("/symbologyAdd.svg"))
self.btnRemoveSetting.setIcon(QgsApplication.getThemeIcon("/symbologyRemove.svg"))
self.btnAddConnection.setIcon(QgsApplication.getThemeIcon("/symbologyAdd.svg"))
self.btnEditConnection.setIcon(QgsApplication.getThemeIcon("/symbologyEdit.svg"))
self.btnRemoveConnection.setIcon(QgsApplication.getThemeIcon("/symbologyRemove.svg"))
self.txtConfFile.setText(str(self.__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.tblServiceConnections.horizontalHeader().setVisible(True)
self.btnRemoveSetting.setEnabled(False)

self.radOverwrite.toggled.connect(self.__update_target_controls)
Expand All @@ -73,9 +86,15 @@ def __initialize_dialog(self):
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.cboConnectionService.currentIndexChanged.connect(self.__connection_service_changed)
self.btnAddConnection.clicked.connect(self.__add_connection_clicked)
self.btnEditConnection.clicked.connect(self.__edit_connection_clicked)
self.btnRemoveConnection.clicked.connect(self.__remove_connection_clicked)
self.tblServiceConnections.doubleClicked.connect(self.__edit_double_clicked_connection)

self.__initialize_edit_services()
self.__initialize_copy_services()
self.__initialize_connection_services()
self.__update_target_controls(True)
self.__update_add_settings_button()

Expand All @@ -85,11 +104,11 @@ def __initialize_dialog(self):

@pyqtSlot()
def __create_file_clicked(self):
dlg = ServiceNameDialog(self)
dlg = NewNameDialog(EnumNewName.SERVICE, self)
dlg.exec()
if dlg.result() == QDialog.DialogCode.Accepted:
Path.touch(self.__conf_file_path)
add_new_service(dlg.service_name)
add_new_service(dlg.new_name)

# Set flag to get a template after some initialization
self.__new_empty_file = True
Expand Down Expand Up @@ -138,6 +157,15 @@ def __initialize_edit_services(self):
self.cboEditService.addItems(service_names(self.__conf_file_path))
self.cboEditService.setCurrentText(current_text)

def __initialize_connection_services(self):
self.__connection_model = None
current_text = self.cboConnectionService.currentText() # Remember latest currentText
self.cboConnectionService.blockSignals(True) # Avoid triggering custom slot while clearing
self.cboConnectionService.clear()
self.cboConnectionService.blockSignals(False)
self.cboConnectionService.addItems(service_names(self.__conf_file_path))
self.cboConnectionService.setCurrentText(current_text)

@pyqtSlot()
def __copy_service(self):
# Validations
Expand Down Expand Up @@ -178,6 +206,8 @@ def __current_tab_changed(self, index):
pass # For now, services to be copied won't be altered in other tabs
elif index == EDIT_TAB_INDEX:
self.__initialize_edit_services()
elif index == CONNECTION_TAB_INDEX:
self.__initialize_connection_services()

@pyqtSlot(int)
def __edit_service_changed(self, index):
Expand Down Expand Up @@ -275,3 +305,72 @@ def __update_service_clicked(self):
self.__edit_model.set_not_dirty()
else:
self.bar.pushInfo("PG service", "Edit the service configuration and try again.")

@pyqtSlot(int)
def __connection_service_changed(self, index):
self.__initialize_service_connections()

def __initialize_service_connections(self, selected_index=QModelIndex()):
service = self.cboConnectionService.currentText()
self.__connection_model = ServiceConnectionModel(service, get_connections(service))
self.__update_connection_controls(False)
self.tblServiceConnections.setModel(self.__connection_model)
self.tblServiceConnections.horizontalHeader().setSectionResizeMode(
0, QHeaderView.ResizeToContents
)

self.tblServiceConnections.selectionModel().selectionChanged.connect(
self.__conn_table_selection_changed
)
self.tblServiceConnections.selectRow(selected_index.row()) # Remember selection

@pyqtSlot()
def __add_connection_clicked(self):
service = self.cboConnectionService.currentText()
dlg = NewNameDialog(EnumNewName.CONNECTION, self, service)
dlg.exec()
if dlg.result() == QDialog.DialogCode.Accepted:
create_connection(service, dlg.new_name)
self.__initialize_service_connections()

@pyqtSlot()
def __edit_connection_clicked(self):
selected_indexes = self.tblServiceConnections.selectedIndexes()
if selected_indexes:
self.__edit_connection(selected_indexes[0])

@pyqtSlot(QModelIndex)
def __edit_double_clicked_connection(self, index):
self.__edit_connection(index)

def __edit_connection(self, index):
connection_name = self.__connection_model.index_to_connection_key(index)
edit_connection(connection_name, self)
self.__initialize_service_connections(index)

@pyqtSlot()
def __remove_connection_clicked(self):
selected_indexes = self.tblServiceConnections.selectedIndexes()
if selected_indexes:
connection_name = self.__connection_model.index_to_connection_key(selected_indexes[0])
if (
QMessageBox.question(
self,
"Remove service connection",
f"Are you sure you want to remove the connection to '{connection_name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
== QMessageBox.StandardButton.Yes
):
remove_connection(connection_name)
self.__initialize_service_connections()

@pyqtSlot(QItemSelection, QItemSelection)
def __conn_table_selection_changed(self, selected, deselected):
selected_indexes = bool(self.tblServiceConnections.selectedIndexes())
self.__update_connection_controls(selected_indexes)

def __update_connection_controls(self, enable):
self.btnEditConnection.setEnabled(enable)
self.btnRemoveConnection.setEnabled(enable)
21 changes: 0 additions & 21 deletions pg_service_parser/gui/dlg_service_name.py

This file was deleted.

2 changes: 1 addition & 1 deletion pg_service_parser/gui/item_delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import QComboBox, QStyledItemDelegate

from pg_service_parser.core.item_models import ServiceConfigModel
from pg_service_parser.core.setting_model import ServiceConfigModel


class ServiceConfigDelegate(QStyledItemDelegate):
Expand Down
Loading