Skip to content

Commit c1ebc27

Browse files
Mtk112LKajan
authored andcommitted
Implement lifecycle management for the plan
1 parent 261e550 commit c1ebc27

9 files changed

+381
-8
lines changed

arho_feature_template/core/models.py

+21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from datetime import datetime
1919

2020
from qgis.core import QgsFeature, QgsGeometry
21+
from qgis.PyQt.QtCore import QDate
2122

2223

2324
logger = logging.getLogger(__name__)
@@ -96,6 +97,7 @@ def from_config_file(
9697
for group_name in feature_data.get("regulation_groups", [])
9798
if (group := cls.find_matching_group_config(group_name, regulation_group_libraries))
9899
],
100+
lifecycles=[],
99101
plan_id=None,
100102
id_=None,
101103
)
@@ -514,6 +516,23 @@ def from_config_data(cls, data: dict) -> RegulationGroup:
514516
)
515517

516518

519+
@dataclass
520+
class LifeCycle:
521+
status_id: str | None = None
522+
id_: str | None = None
523+
plan_id: str | None = None
524+
plan_regulation_id: str | None = None
525+
plan_proposition_id: str | None = None
526+
starting_at: QDate | None = None
527+
ending_at: QDate | None = None
528+
# might not need the following
529+
land_use_are_id: str | None = None
530+
other_area_id: str | None = None
531+
line_id: str | None = None
532+
land_use_point_id: str | None = None
533+
other_point_id: str | None = None
534+
535+
517536
@dataclass
518537
class PlanFeature:
519538
geom: QgsGeometry | None = None # Need to allow None for feature templates
@@ -522,6 +541,7 @@ class PlanFeature:
522541
name: str | None = None
523542
description: str | None = None
524543
regulation_groups: list[RegulationGroup] = field(default_factory=list)
544+
lifecycles: list[LifeCycle] = field(default_factory=list)
525545
plan_id: int | None = None
526546
id_: str | None = None
527547

@@ -537,6 +557,7 @@ class Plan:
537557
description: str | None = None
538558
plan_type_id: str | None = None
539559
lifecycle_status_id: str | None = None
560+
lifecycles: list[LifeCycle] = field(default_factory=list)
540561
record_number: str | None = None
541562
matter_management_identifier: str | None = None
542563
permanent_plan_identifier: str | None = None

arho_feature_template/core/plan_manager.py

+37-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
AdditionalInformation,
1515
Document,
1616
FeatureTemplateLibrary,
17+
LifeCycle,
1718
Plan,
1819
PlanFeature,
1920
RegulationGroup,
2021
RegulationGroupCategory,
2122
RegulationGroupLibrary,
2223
)
2324
from arho_feature_template.exceptions import UnsavedChangesError
25+
from arho_feature_template.gui.dialogs.lifecycle_editor import LifecycleEditor
2426
from arho_feature_template.gui.dialogs.load_plan_dialog import LoadPlanDialog
2527
from arho_feature_template.gui.dialogs.plan_attribute_form import PlanAttributeForm
2628
from arho_feature_template.gui.dialogs.plan_feature_form import PlanFeatureForm
@@ -35,6 +37,7 @@
3537
DocumentLayer,
3638
LandUseAreaLayer,
3739
LandUsePointLayer,
40+
LifeCycleLayer,
3841
LineLayer,
3942
OtherAreaLayer,
4043
OtherPointLayer,
@@ -231,7 +234,6 @@ def edit_plan(self):
231234

232235
feature = PlanLayer.get_feature_by_id(get_active_plan_id(), no_geometries=False)
233236
if feature is None:
234-
iface.messageBar().pushWarning("", "No active/open plan found!")
235237
return
236238
plan_model = PlanLayer.model_from_feature(feature)
237239

@@ -240,6 +242,21 @@ def edit_plan(self):
240242
feature = save_plan(attribute_form.model)
241243
self.regulation_groups_dock.initialize_regulation_groups(regulation_group_library_from_active_plan())
242244

245+
def edit_lifecycles(self):
246+
plan_layer = PlanLayer.get_from_project()
247+
if not plan_layer:
248+
return
249+
250+
feature = PlanLayer.get_feature_by_id(get_active_plan_id(), no_geometries=False)
251+
if feature is None:
252+
return
253+
plan_model = PlanLayer.model_from_feature(feature)
254+
255+
lifecycle_editor = LifecycleEditor(plan=plan_model)
256+
257+
if lifecycle_editor.exec_():
258+
save_plan(plan_model)
259+
243260
def add_new_plan_feature(self):
244261
if not handle_unsaved_changes():
245262
return
@@ -304,7 +321,6 @@ def edit_plan_feature(self, feature: QgsFeature, layer_name: str):
304321
layer_class = FEATURE_LAYER_NAME_TO_CLASS_MAP[layer_name]
305322
plan_feature = layer_class.model_from_feature(feature)
306323

