Skip to content

Commit ccf6b43

Browse files
Mtk112LKajan
authored andcommitted
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 31b68d5 commit ccf6b43

File tree

5 files changed

+179
-10
lines changed

5 files changed

+179
-10
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def get_plan_json(self):
152152

153153
self.lambda_service = LambdaService()
154154
self.lambda_service.jsons_received.connect(self.save_plan_jsons)
155-
self.lambda_service.send_request("get_plans", plan_id)
155+
self.lambda_service.serialize_plan(plan_id)
156156

157157
def save_plan_jsons(self, plan_json, outline_json):
158158
"""This slot saves the plan and outline JSONs to files."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 validate(self):
34+
"""Handles the button press to trigger the validation process."""
35+
active_plan_id = get_active_plan_id()
36+
if not active_plan_id:
37+
iface.messageBar().pushMessage("Virhe", "Ei aktiivista kaavaa.", level=3)
38+
return
39+
40+
self.lambda_service.validate_plan(active_plan_id)
41+
42+
def list_validation_errors(self, validation_json):
43+
"""Slot for listing validation errors and warnings."""
44+
if not validation_json:
45+
iface.messageBar().pushMessage("Virhe", "Validaatio json puuttuu.", level=1)
46+
return
47+
# Clear the existing errors from the list view
48+
self.error_list_model.setStringList([])
49+
50+
new_errors = []
51+
52+
if not validation_json:
53+
# If no errors or warnings, display a message and exit
54+
iface.messageBar().pushMessage("Virhe", "Ei virheitä havaittu.", level=1)
55+
return
56+
57+
for error_data in validation_json.values():
58+
if isinstance(error_data, dict):
59+
# Get the errors for this plan
60+
errors = error_data.get("errors", [])
61+
for error in errors:
62+
rule_id = error.get("ruleId", "Tuntematon sääntö")
63+
message = error.get("message", "Ei viestiä")
64+
instance = error.get("instance", "Tuntematon instance")
65+
error_message = f"Validointivirhe - Sääntö: {rule_id}, Viesti: {message}, Instance: {instance}"
66+
new_errors.append(error_message)
67+
68+
# Get any warnings for this plan using list comprehension
69+
warnings = error_data.get("warnings", [])
70+
new_errors.extend([f"Varoitus: {warning}" for warning in warnings])
71+
72+
# If no errors or warnings, display a message
73+
if not new_errors:
74+
new_errors.append("Kaava on validi. Ei virheitä tai varoituksia havaittu.")
75+
return
76+
77+
# Update the list view with the new errors and warnings
78+
self.error_list_model.setStringList(new_errors)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<ui version="4.0">
2+
<class>ValidationDock</class>
3+
<widget class="QDockWidget" name="ValidationDock">
4+
<property name="windowTitle">
5+
<string>Validaation tulokset</string>
6+
</property>
7+
<widget class="QWidget" name="dockWidgetContents">
8+
<layout class="QVBoxLayout" name="verticalLayout">
9+
<item>
10+
<layout class="QHBoxLayout" name="buttonLayout">
11+
<item>
12+
<widget class="QPushButton" name="validate_button">
13+
<property name="text">
14+
<string>Validoi aktiivinen kaava</string>
15+
</property>
16+
</widget>
17+
</item>
18+
</layout>
19+
</item>
20+
<item>
21+
<widget class="QListView" name="error_list_view"/>
22+
</item>
23+
</layout>
24+
</widget>
25+
</widget>
26+
<resources/>
27+
<connections/>
28+
</ui>

arho_feature_template/plugin.py

+22
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,11 @@ def initGui(self) -> None: # noqa N802
137138

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

141+
self.validation_dock = ValidationDock()
142+
iface.addDockWidget(Qt.RightDockWidgetArea, self.validation_dock)
143+
144+
self.validation_dock.visibilityChanged.connect(self.validation_dock_visibility_changed)
145+
140146
# icons to consider:
141147
# icon=QgsApplication.getThemeIcon("mActionStreamingDigitize.svg"),
142148
# icon=QgsApplication.getThemeIcon("mIconGeometryCollectionLayer.svg"),
@@ -171,6 +177,15 @@ def initGui(self) -> None: # noqa N802
171177
add_to_toolbar=True,
172178
)
173179

180+
self.validation_dock_action = self.add_action(
181+
text="Validointi virheet",
182+
icon=QgsApplication.getThemeIcon("mActionEditNodesItem.svg"),
183+
toggled_callback=self.toggle_validation_dock,
184+
checkable=True,
185+
add_to_menu=True,
186+
add_to_toolbar=True,
187+
)
188+
174189
self.new_plan_regulation_group = self.add_action(
175190
text="Luo kaavamääräysryhmä",
176191
icon=QgsApplication.getThemeIcon("mActionAddManualTable.svg"),
@@ -224,13 +239,20 @@ def unload(self) -> None:
224239
teardown_logger(Plugin.name)
225240

226241
self.templater.template_dock.close()
242+
self.validation_dock.close()
227243

228244
def dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001
229245
self.template_dock_action.setChecked(visible)
230246

231247
def toggle_template_dock(self, show: bool) -> None: # noqa: FBT001
232248
self.templater.template_dock.setUserVisible(show)
233249

250+
def validation_dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001
251+
self.validation_dock_action.setChecked(visible)
252+
253+
def toggle_validation_dock(self, show: bool) -> None: # noqa: FBT001
254+
self.validation_dock.setUserVisible(show)
255+
234256
def open_plan_regulation_group_form(self):
235257
self.new_plan_regulation_group_dialog = NewPlanRegulationGroupForm()
236258
self.new_plan_regulation_group_dialog.exec_()

0 commit comments

Comments
 (0)