From bab19dd3209aef64ef16f226b13355e6a86dc608 Mon Sep 17 00:00:00 2001 From: Damiano Lombardi Date: Sat, 10 Feb 2024 17:26:19 +0100 Subject: [PATCH 1/5] Fix multiedit features deletion --- linking_relation_editor/gui/linking_relation_editor_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linking_relation_editor/gui/linking_relation_editor_widget.py b/linking_relation_editor/gui/linking_relation_editor_widget.py index d57a38a..1898f28 100644 --- a/linking_relation_editor/gui/linking_relation_editor_widget.py +++ b/linking_relation_editor/gui/linking_relation_editor_widget.py @@ -553,7 +553,7 @@ def addFeatureGeometry(self): self.editorContext().mainMessageBar().pushItem(self.mMessageBarItem) def deleteSelectedFeatures(self): - self.deleteFeatures(self.selectedChildFeatureIds()) + self.deleteFeatures(list(self.selectedChildFeatureIds())) def duplicateFeatures(self, fids): layer = self.relation().referencingLayer() From 502f5f6aa9eef23c9860ef0dd89125d3b2b3ea74 Mon Sep 17 00:00:00 2001 From: Damiano Lombardi Date: Sat, 10 Feb 2024 17:44:06 +0100 Subject: [PATCH 2/5] Multiedit fix selectin of simoultaneously added features --- .../gui/linking_relation_editor_widget.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/linking_relation_editor/gui/linking_relation_editor_widget.py b/linking_relation_editor/gui/linking_relation_editor_widget.py index 1898f28..d6c1990 100644 --- a/linking_relation_editor/gui/linking_relation_editor_widget.py +++ b/linking_relation_editor/gui/linking_relation_editor_widget.py @@ -689,35 +689,35 @@ def multiEditItemSelectionChanged(self): if len(selectedItems) == 1 and len(self.mMultiEditPreviousSelectedItems) <= 1: selectedItem = selectedItems[0] if selectedItem.data(0, self.MultiEditTreeWidgetRole.FeatureType) == self.MultiEditFeatureType.Child: - self.mMultiEditTreeWidget.blockSignals(True) - featureIdSelectedItem = selectedItem.data(0, self.MultiEditTreeWidgetRole.FeatureId) - for indexTopLevelItem in range(self.mMultiEditTreeWidget.topLevelItemCount()): - treeWidgetTopLevelItem = self.mMultiEditTreeWidget.topLevelItem(indexTopLevelItem) - - for indexItem in range(treeWidgetTopLevelItem.childCount()): - treeWidgetItem = treeWidgetTopLevelItem.child(indexItem) - - if ( - treeWidgetItem.data(0, self.MultiEditTreeWidgetRole.FeatureType) - != self.MultiEditFeatureType.Child - ): - QgsLogger.warning(self.tr("Not a child item")) - continue - - featureIdCurrentItem = treeWidgetItem.data(0, self.MultiEditTreeWidgetRole.FeatureId) - if self.nmRelation().isValid(): - if featureIdSelectedItem == featureIdCurrentItem: - treeWidgetItem.setSelected(True) - else: - if featureIdSelectedItem not in self.mMultiEdit1NJustAddedIds: - break + if featureIdSelectedItem in self.mMultiEdit1NJustAddedIds: + self.mMultiEditTreeWidget.blockSignals(True) + for indexTopLevelItem in range(self.mMultiEditTreeWidget.topLevelItemCount()): + treeWidgetTopLevelItem = self.mMultiEditTreeWidget.topLevelItem(indexTopLevelItem) - if featureIdCurrentItem not in self.mMultiEdit1NJustAddedIds: - treeWidgetItem.setSelected(True) - - self.mMultiEditTreeWidget.blockSignals(False) + for indexItem in range(treeWidgetTopLevelItem.childCount()): + treeWidgetItem = treeWidgetTopLevelItem.child(indexItem) + + if ( + treeWidgetItem.data(0, self.MultiEditTreeWidgetRole.FeatureType) + != self.MultiEditFeatureType.Child + ): + QgsLogger.warning(self.tr("Not a child item")) + continue + + featureIdCurrentItem = treeWidgetItem.data(0, self.MultiEditTreeWidgetRole.FeatureId) + if self.nmRelation().isValid(): + if featureIdSelectedItem == featureIdCurrentItem: + treeWidgetItem.setSelected(True) + else: + if featureIdSelectedItem not in self.mMultiEdit1NJustAddedIds: + break + + if featureIdCurrentItem in self.mMultiEdit1NJustAddedIds: + treeWidgetItem.setSelected(True) + + self.mMultiEditTreeWidget.blockSignals(False) self.mMultiEditPreviousSelectedItems = selectedItems self.updateButtons() From 3741aecaceb4a419d8e55624cfaa791c9956dc2f Mon Sep 17 00:00:00 2001 From: Damiano Lombardi Date: Sat, 10 Feb 2024 18:23:02 +0100 Subject: [PATCH 3/5] Multiedit fix N:M listing of childrens --- .../gui/linking_relation_editor_widget.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/linking_relation_editor/gui/linking_relation_editor_widget.py b/linking_relation_editor/gui/linking_relation_editor_widget.py index d6c1990..e6d1d7a 100644 --- a/linking_relation_editor/gui/linking_relation_editor_widget.py +++ b/linking_relation_editor/gui/linking_relation_editor_widget.py @@ -10,6 +10,7 @@ import os from enum import IntEnum +import copy from qgis.core import ( Qgis, @@ -426,10 +427,10 @@ def updateUiMultiEdit(self): treeWidgetItem.addChild(treeWidgetItemChild) featureIdsMixedValues.add(featureChildChild.id()) - if treeWidgetItem in multimapChildFeatures: - multimapChildFeatures[treeWidgetItem].append(featureChildChild.id()) + if id(treeWidgetItem) in multimapChildFeatures: + multimapChildFeatures[id(treeWidgetItem)].append(featureChildChild.id()) else: - multimapChildFeatures[treeWidgetItem] = [featureChildChild.id()] + multimapChildFeatures[id(treeWidgetItem)] = [featureChildChild.id()] else: treeWidgetItemChild = self.createMultiEditTreeWidgetItem( @@ -453,10 +454,10 @@ def updateUiMultiEdit(self): # See https://github.com/qgis/QGIS/pull/45703 # if self.nmRelation().isValid(): - for featureIdMixedValue in featureIdsMixedValues[:]: + for featureIdMixedValue in copy.copy(featureIdsMixedValues): mixedValues = True for parentTreeWidgetItem in parentTreeWidgetItems: - if featureIdMixedValue in multimapChildFeatures[parentTreeWidgetItem]: + if featureIdMixedValue in multimapChildFeatures[id(parentTreeWidgetItem)]: mixedValues = True break From fe58a0805c76bc6472e201fb483118183c4abb65 Mon Sep 17 00:00:00 2001 From: Damiano Lombardi Date: Sat, 10 Feb 2024 18:26:11 +0100 Subject: [PATCH 4/5] Multieditor fix unlinking --- linking_relation_editor/gui/linking_relation_editor_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linking_relation_editor/gui/linking_relation_editor_widget.py b/linking_relation_editor/gui/linking_relation_editor_widget.py index e6d1d7a..ab37b2f 100644 --- a/linking_relation_editor/gui/linking_relation_editor_widget.py +++ b/linking_relation_editor/gui/linking_relation_editor_widget.py @@ -758,7 +758,7 @@ def showContextMenu(self, menu, fid): qAction.triggered.connect(lambda state, fid=fid: self.unlinkFeature(fid)) def unlinkSelectedFeatures(self): - self.unlinkFeatures(self.selectedChildFeatureIds()) + self.unlinkFeatures(list(self.selectedChildFeatureIds())) def zoomToSelectedFeatures(self): if self.editorContext().mapCanvas(): From f27075c2a08c53fa77bf6ff6bed3026ade333d06 Mon Sep 17 00:00:00 2001 From: Damiano Lombardi Date: Mon, 4 Mar 2024 15:08:56 +0100 Subject: [PATCH 5/5] Add tests --- .pre-commit-config.yaml | 10 +- .../gui/linking_relation_editor_widget.py | 2 +- .../tests/test_relation_editor_multiedit.py | 242 ++++++++++++++++++ 3 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 linking_relation_editor/tests/test_relation_editor_multiedit.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e5ab92..e61e259 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Fix end of files - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -11,7 +11,7 @@ repos: # Remove unused imports/variables - repo: https://github.com/myint/autoflake - rev: v2.0.1 + rev: v2.3.0 hooks: - id: autoflake args: @@ -22,7 +22,7 @@ repos: # Sort imports - repo: https://github.com/pycqa/isort - rev: "5.12.0" + rev: "5.13.2" hooks: - id: isort args: @@ -31,7 +31,7 @@ repos: # Black formatting - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 24.2.0 hooks: - id: black language_version: python3 @@ -40,7 +40,7 @@ repos: # Flake8 linter - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 args: diff --git a/linking_relation_editor/gui/linking_relation_editor_widget.py b/linking_relation_editor/gui/linking_relation_editor_widget.py index ab37b2f..b5db2f6 100644 --- a/linking_relation_editor/gui/linking_relation_editor_widget.py +++ b/linking_relation_editor/gui/linking_relation_editor_widget.py @@ -454,7 +454,7 @@ def updateUiMultiEdit(self): # See https://github.com/qgis/QGIS/pull/45703 # if self.nmRelation().isValid(): - for featureIdMixedValue in copy.copy(featureIdsMixedValues): + for featureIdMixedValue in set(featureIdsMixedValues): mixedValues = True for parentTreeWidgetItem in parentTreeWidgetItems: if featureIdMixedValue in multimapChildFeatures[id(parentTreeWidgetItem)]: diff --git a/linking_relation_editor/tests/test_relation_editor_multiedit.py b/linking_relation_editor/tests/test_relation_editor_multiedit.py new file mode 100644 index 0000000..0a4c5e5 --- /dev/null +++ b/linking_relation_editor/tests/test_relation_editor_multiedit.py @@ -0,0 +1,242 @@ +from qgis.core import ( + QgsApplication, + QgsFeature, + QgsProject, + QgsRelation, + QgsVectorLayer, +) +from qgis.gui import QgsGui +from qgis.PyQt.QtWidgets import QWidget +from qgis.testing import start_app, unittest + +from linking_relation_editor.gui.linking_relation_editor_widget_factory import ( + LinkingRelationEditorWidget, +) + +start_app() + + +class TestRelationEditorMultiedit(unittest.TestCase): + + def setUp(self): + pass + # QgsApplication.init() + QgsApplication.initQgis() + QgsGui.editorWidgetRegistry().initEditors() + + def tearDown(self): + return + QgsApplication.exitQgis() + + @classmethod + def setUpClass(cls): + + cls.mLayer1 = None + cls.mLayer2 = None + cls.mLayerJoin = None + cls.mRelation = None + cls.mRelation1N = None + cls.mRelationNM = None + + # create layer + cls.mLayer1 = QgsVectorLayer("LineString?field=pk:int&field=fk:int", "vl1", "memory") + cls.mLayer1.setDisplayExpression("'Layer1-' || pk") + QgsProject.instance().addMapLayer(cls.mLayer1, False) + + cls.mLayer2 = QgsVectorLayer("LineString?field=pk:int", "vl2", "memory") + cls.mLayer2.setDisplayExpression("'Layer2-' || pk") + QgsProject.instance().addMapLayer(cls.mLayer2, False) + + cls.mLayerJoin = QgsVectorLayer( + "LineString?field=pk:int&field=fk_layer1:int&field=fk_layer2:int", "join_layer", "memory" + ) + cls.mLayerJoin.setDisplayExpression("'LayerJoin-' || pk") + QgsProject.instance().addMapLayer(cls.mLayerJoin, False) + + # create relation + cls.mRelation = QgsRelation() + cls.mRelation.setId("vl1.vl2") + cls.mRelation.setName("vl1.vl2") + cls.mRelation.setReferencingLayer(cls.mLayer1.id()) + cls.mRelation.setReferencedLayer(cls.mLayer2.id()) + cls.mRelation.addFieldPair("fk", "pk") + assert cls.mRelation.isValid() + QgsProject.instance().relationManager().addRelation(cls.mRelation) + + # create nm relations + cls.mRelation1N = QgsRelation() + cls.mRelation1N.setId("join_layer.vl1") + cls.mRelation1N.setName("join_layer.vl1") + cls.mRelation1N.setReferencingLayer(cls.mLayerJoin.id()) + cls.mRelation1N.setReferencedLayer(cls.mLayer1.id()) + cls.mRelation1N.addFieldPair("fk_layer1", "pk") + assert cls.mRelation1N.isValid() + QgsProject.instance().relationManager().addRelation(cls.mRelation1N) + + cls.mRelationNM = QgsRelation() + cls.mRelationNM.setId("join_layer.vl2") + cls.mRelationNM.setName("join_layer.vl2") + cls.mRelationNM.setReferencingLayer(cls.mLayerJoin.id()) + cls.mRelationNM.setReferencedLayer(cls.mLayer2.id()) + cls.mRelationNM.addFieldPair("fk_layer2", "pk") + assert cls.mRelationNM.isValid() + QgsProject.instance().relationManager().addRelation(cls.mRelationNM) + + # add features + ft0 = QgsFeature(cls.mLayer1.fields()) + ft0.setAttribute("pk", 0) + ft0.setAttribute("fk", 10) + cls.mLayer1.startEditing() + cls.mLayer1.addFeature(ft0) + cls.mLayer1.commitChanges() + + ft1 = QgsFeature(cls.mLayer1.fields()) + ft1.setAttribute("pk", 1) + ft1.setAttribute("fk", 11) + cls.mLayer1.startEditing() + cls.mLayer1.addFeature(ft1) + cls.mLayer1.commitChanges() + + ft2 = QgsFeature(cls.mLayer2.fields()) + ft2.setAttribute("pk", 10) + cls.mLayer2.startEditing() + cls.mLayer2.addFeature(ft2) + cls.mLayer2.commitChanges() + + ft3 = QgsFeature(cls.mLayer2.fields()) + ft3.setAttribute("pk", 11) + cls.mLayer2.startEditing() + cls.mLayer2.addFeature(ft3) + cls.mLayer2.commitChanges() + + ft4 = QgsFeature(cls.mLayer2.fields()) + ft4.setAttribute("pk", 12) + cls.mLayer2.startEditing() + cls.mLayer2.addFeature(ft4) + cls.mLayer2.commitChanges() + + # Add join features + jft1 = QgsFeature(cls.mLayerJoin.fields()) + jft1.setAttribute("pk", 101) + jft1.setAttribute("fk_layer1", 0) + jft1.setAttribute("fk_layer2", 10) + cls.mLayerJoin.startEditing() + cls.mLayerJoin.addFeature(jft1) + cls.mLayerJoin.commitChanges() + + jft2 = QgsFeature(cls.mLayerJoin.fields()) + jft2.setAttribute("pk", 102) + jft2.setAttribute("fk_layer1", 1) + jft2.setAttribute("fk_layer2", 11) + cls.mLayerJoin.startEditing() + cls.mLayerJoin.addFeature(jft2) + cls.mLayerJoin.commitChanges() + + jft3 = QgsFeature(cls.mLayerJoin.fields()) + jft3.setAttribute("pk", 102) + jft3.setAttribute("fk_layer1", 0) + jft3.setAttribute("fk_layer2", 11) + cls.mLayerJoin.startEditing() + cls.mLayerJoin.addFeature(jft3) + cls.mLayerJoin.commitChanges() + + @classmethod + def tearDownClass(cls): + QgsProject.instance().removeMapLayer(cls.mLayer1) + QgsProject.instance().removeMapLayer(cls.mLayer2) + QgsProject.instance().removeMapLayer(cls.mLayerJoin) + + def testMultiEdit1N(self): + # Init a relation editor widget + parent = QWidget() + relationEditorWidget = LinkingRelationEditorWidget({}, parent) + relationEditorWidget.setRelations(self.mRelation, QgsRelation()) + + self.assertFalse(relationEditorWidget.multiEditModeActive()) + + featureIds = list() + for feature in self.mLayer2.getFeatures(): + featureIds.append(feature.id()) + + relationEditorWidget.setMultiEditFeatureIds(featureIds) + + # Update ui + relationEditorWidget.updateUiMultiEdit() + + self.assertTrue(relationEditorWidget.multiEditModeActive()) + + setParentItemsText = set() + setChildrenItemsText = set() + for parentIndex in range(relationEditorWidget.mMultiEditTreeWidget.topLevelItemCount()): + parentItem = relationEditorWidget.mMultiEditTreeWidget.topLevelItem(parentIndex) + setParentItemsText.add(parentItem.text(0)) + self.assertEqual( + parentItem.data(0, (LinkingRelationEditorWidget.MultiEditTreeWidgetRole.FeatureType)), + (LinkingRelationEditorWidget.MultiEditFeatureType.Parent), + ) + for childIndex in range(parentItem.childCount()): + childItem = parentItem.child(childIndex) + setChildrenItemsText.add(childItem.text(0)) + self.assertEqual( + childItem.data(0, (LinkingRelationEditorWidget.MultiEditTreeWidgetRole.FeatureType)), + (LinkingRelationEditorWidget.MultiEditFeatureType.Child), + ) + + if childItem.text(0) == "Layer1-0": + self.assertEqual(parentItem.text(0), "Layer2-10") + + if childItem.text(0) == "Layer1-1": + self.assertEqual(parentItem.text(0), "Layer2-11") + + self.assertEqual(setParentItemsText, {"Layer2-10", "Layer2-11", "Layer2-12"}) + self.assertEqual(setChildrenItemsText, {"Layer1-0", "Layer1-1"}) + + def testMultiEditNM(self): + # Init a relation editor widget + parent = QWidget() + relationEditorWidget = LinkingRelationEditorWidget({}, parent) + relationEditorWidget.setRelations(self.mRelation1N, self.mRelationNM) + + self.assertFalse(relationEditorWidget.multiEditModeActive()) + + featureIds = list() + for feature in self.mLayer1.getFeatures(): + featureIds.append(feature.id()) + relationEditorWidget.setMultiEditFeatureIds(featureIds) + + # Update ui + relationEditorWidget.updateUiMultiEdit() + + self.assertTrue(relationEditorWidget.multiEditModeActive()) + + setParentItemsText = set() + listChildrenItemsText = list() + for parentIndex in range(relationEditorWidget.mMultiEditTreeWidget.topLevelItemCount()): + parentItem = relationEditorWidget.mMultiEditTreeWidget.topLevelItem(parentIndex) + setParentItemsText.add(parentItem.text(0)) + self.assertEqual( + parentItem.data(0, (LinkingRelationEditorWidget.MultiEditTreeWidgetRole.FeatureType)), + (LinkingRelationEditorWidget.MultiEditFeatureType.Parent), + ) + + for childIndex in range(parentItem.childCount()): + childItem = parentItem.child(childIndex) + listChildrenItemsText.append(childItem.text(0)) + self.assertEqual( + childItem.data(0, (LinkingRelationEditorWidget.MultiEditTreeWidgetRole.FeatureType)), + (LinkingRelationEditorWidget.MultiEditFeatureType.Child), + ) + + if childItem.text(0) == "Layer2-10": + self.assertEqual(parentItem.text(0), "Layer1-0") + + if childItem.text(0) == "Layer2-11": + possibleParents = list() + possibleParents.append("Layer1-0") + possibleParents.append("Layer1-1") + self.assertTrue(parentItem.text(0), possibleParents) + + self.assertEqual(setParentItemsText, {"Layer1-0", "Layer1-1"}) + + listChildrenItemsText.sort() + self.assertEqual(listChildrenItemsText, ["Layer2-10", "Layer2-11", "Layer2-11"])