307-
# Geom editing handled with basic QGIS vertex editing?
308324
title = plan_feature.name if plan_feature.name else layer_name
309325
attribute_form = PlanFeatureForm(
310326
plan_feature, title, [*self.regulation_group_libraries, regulation_group_library_from_active_plan()]
@@ -534,6 +550,11 @@ def save_plan(plan: Plan) -> QgsFeature:
534550
document.plan_id = plan_id
535551
save_document(document)
536552

553+
# Save lifecycles
554+
for lifecycle in plan.lifecycles:
555+
lifecycle.plan_id = plan_id
556+
save_lifecycle(lifecycle)
557+
537558
return feature
538559

539560

@@ -736,3 +757,17 @@ def save_document(document: Document) -> QgsFeature:
736757
)
737758

738759
return feature
760+
761+
762+
def save_lifecycle(lifecycle: LifeCycle) -> QgsFeature:
763+
"""Save a LifeCycle object to the layer."""
764+
feature = LifeCycleLayer.feature_from_model(lifecycle)
765+
layer = LifeCycleLayer.get_from_project()
766+
767+
_save_feature(
768+
feature=feature,
769+
layer=layer,
770+
id_=lifecycle.id_,
771+
edit_text="Elinkaaren lisäys" if lifecycle.id_ is None else "Elinkaaren muokkaus",
772+
)
773+
return feature
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
from qgis.gui import QgsDateTimeEdit
4+
from qgis.PyQt.QtCore import Qt, pyqtSignal
5+
from qgis.PyQt.QtWidgets import (
6+
QHeaderView,
7+
QTableWidget,
8+
QTableWidgetItem,
9+
)
10+
11+
from arho_feature_template.core.models import LifeCycle
12+
from arho_feature_template.gui.components.code_combobox import CodeComboBox
13+
from arho_feature_template.project.layers.code_layers import LifeCycleStatusLayer
14+
15+
16+
class LifecycleTableWidget(QTableWidget):
17+
table_edited = pyqtSignal()
18+
19+
def __init__(self, lifecycles: list[LifeCycle], parent=None):
20+
super().__init__(parent)
21+
22+
self.lifecycles = lifecycles
23+
24+
# Initialize table widget
25+
self.setColumnCount(3)
26+
self.setHorizontalHeaderLabels(["Elinkaaren tila", "Alkupäivämäärä", "Loppupäivämäärä"])
27+
28+
header = self.horizontalHeader()
29+
header.setSectionResizeMode(0, QHeaderView.Stretch)
30+
self.setMinimumWidth(600)
31+
self.setColumnWidth(1, 120)
32+
self.setColumnWidth(2, 120)
33+
34+
# Add existing lifecycles
35+
for lifecycle in sorted(lifecycles, key=lambda x: x.starting_at): # type: ignore[arg-type, return-value]
36+
self.add_lifecycle_row(lifecycle)
37+
38+
# If 0 rows, initialize 1 row since Plan needs at least one lifecycle
39+
if self.rowCount() == 0:
40+
self.add_new_lifecycle_row()
41+
42+
def add_new_lifecycle_row(self):
43+
self.add_lifecycle_row(LifeCycle())
44+
45+
def add_lifecycle_row(self, lifecycle: LifeCycle):
46+
row_position = self.rowCount()
47+
self.insertRow(row_position)
48+
49+
id_item = QTableWidgetItem()
50+
id_item.setData(Qt.UserRole, lifecycle.id_)
51+
self.setItem(row_position, 0, id_item)
52+
53+
status = CodeComboBox()
54+
status.populate_from_code_layer(LifeCycleStatusLayer)
55+
status.currentIndexChanged.connect(self.table_edited.emit)
56+
if lifecycle.status_id:
57+
status.set_value(lifecycle.status_id)
58+
self.setCellWidget(row_position, 0, status)
59+
60+
start_date_edit = QgsDateTimeEdit()
61+
start_date_edit.setDisplayFormat("yyyy-MM-dd")
62+
start_date_edit.setCalendarPopup(True)
63+
start_date_edit.valueChanged.connect(self.table_edited.emit)
64+
if lifecycle.starting_at:
65+
start_date_edit.setDate(lifecycle.starting_at.date())
66+
self.setCellWidget(row_position, 1, start_date_edit)
67+
68+
end_date_edit = QgsDateTimeEdit()
69+
end_date_edit.setDisplayFormat("yyyy-MM-dd")
70+
end_date_edit.setCalendarPopup(True)
71+
end_date_edit.setSpecialValueText("") # Allow empty value
72+
if lifecycle.ending_at:
73+
end_date_edit.setDate(lifecycle.ending_at.date())
74+
else:
75+
end_date_edit.clear()
76+
self.setCellWidget(row_position, 2, end_date_edit)
77+
78+
self.table_edited.emit()
79+
80+
def is_ok(self) -> bool:
81+
for row in range(self.rowCount()):
82+
status_item = self.cellWidget(row, 0)
83+
start_date_item = self.cellWidget(row, 1)
84+
if status_item.value() is None or start_date_item.date().isNull():
85+
return False
86+
87+
return True
88+
89+
def row_into_model(self, row_i: int) -> LifeCycle:
90+
status_item = self.cellWidget(row_i, 0)
91+
start_date_item = self.cellWidget(row_i, 1)
92+
end_date_item = self.cellWidget(row_i, 2)
93+
94+
id_item = self.item(row_i, 0)
95+
96+
return LifeCycle(
97+
status_id=status_item.value(),
98+
starting_at=start_date_item.date(),
99+
ending_at=end_date_item.date(),
100+
id_=id_item.data(Qt.UserRole),
101+
)
102+
103+
def into_model(self) -> list[LifeCycle]:
104+
"""Extracts all lifecycle data from the table into a list of LifeCycle objects."""
105+
return [self.row_into_model(row) for row in range(self.rowCount())]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
3+
from importlib import resources
4+
from typing import TYPE_CHECKING
5+
6+
from qgis.core import QgsApplication
7+
from qgis.PyQt import uic
8+
from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QPushButton
9+
10+
from arho_feature_template.gui.components.lifecycle_table_widget import LifecycleTableWidget
11+
12+
if TYPE_CHECKING:
13+
from arho_feature_template.core.models import Plan
14+
15+
ui_path = resources.files(__package__) / "lifecycle_editor.ui"
16+
FormClass, _ = uic.loadUiType(ui_path)
17+
18+
19+
class LifecycleEditor(QDialog, FormClass): # type: ignore
20+
add_lifecycle_button: QPushButton
21+
button_box: QDialogButtonBox
22+
23+
def __init__(self, plan: Plan, parent=None):
24+
super().__init__(parent)
25+
self.setupUi(self)
26+
27+
self.plan = plan
28+
if not self.plan:
29+
self.reject()
30+
return
31+
32+
self.lifecycle_table = LifecycleTableWidget(self.plan.lifecycles)
33+
self.layout().insertWidget(1, self.lifecycle_table)
34+
self.add_lifecycle_button.setIcon(QgsApplication.getThemeIcon("mActionAdd.svg"))
35+
36+
self.add_lifecycle_button.clicked.connect(self.lifecycle_table.add_new_lifecycle_row)
37+
self.lifecycle_table.table_edited.connect(self._check_required_fields)
38+
39+
self.button_box.accepted.connect(self._on_ok_clicked)
40+
self.button_box.rejected.connect(self.reject)
41+
self.button_box.button(QDialogButtonBox.Ok).setEnabled(False)
42+
43+
self._check_required_fields()
44+
45+
def _check_required_fields(self) -> None:
46+
"""Enable/disable the OK button based on the validation of the lifecycle table."""
47+
ok_button = self.button_box.button(QDialogButtonBox.Ok)
48+
ok_button.setEnabled(self.lifecycle_table.is_ok())
49+
50+
def _on_ok_clicked(self):
51+
"""Replace existing lifecycles with updated data and save the plan."""
52+
if not self.plan:
53+
return
54+
55+
self.plan.lifecycles = self.lifecycle_table.into_model()
56+
57+
self.accept()
58+
59+
def reject(self):
60+
super().reject()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Dialog</class>
4+
<widget class="QDialog" name="Dialog">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>400</width>
10+
<height>300</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Kaavan elinkaaret</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QPushButton" name="add_lifecycle_button">
19+
<property name="sizePolicy">
20+
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
21+
<horstretch>0</horstretch>
22+
<verstretch>0</verstretch>
23+
</sizepolicy>
24+
</property>
25+
<property name="text">
26+
<string>Lisää elinkaari</string>
27+
</property>
28+
</widget>
29+
</item>
30+
<item>
31+
<widget class="QDialogButtonBox" name="button_box">
32+
<property name="orientation">
33+
<enum>Qt::Horizontal</enum>
34+
</property>
35+
<property name="standardButtons">
36+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
37+
</property>
38+
</widget>
39+
</item>
40+
</layout>
41+
</widget>
42+
<resources/>
43+
<connections>
44+
<connection>
45+
<sender>button_box</sender>
46+
<signal>accepted()</signal>
47+
<receiver>Dialog</receiver>
48+
<slot>accept()</slot>
49+
<hints>
50+
<hint type="sourcelabel">
51+
<x>248</x>
52+
<y>254</y>
53+
</hint>
54+
<hint type="destinationlabel">
55+
<x>157</x>
56+
<y>274</y>
57+
</hint>
58+
</hints>
59+
</connection>
60+
<connection>
61+
<sender>button_box</sender>
62+
<signal>rejected()</signal>
63+
<receiver>Dialog</receiver>
64+
<slot>reject()</slot>
65+
<hints>
66+
<hint type="sourcelabel">
67+
<x>316</x>
68+
<y>260</y>
69+
</hint>
70+
<hint type="destinationlabel">
71+
<x>286</x>
72+
<y>274</y>
73+
</hint>
74+
</hints>
75+
</connection>
76+
</connections>
77+
</ui>

0 commit comments

Comments
 (0)