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

Elinkaarien päivämäärien asetus / tallennus #134

Merged
merged 1 commit into from
Feb 20, 2025
Merged
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
21 changes: 21 additions & 0 deletions arho_feature_template/core/models.py
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
from datetime import datetime

from qgis.core import QgsFeature, QgsGeometry
from qgis.PyQt.QtCore import QDate


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


@dataclass
class LifeCycle:
status_id: str | None = None
id_: str | None = None
plan_id: str | None = None
plan_regulation_id: str | None = None
plan_proposition_id: str | None = None
starting_at: QDate | None = None
ending_at: QDate | None = None
# might not need the following
land_use_are_id: str | None = None
other_area_id: str | None = None
line_id: str | None = None
land_use_point_id: str | None = None
other_point_id: str | None = None


@dataclass
class PlanFeature:
geom: QgsGeometry | None = None # Need to allow None for feature templates
@@ -522,6 +541,7 @@ class PlanFeature:
name: str | None = None
description: str | None = None
regulation_groups: list[RegulationGroup] = field(default_factory=list)
lifecycles: list[LifeCycle] = field(default_factory=list)
plan_id: int | None = None
id_: str | None = None

@@ -537,6 +557,7 @@ class Plan:
description: str | None = None
plan_type_id: str | None = None
lifecycle_status_id: str | None = None
lifecycles: list[LifeCycle] = field(default_factory=list)
record_number: str | None = None
matter_management_identifier: str | None = None
permanent_plan_identifier: str | None = None
39 changes: 37 additions & 2 deletions arho_feature_template/core/plan_manager.py
Original file line number Diff line number Diff line change
@@ -14,13 +14,15 @@
AdditionalInformation,
Document,
FeatureTemplateLibrary,
LifeCycle,
Plan,
PlanFeature,
RegulationGroup,
RegulationGroupCategory,
RegulationGroupLibrary,
)
from arho_feature_template.exceptions import UnsavedChangesError
from arho_feature_template.gui.dialogs.lifecycle_editor import LifecycleEditor
from arho_feature_template.gui.dialogs.load_plan_dialog import LoadPlanDialog
from arho_feature_template.gui.dialogs.plan_attribute_form import PlanAttributeForm
from arho_feature_template.gui.dialogs.plan_feature_form import PlanFeatureForm
@@ -35,6 +37,7 @@
DocumentLayer,
LandUseAreaLayer,
LandUsePointLayer,
LifeCycleLayer,
LineLayer,
OtherAreaLayer,
OtherPointLayer,
@@ -231,7 +234,6 @@ def edit_plan(self):

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

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

def edit_lifecycles(self):
plan_layer = PlanLayer.get_from_project()
if not plan_layer:
return

feature = PlanLayer.get_feature_by_id(get_active_plan_id(), no_geometries=False)
if feature is None:
return
plan_model = PlanLayer.model_from_feature(feature)

lifecycle_editor = LifecycleEditor(plan=plan_model)

if lifecycle_editor.exec_():
save_plan(plan_model)

def add_new_plan_feature(self):
if not handle_unsaved_changes():
return
@@ -304,7 +321,6 @@ def edit_plan_feature(self, feature: QgsFeature, layer_name: str):
layer_class = FEATURE_LAYER_NAME_TO_CLASS_MAP[layer_name]
plan_feature = layer_class.model_from_feature(feature)

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

# Save lifecycles
for lifecycle in plan.lifecycles:
lifecycle.plan_id = plan_id
save_lifecycle(lifecycle)

return feature


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

return feature


def save_lifecycle(lifecycle: LifeCycle) -> QgsFeature:
"""Save a LifeCycle object to the layer."""
feature = LifeCycleLayer.feature_from_model(lifecycle)
layer = LifeCycleLayer.get_from_project()

_save_feature(
feature=feature,
layer=layer,
id_=lifecycle.id_,
edit_text="Elinkaaren lisäys" if lifecycle.id_ is None else "Elinkaaren muokkaus",
)
return feature
105 changes: 105 additions & 0 deletions arho_feature_template/gui/components/lifecycle_table_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations

from qgis.gui import QgsDateTimeEdit
from qgis.PyQt.QtCore import Qt, pyqtSignal
from qgis.PyQt.QtWidgets import (
QHeaderView,
QTableWidget,
QTableWidgetItem,
)

