Skip to content

Commit aebb8bc

Browse files
committed
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.
1 parent ae4c791 commit aebb8bc

14 files changed

+955
-1
lines changed
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from arho_feature_template.qgis_plugin_tools.tools.exceptions import QgsPluginException
2+
3+
4+
class AuthConfigException(QgsPluginException):
5+
pass
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from dataclasses import dataclass
2+
3+
from qgis.core import QgsMapLayer, QgsProject, QgsVectorLayer
4+
from qgis.utils import iface
5+
6+
7+
# To be extended and moved
8+
@dataclass
9+
class LandUsePlan:
10+
id: str
11+
12+
13+
# To be replaced later
14+
LAYER_PLAN_ID_MAP = {
15+
"Kaava": "id",
16+
"Maankäytön kohteet": "plan_id",
17+
"Muut pisteet": "plan_id",
18+
"Viivat": "plan_id",
19+
"Aluevaraus": "plan_id",
20+
"Osa-alue": "plan_id",
21+
}
22+
23+
24+
def update_selected_plan(new_plan: LandUsePlan):
25+
"""Update the project layers based on the selected land use plan."""
26+
plan_id = new_plan.id
27+
28+
for layer_name, field_name in LAYER_PLAN_ID_MAP.items():
29+
# Set the filter on each layer using the plan_id
30+
set_filter_for_vector_layer(layer_name, field_name, plan_id)
31+
32+
33+
def set_filter_for_vector_layer(layer_name: str, field_name: str, field_value: str):
34+
"""Set a filter for the given vector layer."""
35+
layers = QgsProject.instance().mapLayersByName(layer_name)
36+
37+
if not _check_layer_count(layers):
38+
return
39+
40+
layer = layers[0]
41+
42+
# Create the filter expression directly based on the plan_id
43+
expression = f"\"{field_name}\" = '{field_value}'" # Properly formatted filter expression
44+
45+
# Apply the filter to the layer
46+
if not layer.setSubsetString(expression):
47+
iface.messageBar().pushMessage("Error", f"Failed to filter layer {layer_name} with query {expression}", level=3)
48+
49+
50+
def _check_layer_count(layers: list) -> bool:
51+
"""Check if any layers are returned."""
52+
if not layers:
53+
iface.messageBar().pushMessage("Error", "ERROR: No layers found with the specified name.", level=3)
54+
return False
55+
return True
56+
57+
58+
def _check_vector_layer(layer: QgsMapLayer) -> bool:
59+
"""Check if the given layer is a vector layer."""
60+
if not isinstance(layer, QgsVectorLayer):
61+
iface.messageBar().pushMessage("Error", f"Layer {layer.name()} is not a vector layer: {type(layer)}", level=3)
62+
return False
63+
return True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from importlib import resources
4+
5+
from qgis.PyQt import uic
6+
from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QLineEdit
7+
8+
# Load the .ui file path using importlib resources
9+
ui_path = resources.files(__package__) / "ask_credentials.ui"
10+
11+
# Use uic.loadUiType to load the UI definition from the .ui file
12+
DbAskCredentialsDialogBase, _ = uic.loadUiType(ui_path)
13+
14+
15+
class DbAskCredentialsDialog(QDialog, DbAskCredentialsDialogBase): # type: ignore
16+
def __init__(self, parent: QDialog = None):
17+
super().__init__(parent)
18+
19+
# Set up the UI from the loaded .ui file
20+
self.setupUi(self)
21+
22+
# The UI elements defined in the .ui file
23+
self.userLineEdit: QLineEdit = self.findChild(QLineEdit, "userLineEdit")
24+
self.pwdLineEdit: QLineEdit = self.findChild(QLineEdit, "pwdLineEdit")
25+
self.buttonBox: QDialogButtonBox = self.findChild(QDialogButtonBox, "buttonBox")
26+
27+
# Connect the OK and Cancel buttons to their respective functions
28+
self.buttonBox.accepted.connect(self.accept)
29+
self.buttonBox.rejected.connect(self.reject)
30+
31+
def get_credentials(self) -> tuple[str, str]:
32+
"""
33+
Returns the entered username and password.
34+
:return: Tuple (username, password)
35+
"""
36+
return self.userLineEdit.text(), self.pwdLineEdit.text()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>DbAskCredentialsDialogBase</class>
4+
<widget class="QDialog" name="DbAskCredentialsDialogBase">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>400</width>
10+
<height>200</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Käyttäjän autentikaatio</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<layout class="QFormLayout" name="formLayout">
19+
<item row="0" column="0">
20+
<widget class="QLabel" name="label_username">
21+
<property name="text">
22+
<string>Käyttäjä:</string>
23+
</property>
24+
</widget>
25+
</item>
26+
<item row="0" column="1">
27+
<widget class="QLineEdit" name="userLineEdit">
28+
<property name="placeholderText">
29+
<string>Käyttäjä...</string>
30+
</property>
31+
</widget>
32+
</item>
33+
<item row="1" column="0">
34+
<widget class="QLabel" name="label_password">
35+
<property name="text">
36+
<string>Salasana:</string>
37+
</property>
38+
</widget>
39+
</item>
40+
<item row="1" column="1">
41+
<widget class="QLineEdit" name="pwdLineEdit">
42+
<property name="placeholderText">
43+
<string>Salasana...</string>
44+
</property>
45+
<property name="echoMode">
46+
<enum>QLineEdit::Password</enum>
47+
</property>
48+
</widget>
49+
</item>
50+
</layout>
51+
</item>
52+
<item>
53+
<widget class="QDialogButtonBox" name="buttonBox">
54+
<property name="orientation">
55+
<enum>Qt::Horizontal</enum>
56+
</property>
57+
<property name="standardButtons">
58+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
59+
</property>
60+
</widget>
61+
</item>
62+
</layout>
63+
</widget>
64+
<resources/>
65+
<connections/>
66+
</ui>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from importlib import resources
2+
3+
import psycopg2
4+
from qgis.PyQt import uic
5+
from qgis.PyQt.QtCore import QRegularExpression, QSortFilterProxyModel, Qt
6+
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
7+
from qgis.PyQt.QtWidgets import QComboBox, QDialog, QLineEdit, QMessageBox, QPushButton, QTableView
8+
9+
from arho_feature_template.gui.ask_credentials import DbAskCredentialsDialog
10+
from arho_feature_template.utils.db_utils import get_db_connection_params
11+
12+
ui_path = resources.files(__package__) / "connection_selection_dialog.ui"
13+
14+
ConnectionSelectionDialogBase, _ = uic.loadUiType(ui_path)
15+
16+
17+
class PlanFilterProxyModel(QSortFilterProxyModel):
18+
def filterAcceptsRow(self, source_row, source_parent): # noqa: N802
19+
model = self.sourceModel()
20+
if not model:
21+
return False
22+
23+
filter_text = self.filterRegularExpression().pattern()
24+
if not filter_text:
25+
return True
26+
27+
for column in range(5):
28+
index = model.index(source_row, column, source_parent)
29+
data = model.data(index)
30+
if data and filter_text.lower() in data.lower():
31+
return True
32+
33+
return False
34+
35+
36+
class ConnectionSelectionDialog(QDialog, ConnectionSelectionDialogBase): # type: ignore
37+
def __init__(self, parent, connections):
38+
super().__init__(parent)
39+
40+
uic.loadUi(ui_path, self)
41+
42+
self.connectionComboBox: QComboBox = self.findChild(QComboBox, "connectionComboBox")
43+
self.planTableView: QTableView = self.findChild(QTableView, "planTableView")
44+
self.okButton: QPushButton = self.findChild(QPushButton, "okButton")
45+
46+
self.searchLineEdit: QLineEdit = self.findChild(QLineEdit, "searchLineEdit")
47+
self.searchLineEdit.setPlaceholderText("Etsi kaavoja...")
48+
49+
self.connectionComboBox.addItems(connections)
50+
51+
self.connectionComboBox.currentIndexChanged.connect(self.load_plans)
52+
self.okButton.clicked.connect(self.accept)
53+
54+
self.okButton.setEnabled(False)
55+
56+
self.filterProxyModel = PlanFilterProxyModel(self)
57+
self.filterProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
58+
59+
self.planTableView.setModel(self.filterProxyModel)
60+
self.searchLineEdit.textChanged.connect(self.filter_plans)
61+
62+
self.selected_plan_id = None
63+
64+
def load_plans(self):
65+
selected_connection = self.connectionComboBox.currentText()
66+
if not selected_connection:
67+
self.planTableView.setModel(QStandardItemModel())
68+
return
69+
70+
cursor = None
71+
conn = None
72+
73+
try:
74+
conn_params = get_db_connection_params(selected_connection)
75+
76+
if not conn_params.get("user") or not conn_params.get("password"):
77+
# Trigger dialog to ask for missing credentials
78+
dialog = DbAskCredentialsDialog(self)
79+
dialog.rejected.connect(self.reject)
80+
if dialog.exec() == QDialog.Accepted:
81+
user, password = dialog.get_credentials()
82+
conn_params["user"] = user
83+
conn_params["password"] = password
84+
85+
conn = psycopg2.connect(
86+
host=conn_params["host"],
87+
port=conn_params["port"],
88+
dbname=conn_params["dbname"],
89+
user=conn_params["user"],
90+
password=conn_params["password"],
91+
sslmode=conn_params["sslmode"],
92+
)
93+
94+
cursor = conn.cursor()
95+
96+
cursor.execute("""
97+
SELECT
98+
p.id,
99+
p.producers_plan_identifier,
100+
p.name ->> 'fin' AS name_fin,
101+
l.name ->> 'fin' AS lifecycle_status_fin,
102+
pt.name ->> 'fin' AS plan_type_fin
103+
FROM
104+
hame.plan p
105+
LEFT JOIN
106+
codes.lifecycle_status l
107+
ON
108+
p.lifecycle_status_id = l.id
109+
LEFT JOIN
110+
codes.plan_type pt
111+
ON
112+
p.plan_type_id = pt.id;
113+
""")
114+
plans = cursor.fetchall()
115+
116+
model = QStandardItemModel(len(plans), 5)
117+
model.setHorizontalHeaderLabels(
118+
[
119+
"ID",
120+
"Tuottajan kaavatunnus",
121+
"Nimi",
122+
"Kaavan elinkaaren tila",
123+
"Kaavalaji",
124+
]
125+
)
126+
127+
for row_idx, plan in enumerate(plans):
128+
model.setItem(row_idx, 0, QStandardItem(str(plan[0]))) # id
129+
model.setItem(row_idx, 1, QStandardItem(str(plan[1]))) # producer_plan_identifier
130+
model.setItem(row_idx, 2, QStandardItem(str(plan[2]))) # name_fin
131+
model.setItem(row_idx, 3, QStandardItem(str(plan[3]))) # lifecycle_status_fin
132+
model.setItem(row_idx, 4, QStandardItem(str(plan[4]))) # plan_type_fin
133+
134+
self.filterProxyModel.setSourceModel(model)
135+
136+
self.planTableView.setSelectionMode(QTableView.SingleSelection)
137+
self.planTableView.setSelectionBehavior(QTableView.SelectRows)
138+
139+
self.planTableView.selectionModel().selectionChanged.connect(self.on_selection_changed)
140+
141+
except ValueError as ve:
142+
QMessageBox.critical(self, "Connection Error", str(ve))
143+
self.planTableView.setModel(QStandardItemModel())
144+
145+
except Exception as e: # noqa: BLE001
146+
QMessageBox.critical(self, "Error", f"Failed to load plans: {e}")
147+
self.planTableView.setModel(QStandardItemModel())
148+
149+
finally:
150+
if cursor:
151+
cursor.close()
152+
if conn:
153+
conn.close()
154+
155+
def filter_plans(self):
156+
search_text = self.searchLineEdit.text()
157+
if search_text:
158+
search_regex = QRegularExpression(search_text)
159+
self.filterProxyModel.setFilterRegularExpression(search_regex)
160+
else:
161+
self.filterProxyModel.setFilterRegularExpression("")
162+
163+
def on_selection_changed(self):
164+
# Enable the OK button only if a row is selected
165+
selection = self.planTableView.selectionModel().selectedRows()
166+
if selection:
167+
selected_row = selection[0].row()
168+
self.selected_plan_id = self.planTableView.model().index(selected_row, 0).data()
169+
self.okButton.setEnabled(True)
170+
else:
171+
self.selected_plan_id = None
172+
self.okButton.setEnabled(False)
173+
174+
def get_selected_connection(self):
175+
return self.connectionComboBox.currentText()
176+
177+
def get_selected_plan_id(self):
178+
return self.selected_plan_id

0 commit comments

Comments
 (0)