Skip to content

Commit dc6b926

Browse files
committed
Added plan validation
Added new dock widget for validating plans. It includes a button to validate plan, and a list view for listing each validation error. Refactored LambdaService so that it can handle each type of lambda call.
1 parent efb717d commit dc6b926

File tree

5 files changed

+184
-14
lines changed

5 files changed

+184
-14
lines changed

arho_feature_template/core/lambda_service.py

+50-9
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,24 @@
1111

1212
class LambdaService(QObject):
1313
jsons_received = pyqtSignal(dict, dict)
14+
validation_received = pyqtSignal(dict)
15+
ActionAttribute = QNetworkRequest.User + 1
16+
ACTION_VALIDATE_PLANS = "validate_plans"
17+
ACTION_GET_PLANS = "get_plans"
1418

1519
def __init__(self):
16-
super().__init__() # Ensure QObject initialization
17-
# Init network manager
20+
super().__init__()
1821
self.network_manager = QNetworkAccessManager()
22+
self.network_manager.finished.connect(self._handle_reply)
1923

20-
def send_request(self, action: str, plan_id: str):
21-
"""Sends a request to the lambda function."""
24+
def serialize_plan(self, plan_id: str):
25+
self._send_request(action=self.ACTION_GET_PLANS, plan_id=plan_id)
26+
27+
def validate_plan(self, plan_id: str):
28+
self._send_request(action=self.ACTION_VALIDATE_PLANS, plan_id=plan_id)
2229

30+
def _send_request(self, action: str, plan_id: str):
31+
"""Sends a request to the lambda function."""
2332
proxy_host, proxy_port, self.lambda_url = get_settings()
2433

2534
# Initialize or reset proxy each time a request is sent. Incase settings have changed.
@@ -35,16 +44,48 @@ def send_request(self, action: str, plan_id: str):
3544

3645
payload = {"action": action, "plan_uuid": plan_id}
3746
payload_bytes = QByteArray(json.dumps(payload).encode("utf-8"))
38-
3947
request = QNetworkRequest(QUrl(self.lambda_url))
48+
request.setAttribute(LambdaService.ActionAttribute, action)
4049
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
50+
self.network_manager.post(request, payload_bytes)
51+
52+
def _handle_reply(self, reply: QNetworkReply):
53+
action = reply.request().attribute(LambdaService.ActionAttribute)
54+
if action == self.ACTION_GET_PLANS:
55+
self._process_json_reply(reply)
56+
elif action == self.ACTION_VALIDATE_PLANS:
57+
self._process_validation_reply(reply)
4158

42-
reply = self.network_manager.post(request, payload_bytes)
59+
def _process_validation_reply(self, reply: QNetworkReply):
60+
"""Processes the validation reply from the lambda and emits a signal."""
61+
if reply.error() != QNetworkReply.NoError:
62+
error_string = reply.errorString()
63+
QMessageBox.critical(None, "API Error", f"Lambda call failed: {error_string}")
64+
reply.deleteLater()
65+
return
4366

44-
# Connect reply signal to handle the response
45-
reply.finished.connect(lambda: self._process_reply(reply))
67+
try:
68+
response_data = reply.readAll().data().decode("utf-8")
69+
response_json = json.loads(response_data)
70+
71+
# Determine if the proxy is set up.
72+
if hasattr(self, "network_manager") and self.network_manager.proxy().type() == QNetworkProxy.Socks5Proxy:
73+
# If proxy has been set up, retrieve 'ryhti_responses' directly
74+
validation_errors = response_json.get("ryhti_responses", {})
75+
else:
76+
# If proxy has not been set up (using local docker lambda), the response includes 'body'.
77+
# In this case we need to retrieve 'ryhti_responses' from 'body' first.
78+
body = response_json.get("body", {})
79+
validation_errors = body.get("ryhti_responses", {})
80+
81+
self.validation_received.emit(validation_errors)
82+
83+
except json.JSONDecodeError as e:
84+
QMessageBox.critical(None, "JSON Error", f"Failed to parse response JSON: {e}")
85+
finally:
86+
reply.deleteLater()
4687

47-
def _process_reply(self, reply: QNetworkReply):
88+
def _process_json_reply(self, reply: QNetworkReply):
4889
"""Processes the reply from the lambda and emits signal."""
4990
plan_id = get_active_plan_id()
5091

