diff --git a/arho_feature_template/core/lambda_service.py b/arho_feature_template/core/lambda_service.py index fbc5a69..f2f211d 100644 --- a/arho_feature_template/core/lambda_service.py +++ b/arho_feature_template/core/lambda_service.py @@ -1,6 +1,9 @@ from __future__ import annotations import json +import re +from http import HTTPStatus +from typing import cast from qgis.PyQt.QtCore import QByteArray, QObject, QUrl, pyqtSignal from qgis.PyQt.QtNetwork import QNetworkAccessManager, QNetworkProxy, QNetworkReply, QNetworkRequest @@ -11,15 +14,24 @@ class LambdaService(QObject): jsons_received = pyqtSignal(dict, dict) + validation_received = pyqtSignal(dict) + ActionAttribute = cast(QNetworkRequest.Attribute, QNetworkRequest.User + 1) + ACTION_VALIDATE_PLANS = "validate_plans" + ACTION_GET_PLANS = "get_plans" def __init__(self): - super().__init__() # Ensure QObject initialization - # Init network manager + super().__init__() self.network_manager = QNetworkAccessManager() + self.network_manager.finished.connect(self._handle_reply) - def send_request(self, action: str, plan_id: str): - """Sends a request to the lambda function.""" + def serialize_plan(self, plan_id: str): + self._send_request(action=self.ACTION_GET_PLANS, plan_id=plan_id) + + def validate_plan(self, plan_id: str): + self._send_request(action=self.ACTION_VALIDATE_PLANS, plan_id=plan_id) + def _send_request(self, action: str, plan_id: str): + """Sends a request to the lambda function.""" proxy_host, proxy_port, self.lambda_url = get_settings() # Initialize or reset proxy each time a request is sent. Incase settings have changed. @@ -35,22 +47,20 @@ def send_request(self, action: str, plan_id: str): payload = {"action": action, "plan_uuid": plan_id} payload_bytes = QByteArray(json.dumps(payload).encode("utf-8")) - request = QNetworkRequest(QUrl(self.lambda_url)) + request.setAttribute(LambdaService.ActionAttribute, action) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.network_manager.post(request, payload_bytes) - reply = self.network_manager.post(request, payload_bytes) - - # Connect reply signal to handle the response - reply.finished.connect(lambda: self._process_reply(reply)) - - def _process_reply(self, reply: QNetworkReply): - """Processes the reply from the lambda and emits signal.""" - plan_id = get_active_plan_id() + def _is_api_gateway_request(self) -> bool: + """Determines if the lambda request is going through the API Gateway.""" + match = re.match(r"^https://.*execute-api.*amazonaws\.com.*$", self.lambda_url) + return bool(match) + def _handle_reply(self, reply: QNetworkReply): if reply.error() != QNetworkReply.NoError: - error_string = reply.errorString() - QMessageBox.critical(None, "API Virhe", f"Lambdan kutsu epäonnistui: {error_string}") + error = reply.errorString() + QMessageBox.critical(None, "API Error", f"Lambda call failed: {error}") reply.deleteLater() return @@ -58,37 +68,58 @@ def _process_reply(self, reply: QNetworkReply): response_data = reply.readAll().data().decode("utf-8") response_json = json.loads(response_data) - # Determine if the proxy is set up. - if hasattr(self, "network_manager") and self.network_manager.proxy().type() == QNetworkProxy.Socks5Proxy: - # If proxy has been set up, retrieve 'details' directly - details = response_json.get("details", {}) + if not self._is_api_gateway_request(): + # If calling the lambda directly, the response includes status code and body + if int(response_json.get("statusCode", 0)) != HTTPStatus.OK: + error = response_json["body"] if "body" in response_json else response_json["errorMessage"] + QMessageBox.critical(None, "API Error", f"Lambda call failed: {error}") + reply.deleteLater() + return + body = response_json["body"] else: - # If proxy has not been set up (using local docker lambda), the response includes 'body'. - # In this case we need to retrieve 'details' from 'body' first. - body = response_json.get("body", {}) - details = body.get("details", {}) - - # Extract the plan JSON for the given plan_id - plan_json = details.get(plan_id, {}) - if not isinstance(plan_json, dict): - plan_json = {} - - outline_json = {} - if plan_json: - geographical_area = plan_json.get("geographicalArea") - if geographical_area: - outline_name = get_plan_name(plan_id, language="fin") - outline_json = { - "type": "Feature", - "properties": {"name": outline_name}, - "srid": geographical_area.get("srid"), - "geometry": geographical_area.get("geometry"), - } - - # Emit the signal with the two JSONs - self.jsons_received.emit(plan_json, outline_json) - - except json.JSONDecodeError as e: - QMessageBox.critical(None, "JSON Virhe", f"Failed to parse response JSON: {e}") + body = response_json + + except (json.JSONDecodeError, KeyError) as e: + QMessageBox.critical(None, "JSON Error", f"Failed to parse response JSON: {e}") + return finally: reply.deleteLater() + + action = reply.request().attribute(LambdaService.ActionAttribute) + if action == self.ACTION_GET_PLANS: + self._process_json_reply(body) + elif action == self.ACTION_VALIDATE_PLANS: + self._process_validation_reply(body) + + def _process_validation_reply(self, response_json: dict): + """Processes the validation reply from the lambda and emits a signal.""" + + validation_errors = response_json.get("ryhti_responses") + + self.validation_received.emit(validation_errors) + + def _process_json_reply(self, response_json: dict): + """Processes the reply from the lambda and emits signal.""" + plan_id = get_active_plan_id() + + details = response_json.get("details", {}) + + # Extract the plan JSON for the given plan_id + plan_json = details.get(plan_id, {}) + if not isinstance(plan_json, dict): + plan_json = {} + + outline_json = {} + if plan_json: + geographical_area = plan_json.get("geographicalArea") + if geographical_area: + outline_name = get_plan_name(plan_id, language="fin") + outline_json = { + "type": "Feature", + "properties": {"name": outline_name}, + "srid": geographical_area.get("srid"), + "geometry": geographical_area.get("geometry"), + } + + # Emit the signal with the two JSONs + self.jsons_received.emit(plan_json, outline_json) diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 0a4feb8..9e4f907 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -142,8 +142,8 @@ def get_plan_json(self): """Serializes plan and plan outline to JSON""" dialog = SerializePlan() if dialog.exec_() == QDialog.Accepted: - self.json_plan_path = dialog.plan_path_edit.text() - self.json_plan_outline_path = dialog.plan_outline_path_edit.text() + self.json_plan_path = str(dialog.plan_file.filePath()) + self.json_plan_outline_path = str(dialog.plan_outline_file.filePath()) plan_id = get_active_plan_id() if not plan_id: @@ -152,7 +152,7 @@ def get_plan_json(self): self.lambda_service = LambdaService() self.lambda_service.jsons_received.connect(self.save_plan_jsons) - self.lambda_service.send_request("get_plans", plan_id) + self.lambda_service.serialize_plan(plan_id) def save_plan_jsons(self, plan_json, outline_json): """This slot saves the plan and outline JSONs to files.""" @@ -188,7 +188,6 @@ def save_plan(plan_data: Plan) -> QgsFeature: edit_message = "Kaavan lisäys" if plan_data.id_ is None else "Kaavan muokkaus" plan_layer.beginEditCommand(edit_message) - plan_data.organisation_id = "99e20d66-9730-4110-815f-5947d3f8abd3" plan_feature = PlanLayer.feature_from_model(plan_data) if plan_data.id_ is None: diff --git a/arho_feature_template/gui/validation_dock.py b/arho_feature_template/gui/validation_dock.py new file mode 100644 index 0000000..49d2d77 --- /dev/null +++ b/arho_feature_template/gui/validation_dock.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from importlib import resources +from typing import TYPE_CHECKING + +from qgis.gui import QgsDockWidget +from qgis.PyQt import uic +from qgis.PyQt.QtCore import QStringListModel +from qgis.utils import iface + +from arho_feature_template.core.lambda_service import LambdaService +from arho_feature_template.utils.misc_utils import get_active_plan_id + +if TYPE_CHECKING: + from qgis.PyQt.QtWidgets import QListView + +ui_path = resources.files(__package__) / "validation_dock.ui" +DockClass, _ = uic.loadUiType(ui_path) + + +class ValidationDock(QgsDockWidget, DockClass): # type: ignore + error_list_view: QListView + + def __init__(self): + super().__init__() + self.setupUi(self) + self.lambda_service = LambdaService() + self.lambda_service.validation_received.connect(self.list_validation_errors) + self.error_list_model = QStringListModel() + self.error_list_view.setModel(self.error_list_model) + self.validate_button.clicked.connect(self.validate) + + def validate(self): + """Handles the button press to trigger the validation process.""" + active_plan_id = get_active_plan_id() + if not active_plan_id: + iface.messageBar().pushMessage("Virhe", "Ei aktiivista kaavaa.", level=3) + return + + self.lambda_service.validate_plan(active_plan_id) + + def list_validation_errors(self, validation_json): + """Slot for listing validation errors and warnings.""" + if not validation_json: + iface.messageBar().pushMessage("Virhe", "Validaatio json puuttuu.", level=1) + return + # Clear the existing errors from the list view + self.error_list_model.setStringList([]) + + new_errors = [] + + if not validation_json: + # If no errors or warnings, display a message and exit + iface.messageBar().pushMessage("Virhe", "Ei virheitä havaittu.", level=1) + return + + for error_data in validation_json.values(): + if isinstance(error_data, dict): + # Get the errors for this plan + errors = error_data.get("errors", []) + for error in errors: + rule_id = error.get("ruleId", "Tuntematon sääntö") + message = error.get("message", "Ei viestiä") + instance = error.get("instance", "Tuntematon instance") + error_message = f"Validointivirhe - Sääntö: {rule_id}, Viesti: {message}, Instance: {instance}" + new_errors.append(error_message) + + # Get any warnings for this plan using list comprehension + warnings = error_data.get("warnings", []) + new_errors.extend([f"Varoitus: {warning}" for warning in warnings]) + + # If no errors or warnings, display a message + if not new_errors: + new_errors.append("Kaava on validi. Ei virheitä tai varoituksia havaittu.") + return + + # Update the list view with the new errors and warnings + self.error_list_model.setStringList(new_errors) diff --git a/arho_feature_template/gui/validation_dock.ui b/arho_feature_template/gui/validation_dock.ui new file mode 100644 index 0000000..f2c4c3e --- /dev/null +++ b/arho_feature_template/gui/validation_dock.ui @@ -0,0 +1,28 @@ + + ValidationDock + + + Validaation tulokset + + + + + + + + + Validoi aktiivinen kaava + + + + + + + + + + + + + + diff --git a/arho_feature_template/plugin.py b/arho_feature_template/plugin.py index ff486ea..f365f87 100644 --- a/arho_feature_template/plugin.py +++ b/arho_feature_template/plugin.py @@ -12,6 +12,7 @@ from arho_feature_template.core.plan_manager import PlanManager from arho_feature_template.gui.new_plan_regulation_group_form import NewPlanRegulationGroupForm from arho_feature_template.gui.plugin_settings import PluginSettings +from arho_feature_template.gui.validation_dock import ValidationDock from arho_feature_template.qgis_plugin_tools.tools.custom_logging import setup_logger, teardown_logger from arho_feature_template.qgis_plugin_tools.tools.i18n import setup_translation from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_name @@ -137,6 +138,11 @@ def initGui(self) -> None: # noqa N802 iface.mapCanvas().mapToolSet.connect(self.templater.digitize_map_tool.deactivate) + self.validation_dock = ValidationDock() + iface.addDockWidget(Qt.RightDockWidgetArea, self.validation_dock) + + self.validation_dock.visibilityChanged.connect(self.validation_dock_visibility_changed) + # icons to consider: # icon=QgsApplication.getThemeIcon("mActionStreamingDigitize.svg"), # icon=QgsApplication.getThemeIcon("mIconGeometryCollectionLayer.svg"), @@ -171,6 +177,15 @@ def initGui(self) -> None: # noqa N802 add_to_toolbar=True, ) + self.validation_dock_action = self.add_action( + text="Validointi virheet", + icon=QgsApplication.getThemeIcon("mActionEditNodesItem.svg"), + toggled_callback=self.toggle_validation_dock, + checkable=True, + add_to_menu=True, + add_to_toolbar=True, + ) + self.new_plan_regulation_group = self.add_action( text="Luo kaavamääräysryhmä", icon=QgsApplication.getThemeIcon("mActionAddManualTable.svg"), @@ -224,6 +239,7 @@ def unload(self) -> None: teardown_logger(Plugin.name) self.templater.template_dock.close() + self.validation_dock.close() def dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001 self.template_dock_action.setChecked(visible) @@ -231,6 +247,12 @@ def dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001 def toggle_template_dock(self, show: bool) -> None: # noqa: FBT001 self.templater.template_dock.setUserVisible(show) + def validation_dock_visibility_changed(self, visible: bool) -> None: # noqa: FBT001 + self.validation_dock_action.setChecked(visible) + + def toggle_validation_dock(self, show: bool) -> None: # noqa: FBT001 + self.validation_dock.setUserVisible(show) + def open_plan_regulation_group_form(self): self.new_plan_regulation_group_dialog = NewPlanRegulationGroupForm() self.new_plan_regulation_group_dialog.exec_()