from arho_feature_template.core.models import LifeCycle
from arho_feature_template.gui.components.code_combobox import CodeComboBox
from arho_feature_template.project.layers.code_layers import LifeCycleStatusLayer


class LifecycleTableWidget(QTableWidget):
table_edited = pyqtSignal()

def __init__(self, lifecycles: list[LifeCycle], parent=None):
super().__init__(parent)

self.lifecycles = lifecycles

# Initialize table widget
self.setColumnCount(3)
self.setHorizontalHeaderLabels(["Elinkaaren tila", "Alkupäivämäärä", "Loppupäivämäärä"])

header = self.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
self.setMinimumWidth(600)
self.setColumnWidth(1, 120)
self.setColumnWidth(2, 120)

# Add existing lifecycles
for lifecycle in sorted(lifecycles, key=lambda x: x.starting_at): # type: ignore[arg-type, return-value]
self.add_lifecycle_row(lifecycle)

# If 0 rows, initialize 1 row since Plan needs at least one lifecycle
if self.rowCount() == 0:
self.add_new_lifecycle_row()

def add_new_lifecycle_row(self):
self.add_lifecycle_row(LifeCycle())

def add_lifecycle_row(self, lifecycle: LifeCycle):
row_position = self.rowCount()
self.insertRow(row_position)

id_item = QTableWidgetItem()
id_item.setData(Qt.UserRole, lifecycle.id_)
self.setItem(row_position, 0, id_item)

status = CodeComboBox()
status.populate_from_code_layer(LifeCycleStatusLayer)
status.currentIndexChanged.connect(self.table_edited.emit)
if lifecycle.status_id:
status.set_value(lifecycle.status_id)
self.setCellWidget(row_position, 0, status)

start_date_edit = QgsDateTimeEdit()
start_date_edit.setDisplayFormat("yyyy-MM-dd")
start_date_edit.setCalendarPopup(True)
start_date_edit.valueChanged.connect(self.table_edited.emit)
if lifecycle.starting_at:
start_date_edit.setDate(lifecycle.starting_at.date())
self.setCellWidget(row_position, 1, start_date_edit)

end_date_edit = QgsDateTimeEdit()
end_date_edit.setDisplayFormat("yyyy-MM-dd")
end_date_edit.setCalendarPopup(True)
end_date_edit.setSpecialValueText("") # Allow empty value
if lifecycle.ending_at:
end_date_edit.setDate(lifecycle.ending_at.date())
else:
end_date_edit.clear()
self.setCellWidget(row_position, 2, end_date_edit)

self.table_edited.emit()

def is_ok(self) -> bool:
for row in range(self.rowCount()):
status_item = self.cellWidget(row, 0)
start_date_item = self.cellWidget(row, 1)
if status_item.value() is None or start_date_item.date().isNull():
return False

return True

def row_into_model(self, row_i: int) -> LifeCycle:
status_item = self.cellWidget(row_i, 0)
start_date_item = self.cellWidget(row_i, 1)
end_date_item = self.cellWidget(row_i, 2)

id_item = self.item(row_i, 0)

return LifeCycle(
status_id=status_item.value(),
starting_at=start_date_item.date(),
ending_at=end_date_item.date(),
id_=id_item.data(Qt.UserRole),
)

def into_model(self) -> list[LifeCycle]:
"""Extracts all lifecycle data from the table into a list of LifeCycle objects."""
return [self.row_into_model(row) for row in range(self.rowCount())]
60 changes: 60 additions & 0 deletions arho_feature_template/gui/dialogs/lifecycle_editor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

from importlib import resources
from typing import TYPE_CHECKING

from qgis.core import QgsApplication
from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QPushButton

from arho_feature_template.gui.components.lifecycle_table_widget import LifecycleTableWidget

if TYPE_CHECKING:
from arho_feature_template.core.models import Plan

ui_path = resources.files(__package__) / "lifecycle_editor.ui"
FormClass, _ = uic.loadUiType(ui_path)


class LifecycleEditor(QDialog, FormClass): # type: ignore
add_lifecycle_button: QPushButton
button_box: QDialogButtonBox

def __init__(self, plan: Plan, parent=None):
super().__init__(parent)
self.setupUi(self)

self.plan = plan
if not self.plan:
self.reject()
return

self.lifecycle_table = LifecycleTableWidget(self.plan.lifecycles)
self.layout().insertWidget(1, self.lifecycle_table)
self.add_lifecycle_button.setIcon(QgsApplication.getThemeIcon("mActionAdd.svg"))