arho_feature_template/core/plan_manager.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ def _feature_added(self):
117117

118118
if plan_layer.isEditable():
119119
if not plan_layer.commitChanges():
120-
iface.messageBar().pushMessage("Error", "Failed to commit changes to the layer.", level=3)
120+
iface.messageBar().pushMessage("Virhe", "Muutosten tallennus tasolle epäonnistui.", level=3)
121121
return
122122
else:
123-
iface.messageBar().pushMessage("Error", "Layer is not editable.", level=3)
123+
iface.messageBar().pushMessage("Virhe", "Taso ei ole muokattavissa.", level=3)
124124
return
125125

126126
feature_ids_after_commit = plan_layer.allFeatureIds()
@@ -135,9 +135,9 @@ def _feature_added(self):
135135
feature_id_value = new_feature["id"]
136136
self.set_active_plan(feature_id_value)
137137
else:
138-
iface.messageBar().pushMessage("Error", "Invalid feature retrieved.", level=3)
138+
iface.messageBar().pushMessage("Virhe", "Virheellinen kohde (feature).", level=3)
139139
else:
140-
iface.messageBar().pushMessage("Error", "No new feature was added.", level=3)
140+
iface.messageBar().pushMessage("Virhe", "Uutta kohdetta (feature) ei lisätty.", level=3)
141141

142142
def set_active_plan(self, plan_id: str | None):
143143
"""Update the project layers based on the selected land use plan."""
@@ -201,7 +201,7 @@ def get_plan_json(self):
201201

202202
self.lambda_service = LambdaService()
203203
self.lambda_service.jsons_received.connect(self.save_plan_jsons)
204-
self.lambda_service.send_request("get_plans", plan_id)
204+
self.lambda_service.serialize_plan(plan_id)
205205

