Skip to content

Commit 01b7e64

Browse files
committed
Present validation errors in a tree view
1 parent 046c56b commit 01b7e64

File tree

3 files changed

+159
-44
lines changed

3 files changed

+159
-44
lines changed

arho_feature_template/gui/validation_dock.py

+31-31
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,39 @@
55

66
from qgis.gui import QgsDockWidget
77
from qgis.PyQt import uic
8-
from qgis.PyQt.QtCore import QStringListModel
98
from qgis.utils import iface
109

1110
from arho_feature_template.core.lambda_service import LambdaService
1211
from arho_feature_template.utils.misc_utils import get_active_plan_id
1312

1413
if TYPE_CHECKING:
15-
from qgis.PyQt.QtWidgets import QListView, QProgressBar
14+
from qgis.PyQt.QtWidgets import QProgressBar, QPushButton
15+
16+
from arho_feature_template.gui.validation_tree_view import ValidationTreeView
1617

1718
ui_path = resources.files(__package__) / "validation_dock.ui"
1819
DockClass, _ = uic.loadUiType(ui_path)
1920

2021

2122
class ValidationDock(QgsDockWidget, DockClass): # type: ignore
22-
error_list_view: QListView
2323
progress_bar: QProgressBar
24+
validation_result_tree_view: ValidationTreeView
25+
validate_button: QPushButton
2426

25-
def __init__(self):
26-
super().__init__()
27+
def __init__(self, parent=None):
28+
super().__init__(parent)
2729
self.setupUi(self)
30+
2831
self.lambda_service = LambdaService()
2932
self.lambda_service.validation_received.connect(self.list_validation_errors)
30-
self.error_list_model = QStringListModel()
31-
self.error_list_view.setModel(self.error_list_model)
3233
self.validate_button.clicked.connect(self.validate)
3334

3435
def validate(self):
3536
"""Handles the button press to trigger the validation process."""
37+
38+
# Clear the existing errors from the list view
39+
self.validation_result_tree_view.clear_errors()
40+
3641
active_plan_id = get_active_plan_id()
3742
if not active_plan_id:
3843
iface.messageBar().pushMessage("Virhe", "Ei aktiivista kaavaa.", level=3)
@@ -49,38 +54,33 @@ def list_validation_errors(self, validation_json):
4954
if not validation_json:
5055
iface.messageBar().pushMessage("Virhe", "Validaatio json puuttuu.", level=1)
5156
return
52-
# Clear the existing errors from the list view
53-
self.error_list_model.setStringList([])
54-
55-
new_errors = []
5657

5758
if not validation_json:
5859
# If no errors or warnings, display a message and exit
5960
iface.messageBar().pushMessage("Virhe", "Ei virheitä havaittu.", level=1)
6061
return
6162

6263
for error_data in validation_json.values():
63-
if isinstance(error_data, dict):
64-
# Get the errors for this plan
65-
errors = error_data.get("errors", [])
66-
for error in errors:
67-
rule_id = error.get("ruleId", "Tuntematon sääntö")
68-
message = error.get("message", "Ei viestiä")
69-
instance = error.get("instance", "Tuntematon instance")
70-
error_message = f"Validointivirhe - Sääntö: {rule_id}, Viesti: {message}, Instance: {instance}"
71-
new_errors.append(error_message)
72-
73-
# Get any warnings for this plan using list comprehension
74-
warnings = error_data.get("warnings", [])
75-
new_errors.extend([f"Varoitus: {warning}" for warning in warnings])
76-
77-
# If no errors or warnings, display a message
78-
if not new_errors:
79-
new_errors.append("Kaava on validi. Ei virheitä tai varoituksia havaittu.")
80-
return
64+
if not isinstance(error_data, dict):
65+
continue
66+
errors = error_data.get("errors", [])
67+
for error in errors:
68+
self.validation_result_tree_view.add_error(
69+
error.get("ruleId", ""),
70+
error.get("instance", ""),
71+
error.get("message", ""),
72+
)
73+
74+
warnings = error_data.get("warnings", [])
75+
for warning in warnings:
76+
self.validation_result_tree_view.add_warning(
77+
warning.get("ruleId", ""),
78+
warning.get("instance", ""),
79+
warning.get("message", ""),
80+
)
8181