self.add_lifecycle_button.clicked.connect(self.lifecycle_table.add_new_lifecycle_row)
self.lifecycle_table.table_edited.connect(self._check_required_fields)

self.button_box.accepted.connect(self._on_ok_clicked)
self.button_box.rejected.connect(self.reject)
self.button_box.button(QDialogButtonBox.Ok).setEnabled(False)

self._check_required_fields()

def _check_required_fields(self) -> None:
"""Enable/disable the OK button based on the validation of the lifecycle table."""
ok_button = self.button_box.button(QDialogButtonBox.Ok)
ok_button.setEnabled(self.lifecycle_table.is_ok())

def _on_ok_clicked(self):
"""Replace existing lifecycles with updated data and save the plan."""
if not self.plan:
return

self.plan.lifecycles = self.lifecycle_table.into_model()

self.accept()

def reject(self):
super().reject()
77 changes: 77 additions & 0 deletions arho_feature_template/gui/dialogs/lifecycle_editor.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Kaavan elinkaaret</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="add_lifecycle_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Lisää elinkaari</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
5 changes: 4 additions & 1 deletion arho_feature_template/gui/dialogs/plan_attribute_form.py
Original file line number Diff line number Diff line change
@@ -9,13 +9,14 @@
QDialog,
QDialogButtonBox,
QLineEdit,
QPushButton,
QTextEdit,
)

from arho_feature_template.core.models import Document, Plan, RegulationGroup, RegulationGroupLibrary
from arho_feature_template.gui.components.general_regulation_group_widget import GeneralRegulationGroupWidget