206206
def save_plan_jsons(self, plan_json, outline_json):
207207
"""This slot saves the plan and outline JSONs to files."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from __future__ import annotations
2+
3+
from importlib import resources
4+
from typing import TYPE_CHECKING
5+
6+
from qgis.gui import QgsDockWidget
7+
from qgis.PyQt import uic
8+
from qgis.PyQt.QtCore import QStringListModel
9+
from qgis.utils import iface
10+
11+
from arho_feature_template.core.lambda_service import LambdaService
12+
from arho_feature_template.utils.misc_utils import get_active_plan_id
13+
14+
if TYPE_CHECKING:
15+
from qgis.PyQt.QtWidgets import QListView
16+
17+
ui_path = resources.files(__package__) / "validation_dock.ui"
18+
DockClass, _ = uic.loadUiType(ui_path)
19+
20+
21+
class ValidationDock(QgsDockWidget, DockClass): # type: ignore
22+
error_list_view: QListView
23+
24+
def __init__(self):
25+
super().__init__()
26+
self.setupUi(self)
27+
self.lambda_service = LambdaService()
28+
self.lambda_service.validation_received.connect(self.list_validation_errors)
29+
self.error_list_model = QStringListModel()
30+
self.error_list_view.setModel(self.error_list_model)
31+
self.validate_button.clicked.connect(self.validate)
32+
33+
def update_errors(self, errors: list[str]):
34+
"""Updates the list view with the provided validation errors."""
35+
self.error_list_model.setStringList(errors)
36+
37+
def validate(self):
38+
"""Handles the button press to trigger the validation process."""
39+
active_plan_id = get_active_plan_id()
40+
if not active_plan_id:
41+
iface.messageBar().pushMessage("Virhe", "Ei aktiivista kaavaa.", level=3)
42+
return
43+
44+
self.lambda_service.validate_plan(active_plan_id)
45+
46+
def list_validation_errors(self, validation_json):
47+
"""Slot for listing validation errors."""
48+
if not validation_json:
49+
iface.messageBar().pushMessage("Virhe", "Ei virheitä havaittu.", level=1)
50+
return
51+
52+
current_errors = self.error_list_model.stringList()
53+
new_errors = []
54+
55+
for plan_id, error_data in validation_json.items():
56+
if isinstance(error_data, str):
57+
new_errors.append(f"Validaatio epäonnistui Kaavalle id {plan_id}: {error_data}")
58+
elif isinstance(error_data, dict):
59+
title = error_data.get("title", "Tuntematon virhe")
60+
status = error_data.get("status", "Tuntematon status")
61+
detail = error_data.get("detail", "Tietoja ei saatavilla")
62+
new_errors.append(f"Virhe kaavalla: {plan_id} - {title} ({status}): {detail}")
63+
64+
errors = error_data.get("errors", [])
65+
for error in errors:
66+
rule_id = error.get("ruleId", "Tuntematon sääntö")
67+
message = error.get("message", "Ei viestiä")
68+
instance = error.get("instance", "Tuntematon instanssi")
69+
error_message = f" Sääntö: {rule_id}, Viesti: {message}, Tapaus/instanssi: {instance}"
70+
new_errors.append(error_message)
71+
72+
warnings = error_data.get("warnings", [])
73+
new_errors.extend([f"Varoitus: {warning}" for warning in warnings])
74+
75+
# Append the new errors to the existing list
76+
current_errors.extend(new_errors)
77+
78+
# Update the model with the combined list of errors
79+
self.error_list_model.setStringList(current_errors)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>ValidationDock</class>
4+
<widget class="QDockWidget" name="ValidationDock">
5+
<property name="windowTitle">
6+
<string>Validaation tulokset</string>
7+
</property>
8+
<widget class="QWidget" name="dockWidgetContents">
9+
<layout class="QVBoxLayout" name="verticalLayout">
10+
<item>
11+
<widget class="QPushButton" name="validate_button">
12+
<property name="text">
13+
<string>Vahvista aktiivinen kaava</string>
14+
</property>
15+
</widget>
16+
</item>
17+
<item>
18+
<widget class="QListView" name="error_list_view"/>
19+
</item>
20+
</layout>
21+
</widget>
22+
</widget>
23+
<resources/>
24+
<connections/>
25+
</ui>

arho_feature_template/plugin.py

+25
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from arho_feature_template.core.plan_manager import PlanManager
1313
from arho_feature_template.gui.new_plan_regulation_group_form import NewPlanRegulationGroupForm
1414
from arho_feature_template.gui.plugin_settings import PluginSettings
15+
from arho_feature_template.gui.validation_dock import ValidationDock
1516
from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger
1617
from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation
1718
from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name
@@ -137,6 +138,13 @@ def initGui(self) -> None: # noqa N802
137138

138139
iface.mapCanvas().mapToolSet.connect(self.templater.digitize_map_tool.deactivate)
139140

141+
# Instantiate and add the Validation Dock Widget
142+
self.validation_dock = ValidationDock()
143+
iface.addDockWidget(Qt.BottomDockWidgetArea, self.validation_dock)
144+
145+
# Connect visibilityChanged signal to sync toggle action
146+
self.validation_dock.visibilityChanged.connect(self.validation_dock_visibility_changed)
147+
140148
# icons to consider:
141149
# icon=QgsApplication.getThemeIcon("mActionStreamingDigitize.svg"),
142150
# icon=QgsApplication.getThemeIcon("mIconGeometryCollectionLayer.svg"),
@@ -171,6 +179,17 @@ def initGui(self) -> None: # noqa N802
171179
add_to_toolbar=True,
172180
)
173181

182+
# Add the toggle action for the validation dock widget
183+
self.validation_dock_action = self.add_action(
184+
"",
185+
"Validointi virheet",
186+
None,
187+
toggled_callback=self.toggle_validation_dock,
188+
checkable=True,
189+
add_to_menu=True,
190+
add_to_toolbar=True,
191+
)
192+
174193
self.new_plan_regulation_group = self.add_action(
175194
text="Luo kaavamääräysryhmä",
176195
icon=QgsApplication.getThemeIcon("mActionAddManualTable.svg"),
@@ -231,6 +250,12 @@ def dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001
231250
def toggle_template_dock(self, show: bool) -> None: # noqa: FBT001
232251
self.templater.template_dock.setUserVisible(show)
233252

253+
def validation_dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001
254+
self.validation_dock_action.setChecked(visible)
255+
256+
def toggle_validation_dock(self, show: bool) -> None: # noqa: FBT001
257+
self.validation_dock.setUserVisible(show)
258+
234259
def open_plan_regulation_group_form(self):
235260
self.new_plan_regulation_group_dialog = NewPlanRegulationGroupForm()
236261
self.new_plan_regulation_group_dialog.exec_()

0 commit comments

Comments
 (0)