diff --git a/arho_feature_template/core/models.py b/arho_feature_template/core/models.py index 7acb172..e8b24c9 100644 --- a/arho_feature_template/core/models.py +++ b/arho_feature_template/core/models.py @@ -562,6 +562,9 @@ def from_config_data(cls, data: dict) -> PlanFeature: # TODO: Implement return cls(**data) + def __str__(self): + return self.name if self.name else "" + @dataclass class Plan: diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 59f31a2..4b5a5cb 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -7,7 +7,7 @@ from qgis.core import QgsProject, QgsVectorLayer, QgsWkbTypes from qgis.gui import QgsMapToolDigitizeFeature -from qgis.PyQt.QtWidgets import QDialog, QMessageBox +from qgis.PyQt.QtWidgets import QDialog from arho_feature_template.core.lambda_service import LambdaService from arho_feature_template.core.models import ( @@ -60,6 +60,7 @@ handle_unsaved_changes, iface, set_active_plan_id, + use_wait_cursor, ) if TYPE_CHECKING: @@ -169,15 +170,20 @@ def _open_regulation_group_form(self, regulation_group: RegulationGroup): regulation_group_form = PlanRegulationGroupForm(regulation_group, self.active_plan_regulation_group_library) if regulation_group_form.exec_(): + model = regulation_group_form.model if regulation_group_form.save_as_config: - save_regulation_group_as_config(regulation_group_form.model) - else: - save_regulation_group(regulation_group_form.model) + save_regulation_group_as_config(model) + elif save_regulation_group(model) is None: + return None + # NOTE: Should we reinitialize regulation group dock even if saving failed? self.update_active_plan_regulation_group_library() + return model + + return None def delete_regulation_group(self, group: RegulationGroup): - delete_regulation_group(group) - self.update_active_plan_regulation_group_library() + if delete_regulation_group(group): + self.update_active_plan_regulation_group_library() def toggle_identify_plan_features(self, activate: bool): # noqa: FBT001 if activate: @@ -238,13 +244,15 @@ def edit_plan(self): feature = PlanLayer.get_feature_by_id(get_active_plan_id(), no_geometries=False) if feature is None: + iface.messageBar().pushWarning("", "Mikään kaava ei ole avattuna.") return plan_model = PlanLayer.model_from_feature(feature) attribute_form = PlanAttributeForm(plan_model, self.regulation_group_libraries) if attribute_form.exec_(): feature = save_plan(attribute_form.model) - self.update_active_plan_regulation_group_library() + if feature: + self.update_active_plan_regulation_group_library() def edit_lifecycles(self): plan_layer = PlanLayer.get_from_project() @@ -290,8 +298,8 @@ def _plan_geom_digitized(self, feature: QgsFeature): plan_model = Plan(geom=feature.geometry()) attribute_form = PlanAttributeForm(plan_model, self.regulation_group_libraries) if attribute_form.exec_(): - feature = save_plan(attribute_form.model) - plan_to_be_activated = feature["id"] + feat = save_plan(attribute_form.model) + plan_to_be_activated = feat["id"] if feat else self.previous_active_plan_id else: plan_to_be_activated = self.previous_active_plan_id @@ -317,8 +325,7 @@ def _plan_feature_geom_digitized(self, feature: QgsFeature): attribute_form = PlanFeatureForm( plan_feature, title, self.regulation_group_libraries, self.active_plan_regulation_group_library ) - if attribute_form.exec_(): - save_plan_feature(attribute_form.model) + if attribute_form.exec_() and save_plan_feature(attribute_form.model): self.update_active_plan_regulation_group_library() def edit_plan_feature(self, feature: QgsFeature, layer_name: str): @@ -329,8 +336,7 @@ def edit_plan_feature(self, feature: QgsFeature, layer_name: str): attribute_form = PlanFeatureForm( plan_feature, title, self.regulation_group_libraries, self.active_plan_regulation_group_library ) - if attribute_form.exec_(): - save_plan_feature(attribute_form.model) + if attribute_form.exec_() and save_plan_feature(attribute_form.model): self.update_active_plan_regulation_group_library() def set_active_plan(self, plan_id: str | None): @@ -363,7 +369,7 @@ def load_land_use_plan(self): connection_names = get_existing_database_connection_names() if not connection_names: - QMessageBox.critical(None, "Error", "No database connections found.") + iface.messageBar().pushCritical("", "Tietokantayhteyksiä ei löytynyt.") return if not handle_unsaved_changes(): @@ -375,10 +381,6 @@ def load_land_use_plan(self): selected_plan_id = dialog.get_selected_plan_id() self.commit_all_editable_layers() - if not selected_plan_id: - QMessageBox.critical(None, "Error", "No plan was selected.") - return - self.set_active_plan(selected_plan_id) def commit_all_editable_layers(self): @@ -389,27 +391,27 @@ def commit_all_editable_layers(self): def get_plan_json(self): """Serializes plan and plan outline to JSON""" + plan_id = get_active_plan_id() + if not plan_id: + iface.messageBar().pushWarning("", "Mikään kaava ei ole avattuna.") + return + dialog = SerializePlan() if dialog.exec_() == QDialog.Accepted: 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: - QMessageBox.critical(None, "Virhe", "Ei aktiivista kaavaa.") - return - 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.""" if plan_json is None or outline_json is None: - QMessageBox.critical(None, "Virhe", "Kaava tai sen ulkoraja ei löytynyt.") + iface.messageBar().pushCritical("", "Kaavaa tai sen ulkorajaa ei löytynyt.") return # Retrieve paths if self.json_plan_path is None or self.json_plan_outline_path is None: - QMessageBox.critical(None, "Virhe", "Tiedostopolut eivät ole saatavilla.") + iface.messageBar().pushCritical("", "Tiedostopolut eivät ole saatavilla.") return # Save the JSONs @@ -419,11 +421,7 @@ def save_plan_jsons(self, plan_json, outline_json): with open(self.json_plan_outline_path, "w", encoding="utf-8") as outline_file: json.dump(outline_json, outline_file, ensure_ascii=False, indent=2) - QMessageBox.information( - None, - "Tallennus onnistui", - "Kaava ja sen ulkoraja tallennettu onnistuneesti.", - ) + iface.messageBar().pushSuccess("", "Kaava ja kaavan ulkoraja tallennettu.") def unload(self): # Set pan map tool as active (to deactivate our custom tools to avoid errors) @@ -494,7 +492,7 @@ def regulation_group_library_from_active_plan() -> RegulationGroupLibrary: ) -def _save_feature(feature: QgsFeature, layer: QgsVectorLayer, id_: str | None, edit_text: str = ""): +def _save_feature(feature: QgsFeature, layer: QgsVectorLayer, id_: str | None, edit_text: str = "") -> bool: if not layer.isEditable(): layer.startEditing() layer.beginEditCommand(edit_text) @@ -505,10 +503,10 @@ def _save_feature(feature: QgsFeature, layer: QgsVectorLayer, id_: str | None, e layer.updateFeature(feature) layer.endEditCommand() - layer.commitChanges(stopEditing=False) + return layer.commitChanges(stopEditing=False) -def _delete_feature(feature: QgsFeature, layer: QgsVectorLayer, delete_text: str = ""): +def _delete_feature(feature: QgsFeature, layer: QgsVectorLayer, delete_text: str = "") -> bool: if not layer.isEditable(): layer.startEditing() layer.beginEditCommand(delete_text) @@ -516,19 +514,22 @@ def _delete_feature(feature: QgsFeature, layer: QgsVectorLayer, delete_text: str layer.deleteFeature(feature.id()) layer.endEditCommand() - layer.commitChanges(stopEditing=False) + return layer.commitChanges(stopEditing=False) -def save_plan(plan: Plan) -> QgsFeature: +@use_wait_cursor +def save_plan(plan: Plan) -> QgsFeature | None: feature = PlanLayer.feature_from_model(plan) layer = PlanLayer.get_from_project() - _save_feature( + if not _save_feature( feature=feature, layer=layer, id_=plan.id_, edit_text="Kaavan muokkaus" if plan.id_ is not None else "Kaavan luominen", - ) + ): + iface.messageBar().pushCritical("", "Kaavan tallentaminen epäonnistui") + return None plan_id = feature["id"] if plan.id_ is not None: @@ -536,21 +537,25 @@ def save_plan(plan: Plan) -> QgsFeature: for association in RegulationGroupAssociationLayer.get_dangling_associations( plan.general_regulations, plan.id_, PlanLayer.name ): - _delete_feature( + if not _delete_feature( association, RegulationGroupAssociationLayer.get_from_project(), "Kaavamääräysryhmän assosiaation poisto", - ) + ): + iface.messageBar().pushCritical("", "Kaavamääräysryhmän assosiaation poistaminen epäonnistui.") # Check for documents to be deleted doc_layer = DocumentLayer.get_from_project() for doc_feature in DocumentLayer.get_documents_to_delete(plan.documents, plan.id_): - _delete_feature(doc_feature, doc_layer, "Asiakirjan poisto") + if not _delete_feature(doc_feature, doc_layer, "Asiakirjan poisto"): + iface.messageBar().pushCritical("", "Asiakirjan poistaminen epäonnistui.") # Save general regulations if plan.general_regulations: for regulation_group in plan.general_regulations: regulation_group_feature = save_regulation_group(regulation_group, plan_id) + if regulation_group_feature is None: + continue # Skip association saving if saving regulation group failed save_regulation_group_association(regulation_group_feature["id"], PlanLayer.name, plan_id) # Save documents @@ -566,7 +571,8 @@ def save_plan(plan: Plan) -> QgsFeature: return feature -def save_plan_feature(plan_model: PlanFeature, plan_id: str | None = None) -> QgsFeature: +@use_wait_cursor +def save_plan_feature(plan_model: PlanFeature, plan_id: str | None = None) -> QgsFeature | None: layer_name = plan_model.layer_name if not layer_name: msg = "Cannot save plan feature without a target layer" @@ -580,42 +586,50 @@ def save_plan_feature(plan_model: PlanFeature, plan_id: str | None = None) -> Qg layer = layer_class.get_from_project() editing = plan_model.id_ is not None - _save_feature( + if not _save_feature( feature=plan_feature, layer=layer, id_=plan_model.id_, edit_text="Kaavakohteen muokkaus" if editing else "Kaavakohteen lisäys", - ) + ): + iface.messageBar().pushCritical("", "Kaavakohteen tallentaminen epäonnistui.") + return None # Check for deleted regulation groups if editing: for association in RegulationGroupAssociationLayer.get_dangling_associations( plan_model.regulation_groups, plan_feature["id"], layer_name ): - _delete_feature( + if not _delete_feature( association, RegulationGroupAssociationLayer.get_from_project(), "Kaavamääräysryhmän assosiaation poisto", - ) + ): + iface.messageBar().pushCritical("", "Kaavamääräysryhmän assosiaation poistaminen epäonnistui.") # Save regulation groups for group in plan_model.regulation_groups: regulation_group_feature = save_regulation_group(group) + if regulation_group_feature is None: + continue # Skip association saving if saving regulation group failed save_regulation_group_association(regulation_group_feature["id"], layer_name, plan_feature["id"]) return plan_feature -def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None = None) -> QgsFeature: +@use_wait_cursor +def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None = None) -> QgsFeature | None: feature = RegulationGroupLayer.feature_from_model(regulation_group, plan_id) layer = RegulationGroupLayer.get_from_project() - _save_feature( + if not _save_feature( feature=feature, layer=layer, id_=regulation_group.id_, edit_text="Kaavamääräysryhmän muokkaus" if regulation_group.id_ is not None else "Kaavamääräysryhmän lisäys", - ) + ): + iface.messageBar().pushCritical("", "Kaavamääräysryhmän tallentaminen epäonnistui.") + return None if regulation_group.id_ is not None: # Check for regulations to be deleted @@ -623,14 +637,16 @@ def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None for reg_feature in PlanRegulationLayer.get_regulations_to_delete( regulation_group.regulations, regulation_group.id_ ): - _delete_feature(reg_feature, regulation_layer, "Kaavamääräyksen poisto") + if not _delete_feature(reg_feature, regulation_layer, "Kaavamääräyksen poisto"): + iface.messageBar().pushCritical("", "Kaavamääräyksen poistaminen epäonnistui.") # Check for propositions to be deleted proposition_layer = PlanPropositionLayer.get_from_project() for prop_feature in PlanPropositionLayer.get_propositions_to_delete( regulation_group.propositions, regulation_group.id_ ): - _delete_feature(prop_feature, proposition_layer, "Kaavasuosituksen poisto") + if not _delete_feature(prop_feature, proposition_layer, "Kaavasuosituksen poisto"): + iface.messageBar().pushCritical("", "Kaavasuosituksen poistaminen epäonnistui.") # Save regulations if regulation_group.regulations: @@ -647,39 +663,51 @@ def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None return feature -def delete_regulation_group(regulation_group: RegulationGroup, plan_id: str | None = None): +@use_wait_cursor +def delete_regulation_group(regulation_group: RegulationGroup, plan_id: str | None = None) -> bool: if regulation_group.id_ is None: - return + iface.messageBar().pushCritical("", "Kaavamääräysryhmän poistaminen epäonnistui (ei IDtä).") + return False feature = RegulationGroupLayer.feature_from_model(regulation_group, plan_id) layer = RegulationGroupLayer.get_from_project() - _delete_feature(feature, layer, "Kaavamääräysryhmän poisto") + if not _delete_feature(feature, layer, "Kaavamääräysryhmän poisto"): + iface.messageBar().pushCritical("", "Kaavamääräysryhmän poistaminen epäonnistui.") + return False + + return True def save_regulation_group_as_config(regulation_group: RegulationGroup): pass -def save_regulation_group_association(regulation_group_id: str, layer_name: str, feature_id: str): +def save_regulation_group_association(regulation_group_id: str, layer_name: str, feature_id: str) -> bool: if RegulationGroupAssociationLayer.association_exists(regulation_group_id, layer_name, feature_id): - return + return True feature = RegulationGroupAssociationLayer.feature_from(regulation_group_id, layer_name, feature_id) layer = RegulationGroupAssociationLayer.get_from_project() - _save_feature(feature=feature, layer=layer, id_=None, edit_text="Kaavamääräysryhmän assosiaation lisäys") + if not _save_feature(feature=feature, layer=layer, id_=None, edit_text="Kaavamääräysryhmän assosiaation lisäys"): + iface.messageBar().pushCritical("", "Kaavamääräysryhmän assosiaation tallentaminen epäonnistui.") + return False + return True -def save_regulation(regulation: Regulation) -> QgsFeature: + +def save_regulation(regulation: Regulation) -> QgsFeature | None: regulation_feature = PlanRegulationLayer.feature_from_model(regulation) layer = PlanRegulationLayer.get_from_project() - _save_feature( + if not _save_feature( feature=regulation_feature, layer=layer, id_=regulation.id_, edit_text="Kaavamääräyksen lisäys" if regulation.id_ is None else "Kaavamääräyksen muokkaus", - ) + ): + iface.messageBar().pushCritical("", "Kaavamääräyksen tallentaminen epäonnistui.") + return None for additional_information in regulation.additional_information: additional_information.plan_regulation_id = regulation_feature["id"] @@ -688,77 +716,98 @@ def save_regulation(regulation: Regulation) -> QgsFeature: return regulation_feature -def save_additional_information(additional_information: AdditionalInformation) -> QgsFeature: +def save_additional_information(additional_information: AdditionalInformation) -> QgsFeature | None: feature = AdditionalInformationLayer.feature_from_model(additional_information) layer = AdditionalInformationLayer.get_from_project() - _save_feature( + if not _save_feature( feature=feature, layer=layer, id_=additional_information.id_, edit_text="Lisätiedon lisäys" if additional_information.id_ is None else "Lisätiedon muokkaus", - ) + ): + iface.messageBar().pushCritical("", "Lisätiedon tallentaminen epäonnistui.") + return None + return feature -def delete_additional_information(additional_information: AdditionalInformation): +def delete_additional_information(additional_information: AdditionalInformation) -> bool: feature = AdditionalInformationLayer.feature_from_model(additional_information) layer = AdditionalInformationLayer.get_from_project() - _delete_feature(feature, layer, "Lisätiedon poisto") + if not _delete_feature(feature, layer, "Lisätiedon poisto"): + iface.messageBar().pushCritical("", "Lisätiedon poistaminen epäonnistui.") + return False + return True -def delete_regulation(regulation: Regulation): + +def delete_regulation(regulation: Regulation) -> bool: feature = PlanRegulationLayer.feature_from_model(regulation) layer = PlanRegulationLayer.get_from_project() - _delete_feature(feature, layer, "Kaavamääräyksen poisto") + if not _delete_feature(feature, layer, "Kaavamääräyksen poisto"): + iface.messageBar().pushCritical("", "Lisätiedon poistaminen epäonnistui.") + return False + + return True -def save_proposition(proposition: Proposition) -> QgsFeature: +def save_proposition(proposition: Proposition) -> QgsFeature | None: feature = PlanPropositionLayer.feature_from_model(proposition) layer = PlanPropositionLayer.get_from_project() - _save_feature( + if not _save_feature( feature=feature, layer=layer, id_=proposition.id_, edit_text="Kaavasuosituksen lisäys" if proposition.id_ is None else "Kaavasuosituksen muokkaus", - ) + ): + iface.messageBar().pushCritical("", "Kaavasuosituksen tallentaminen epäonnistui.") + return None return feature -def delete_proposition(proposition: Proposition): +def delete_proposition(proposition: Proposition) -> bool: feature = PlanPropositionLayer.feature_from_model(proposition) layer = PlanPropositionLayer.get_from_project() - _delete_feature(feature, layer, "Kaavasuosituksen poisto") + if not _delete_feature(feature, layer, "Kaavasuosituksen poisto"): + iface.messageBar().pushCritical("", "Kaavasuosituksen poistaminen epäonnistui.") + return False + + return True -def save_document(document: Document) -> QgsFeature: +def save_document(document: Document) -> QgsFeature | None: feature = DocumentLayer.feature_from_model(document) layer = DocumentLayer.get_from_project() - _save_feature( + if not _save_feature( feature=feature, layer=layer, id_=document.id_, edit_text="Asiakirjan lisäys" if document.id_ is None else "Asiakirjan muokkaus", - ) + ): + iface.messageBar().pushCritical("", "Asiakirjan tallentaminen epäonnistui.") + return None return feature -def save_lifecycle(lifecycle: LifeCycle) -> QgsFeature: - """Save a LifeCycle object to the layer.""" +def save_lifecycle(lifecycle: LifeCycle) -> QgsFeature | None: feature = LifeCycleLayer.feature_from_model(lifecycle) layer = LifeCycleLayer.get_from_project() - _save_feature( + if not _save_feature( feature=feature, layer=layer, id_=lifecycle.id_, edit_text="Elinkaaren lisäys" if lifecycle.id_ is None else "Elinkaaren muokkaus", - ) + ): + iface.messageBar().pushCritical("", "Elinkaaren tallentaminen epäonnistui.") + return None + return feature diff --git a/arho_feature_template/gui/dialogs/load_plan_dialog.py b/arho_feature_template/gui/dialogs/load_plan_dialog.py index cccb279..e5aec46 100644 --- a/arho_feature_template/gui/dialogs/load_plan_dialog.py +++ b/arho_feature_template/gui/dialogs/load_plan_dialog.py @@ -64,6 +64,7 @@ def __init__(self, parent, connection_names: list[str]): self.setupUi(self) self._selected_plan_id = None + self._selected_plan_name = None self.button_box.rejected.connect(self.reject) self.button_box.accepted.connect(self.accept) @@ -193,9 +194,11 @@ def on_selection_changed(self): if selection: selected_row = selection[0].row() self._selected_plan_id = self.plan_table_view.model().index(selected_row, 0).data(Qt.UserRole) + self._selected_plan_name = self.plan_table_view.model().index(selected_row, 0) self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) else: self._selected_plan_id = None + self._selected_plan_name = None self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) def get_selected_connection(self): @@ -203,3 +206,6 @@ def get_selected_connection(self): def get_selected_plan_id(self): return self._selected_plan_id + + def get_selected_plan_name(self): + return self._selected_plan_name diff --git a/arho_feature_template/utils/misc_utils.py b/arho_feature_template/utils/misc_utils.py index bb4f577..6f82e1f 100644 --- a/arho_feature_template/utils/misc_utils.py +++ b/arho_feature_template/utils/misc_utils.py @@ -2,12 +2,13 @@ import os from contextlib import suppress +from functools import wraps from typing import TYPE_CHECKING, Any, cast from qgis.core import QgsExpressionContextUtils, QgsProject, QgsVectorLayer -from qgis.PyQt.QtCore import QSettings, pyqtBoundSignal +from qgis.PyQt.QtCore import QSettings, Qt, pyqtBoundSignal from qgis.PyQt.QtWidgets import QMessageBox -from qgis.utils import iface +from qgis.utils import OverrideCursor, iface if TYPE_CHECKING: from qgis.core import QgsMapLayer @@ -128,3 +129,12 @@ def deserialize_localized_text(text_value: dict[str, str] | None | Any) -> str | if isinstance(text_value, dict): text = text_value.get(LANGUAGE) return text + + +def use_wait_cursor(func): + @wraps(func) + def wrapper(*args, **kwargs): + with OverrideCursor(Qt.WaitCursor): + return func(*args, **kwargs) + + return wrapper