# from arho_feature_template.gui.components.plan_regulation_group_widget import RegulationGroupWidget
from arho_feature_template.gui.components.general_regulation_group_widget import GeneralRegulationGroupWidget
from arho_feature_template.gui.components.plan_document_widget import DocumentWidget
from arho_feature_template.project.layers.code_layers import (
LifeCycleStatusLayer,
@@ -61,6 +62,7 @@ def __init__(self, plan: Plan, _regulation_group_libraries: list[RegulationGroup
self.setupUi(self)

self.plan = plan
self.lifecycle_models = plan.lifecycles

self.plan_type_combo_box.populate_from_code_layer(PlanTypeLayer)
self.lifecycle_status_combo_box.populate_from_code_layer(LifeCycleStatusLayer)
@@ -98,6 +100,7 @@ def __init__(self, plan: Plan, _regulation_group_libraries: list[RegulationGroup
for regulation_group in plan.general_regulations:
self.add_plan_regulation_group(regulation_group)

# Documents
self.document_widgets: list[DocumentWidget] = []
for document in plan.documents:
self.add_document(document)
8 changes: 4 additions & 4 deletions arho_feature_template/gui/dialogs/plan_attribute_form.ui
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
<property name="windowTitle">
<string>Kaava</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
@@ -26,7 +26,7 @@
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
<number>3</number>
</property>
<widget class="QWidget" name="general_info_tab">
<attribute name="title">
@@ -148,7 +148,7 @@
<item row="5" column="0">
<widget class="QLabel" name="elinkaarenTilaLabel">
<property name="text">
<string>&lt;span style=&quot;color: red;&quot;&gt;*&lt;/span&gt; Elinkaaren tila:</string>
<string>Elinkaaren tila*:</string>
</property>
<property name="buddy">
<cstring>lifecycle_status_combo_box</cstring>
@@ -165,7 +165,7 @@
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&lt;span style=&quot;color: red;&quot;&gt;*&lt;/span&gt; Organisaatio:</string>
<string>Organisaatio*:</string>
</property>
</widget>
</item>
14 changes: 14 additions & 0 deletions arho_feature_template/plugin.py
Original file line number Diff line number Diff line change
@@ -227,6 +227,16 @@ def initGui(self) -> None: # noqa N802
status_tip="Tallenna aktiivinen kaava JSON muodossa",
)

self.edit_lifecycles_action = self.add_action(
text="Kaavan elinkaaret",
icon=QgsApplication.getThemeIcon("mIconFieldDate.svg"),
# icon=QIcon(resources_path("icons", "toolbar", "tallenna_jsonina2.svg")),
triggered_callback=self.edit_lifecycles,
add_to_menu=True,
add_to_toolbar=True,
status_tip="Muokkaa kaavan elinkaaria",
)

self.plugin_settings_action = self.add_action(
text="Asetukset",
triggered_callback=self.open_settings,
@@ -254,6 +264,10 @@ def open_settings(self):
settings = PluginSettings()
settings.exec_()

def edit_lifecycles(self):
"""Edit lifecycles of currently active plan."""
self.plan_manager.edit_lifecycles()

def unload(self) -> None:
"""Removes the plugin menu item and icon from QGIS GUI."""
# Handle signals
60 changes: 59 additions & 1 deletion arho_feature_template/project/layers/plan_layers.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
AdditionalInformationConfigLibrary,
AttributeValue,
Document,
LifeCycle,
Plan,
PlanFeature,
Proposition,
@@ -34,11 +35,13 @@


class AbstractPlanLayer(AbstractLayer):
filter_template: ClassVar[Template]
filter_template: ClassVar[Template | None]

@classmethod
def apply_filter(cls, plan_id: str | None) -> None:
"""Apply a filter to the layer based on the plan_id."""
if cls.filter_template is None:
return
filter_expression = cls.filter_template.substitute(plan_id=plan_id) if plan_id else ""
layer = cls.get_from_project()
if layer.isEditable():
@@ -122,6 +125,11 @@ def model_from_feature(cls, feature: QgsFeature) -> Plan:
for feat in DocumentLayer.get_features_by_attribute_value("plan_id", feature["id"])
],
id_=feature["id"],
lifecycles=[
LifeCycleLayer.model_from_feature(feat)
for feat in LifeCycleLayer.get_features_by_plan_id(feature["id"])
if feat is not None
],
)

@classmethod
@@ -154,6 +162,8 @@ def model_from_feature(cls, feature: QgsFeature) -> PlanFeature:
RegulationGroupLayer.get_feature_by_id(group_id)
for group_id in RegulationGroupAssociationLayer.get_group_ids_for_feature(feature["id"], cls.name)
]
plan_lifecycle_features = [LifeCycleLayer.get_features_by_plan_id(feature["id"])]

return PlanFeature(
geom=feature.geometry(),
type_of_underground_id=feature["type_of_underground_id"],
@@ -163,6 +173,9 @@ def model_from_feature(cls, feature: QgsFeature) -> PlanFeature:
regulation_groups=[
RegulationGroupLayer.model_from_feature(feat) for feat in regulation_group_features if feat is not None
],
lifecycles=[
LifeCycleLayer.model_from_feature(feat) for feat in plan_lifecycle_features if feat is not None
],
plan_id=feature["plan_id"],
id_=feature["id"],
)
@@ -568,6 +581,51 @@ def model_from_feature(cls, feature: QgsFeature) -> AdditionalInformation:
)


class LifeCycleLayer(AbstractPlanLayer):
name = "Elinkaaren päiväykset"
filter_template = None

@classmethod
def feature_from_model(cls, model: LifeCycle) -> QgsFeature:
feature = cls.initialize_feature_from_model(model)

feature["id"] = model.id_ if model.id_ else feature["id"]
feature["lifecycle_status_id"] = model.status_id
feature["starting_at"] = model.starting_at
feature["ending_at"] = model.ending_at if model.ending_at else None
feature["plan_id"] = model.plan_id
feature["land_use_area_id"] = model.land_use_are_id
feature["other_area_id"] = model.other_area_id
feature["line_id"] = model.line_id
feature["land_use_point_id"] = model.land_use_point_id
feature["other_point_id"] = model.other_point_id
feature["plan_regulation_id"] = model.plan_regulation_id
feature["plan_proposition_id"] = model.plan_proposition_id

return feature

@classmethod
def model_from_feature(cls, feature: QgsFeature) -> LifeCycle:
return LifeCycle(
id_=feature["id"],
status_id=feature["lifecycle_status_id"],
starting_at=feature["starting_at"],
ending_at=feature["ending_at"],
plan_id=feature["plan_id"],
land_use_are_id=feature["land_use_area_id"],
other_area_id=feature["other_area_id"],
line_id=feature["line_id"],
land_use_point_id=feature["land_use_point_id"],
other_point_id=feature["other_point_id"],
plan_regulation_id=feature["plan_regulation_id"],
plan_proposition_id=feature["plan_proposition_id"],
)

@classmethod
def get_features_by_plan_id(cls, plan_id: str) -> list[QgsFeature]:
return list(cls.get_features_by_attribute_value("plan_id", plan_id))


plan_layers = AbstractPlanLayer.__subclasses__()
plan_layers.remove(PlanFeatureLayer)