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

Yksinkertainen kaavan validointi #92

Merged
merged 4 commits into from
Dec 12, 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
123 changes: 77 additions & 46 deletions arho_feature_template/core/lambda_service.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -35,60 +47,79 @@ 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

try:
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)
7 changes: 3 additions & 4 deletions arho_feature_template/core/plan_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down
78 changes: 78 additions & 0 deletions arho_feature_template/gui/validation_dock.py
Original file line number Diff line number Diff line change
@@ -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)
28 changes: 28 additions & 0 deletions arho_feature_template/gui/validation_dock.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<ui version="4.0">
<class>ValidationDock</class>
<widget class="QDockWidget" name="ValidationDock">
<property name="windowTitle">
<string>Validaation tulokset</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="validate_button">
<property name="text">
<string>Validoi aktiivinen kaava</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="error_list_view"/>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
22 changes: 22 additions & 0 deletions arho_feature_template/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -224,13 +239,20 @@ 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)

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_()
Loading