82-
# Update the list view with the new errors and warnings
83-
self.error_list_model.setStringList(new_errors)
8482
# Hide progress bar and re-enable the button
8583
self.progress_bar.setVisible(False)
8684
self.validate_button.setEnabled(True)
85+
self.validation_result_tree_view.expandAll()
86+
self.validation_result_tree_view.resizeColumnToContents(0)
+25-13
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
12
<ui version="4.0">
23
<class>ValidationDock</class>
34
<widget class="QDockWidget" name="ValidationDock">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>269</width>
10+
<height>362</height>
11+
</rect>
12+
</property>
413
<property name="windowTitle">
514
<string>Validaation tulokset</string>
615
</property>
716
<widget class="QWidget" name="dockWidgetContents">
817
<layout class="QVBoxLayout" name="verticalLayout">
918
<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+
<widget class="QPushButton" name="validate_button">
20+
<property name="text">
21+
<string>Validoi aktiivinen kaava</string>
22+
</property>
23+
</widget>
1924
</item>
2025
<item>
2126
<widget class="QProgressBar" name="progress_bar">
27+
<property name="visible">
28+
<bool>false</bool>
29+
</property>
2230
<property name="minimum">
2331
<number>0</number>
2432
</property>
2533
<property name="maximum">
2634
<number>0</number>
2735
</property>
28-
<property name="visible">
29-
<bool>false</bool>
30-
</property>
3136
</widget>
3237
</item>
3338
<item>
34-
<widget class="QListView" name="error_list_view"/>
39+
<widget class="ValidationTreeView" name="validation_result_tree_view"/>
3540
</item>
3641
</layout>
3742
</widget>
3843
</widget>
44+
<customwidgets>
45+
<customwidget>
46+
<class>ValidationTreeView</class>
47+
<extends>QTreeView</extends>
48+
<header>arho_feature_template.gui.validation_tree_view</header>
49+
</customwidget>
50+
</customwidgets>
3951
<resources/>
4052
<connections/>
4153
</ui>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import re
2+
from textwrap import dedent
3+
from typing import cast
4+
5+
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
6+
from qgis.PyQt.QtWidgets import QTreeView
7+
8+
category_map = {
9+
"plan": "Kaava",
10+
"geographicalArea": "Kaavan ulkoraja",
11+
"planObjects": "Kaavakohteet",
12+
"planRegulationGroups": "Kaavamääräysryhmät",
13+
}
14+
15+
16+
class ValidationItem(QStandardItem):
17+
def __init__(self, *args, **kwargs):
18+
super().__init__(*args, **kwargs)
19+
self.setEditable(False)
20+
21+
22+
class ValidationModel(QStandardItemModel):
23+
ERROR_INDEX = 0
24+
WARNING_INDEX = 1
25+
26+
def __init__(self) -> None:
27+
super().__init__()
28+
29+
self._parent_items: dict[str, ValidationItem] = {}
30+
31+
self.setColumnCount(3)
32+
self.setHorizontalHeaderLabels(["", "Attribuutti", "Viesti"])
33+
34+
self.root = self.invisibleRootItem()
35+
self.root.appendRow([ValidationItem("Virheet"), ValidationItem(""), ValidationItem("")])
36+
self.root.appendRow([ValidationItem("Varoitukset"), ValidationItem(""), ValidationItem("")])
37+
38+
def clear(self):
39+
self.item(self.ERROR_INDEX, 0).removeRows(0, self.item(self.ERROR_INDEX, 0).rowCount())
40+
self.item(self.WARNING_INDEX, 0).removeRows(0, self.item(self.WARNING_INDEX, 0).rowCount())
41+
self._parent_items = {}
42+
43+
def _add_item(self, root_index: int, error: str, instance: str, message: str) -> None:
44+
current_parent = cast(ValidationItem, self.item(root_index, 0))
45+
# replace '.planObjects[index]' with '.planObjects.index' so it is splittable by '.'
46+
instance = re.sub(r"\.(?P<object>\w+)\[(?P<object_num>\d+)\]", r".\g<object>.\g<object_num>", instance)
47+
path_parts = []
48+
attribute_index = None
49+
attribute = ""
50+
for i, instance_part in enumerate(instance.split(".")):
51+
if instance_part == "planObjects":
52+
attribute_index = i + 2
53+
54+
path_parts.append(instance_part)
55+
path = ".".join(path_parts)
56+
if i == attribute_index:
57+
attribute = instance_part
58+
self._parent_items[path] = current_parent
59+
elif path not in self._parent_items:
60+
new_item = ValidationItem(category_map.get(instance_part, instance_part))
61+
current_parent.appendRow([new_item, ValidationItem(""), ValidationItem("")])
62+
self._parent_items[path] = new_item
63+
current_parent = self._parent_items[path]
64+
65+
current_parent = self._parent_items[instance]
66+
message_item = ValidationItem(message)
67+
message_tooltip = dedent(
68+
f"""\
69+
<p>
70+
<span style='font-weight:bold'>Virhe:</span><br/>
71+
{error}
72+
</p>
73+
<p>
74+
<span style='font-weight:bold'>Virheviesti:</span><br/>
75+
{message}
76+
</p>
77+
"""
78+
)
79+
message_item.setToolTip(message_tooltip)
80+
current_parent.appendRow([ValidationItem(""), ValidationItem(attribute), message_item])
81+
82+
def add_error(self, error: str, instance: str, message: str) -> None:
83+
self._add_item(self.ERROR_INDEX, error, instance, message)
84+
85+
def add_warning(self, error: str, instance: str, message: str) -> None:
86+
self._add_item(self.WARNING_INDEX, error, instance, message)
87+
88+
89+
class ValidationTreeView(QTreeView):
90+
def __init__(self, parent=None) -> None:
91+
super().__init__(parent)
92+
93+
self._model = ValidationModel()
94+
self.setModel(self._model)
95+
96+
def clear_errors(self) -> None:
97+
self._model.clear()
98+
99+
def add_error(self, error: str, instance: str, message: str) -> None:
100+
self._model.add_error(error, instance, message)
101+
102+
def add_warning(self, error: str, instance: str, message: str) -> None:
103+
self._model.add_warning(error, instance, message)

0 commit comments

Comments
 (0)