class TestQgsRelationEditWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Setup the involved layers and relations for a n:m relation :return: """ cls.mapCanvas = QgsMapCanvas() QgsGui.editorWidgetRegistry().initEditors(cls.mapCanvas) cls.dbconn = 'service=\'qgis_test\'' if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] # Create test layer cls.vl_b = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'books', 'postgres') cls.vl_a = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'authors', 'postgres') cls.vl_link = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'books_authors', 'postgres') QgsProject.instance().addMapLayer(cls.vl_b) QgsProject.instance().addMapLayer(cls.vl_a) QgsProject.instance().addMapLayer(cls.vl_link) cls.relMgr = QgsProject.instance().relationManager() cls.rel_a = QgsRelation() cls.rel_a.setReferencingLayer(cls.vl_link.id()) cls.rel_a.setReferencedLayer(cls.vl_a.id()) cls.rel_a.addFieldPair('fk_author', 'pk') cls.rel_a.setId('rel_a') assert(cls.rel_a.isValid()) cls.relMgr.addRelation(cls.rel_a) cls.rel_b = QgsRelation() cls.rel_b.setReferencingLayer(cls.vl_link.id()) cls.rel_b.setReferencedLayer(cls.vl_b.id()) cls.rel_b.addFieldPair('fk_book', 'pk') cls.rel_b.setId('rel_b') assert(cls.rel_b.isValid()) cls.relMgr.addRelation(cls.rel_b) # Our mock QgsVectorLayerTools, that allow injecting data where user input is expected cls.vltools = VlTools() assert(cls.vl_a.isValid()) assert(cls.vl_b.isValid()) assert(cls.vl_link.isValid()) def setUp(self): self.startTransaction() def tearDown(self): self.rollbackTransaction() del self.transaction def test_delete_feature(self): """ Check if a feature can be deleted properly """ self.createWrapper(self.vl_a, '"name"=\'Erich Gamma\'') self.assertEqual(self.table_view.model().rowCount(), 1) self.assertEqual(1, len([f for f in self.vl_b.getFeatures()])) fid = next(self.vl_b.getFeatures(QgsFeatureRequest().setFilterExpression('"name"=\'Design Patterns. Elements of Reusable Object-Oriented Software\''))).id() self.widget.featureSelectionManager().select([fid]) btn = self.widget.findChild(QToolButton, 'mDeleteFeatureButton') def clickOk(): # Click the "Delete features" button on the confirmation message # box widget = self.widget.findChild(QMessageBox) buttonBox = widget.findChild(QDialogButtonBox) deleteButton = next((b for b in buttonBox.buttons() if buttonBox.buttonRole(b) == QDialogButtonBox.AcceptRole)) deleteButton.click() QTimer.singleShot(1, clickOk) btn.click() # This is the important check that the feature is deleted self.assertEqual(0, len([f for f in self.vl_b.getFeatures()])) # This is actually more checking that the database on delete action is properly set on the relation self.assertEqual(0, len([f for f in self.vl_link.getFeatures()])) self.assertEqual(self.table_view.model().rowCount(), 0) def test_list(self): """ Simple check if several related items are shown """ wrapper = self.createWrapper(self.vl_b) # NOQA self.assertEqual(self.table_view.model().rowCount(), 4) @unittest.expectedFailure(os.environ.get('QT_VERSION', '5') == '4' and os.environ.get('TRAVIS_OS_NAME', '') == 'linux') # It's probably not related to this variables at all, but that's the closest we can get to the real source of this problem at the moment... def test_add_feature(self): """ Check if a new related feature is added """ self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') self.assertEqual(self.table_view.model().rowCount(), 0) self.vltools.setValues([None, 'The Hitchhiker\'s Guide to the Galaxy']) btn = self.widget.findChild(QToolButton, 'mAddFeatureButton') btn.click() # Book entry has been created self.assertEqual(2, len([f for f in self.vl_b.getFeatures()])) # Link entry has been created self.assertEqual(5, len([f for f in self.vl_link.getFeatures()])) self.assertEqual(self.table_view.model().rowCount(), 1) def test_link_feature(self): """ Check if an existing feature can be linked """ wrapper = self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') # NOQA f = QgsFeature(self.vl_b.fields()) f.setAttributes([self.vl_b.dataProvider().defaultValueClause(0), 'The Hitchhiker\'s Guide to the Galaxy']) self.vl_b.addFeature(f) def choose_linked_feature(): dlg = QApplication.activeModalWidget() dlg.setSelectedFeatures([f.id()]) dlg.accept() btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton') timer = QTimer() timer.setSingleShot(True) timer.setInterval(0) # will run in the event loop as soon as it's processed when the dialog is opened timer.timeout.connect(choose_linked_feature) timer.start() btn.click() # magically the above code selects the feature here... link_feature = next(self.vl_link.getFeatures(QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format(f[0])))) self.assertIsNotNone(link_feature[0]) self.assertEqual(self.table_view.model().rowCount(), 1) def test_unlink_feature(self): """ Check if a linked feature can be unlinked """ wrapper = self.createWrapper(self.vl_b) # NOQA # All authors are listed self.assertEqual(self.table_view.model().rowCount(), 4) it = self.vl_a.getFeatures( QgsFeatureRequest().setFilterExpression('"name" IN (\'Richard Helm\', \'Ralph Johnson\')')) self.widget.featureSelectionManager().select([f.id() for f in it]) self.assertEqual(2, self.widget.featureSelectionManager().selectedFeatureCount()) btn = self.widget.findChild(QToolButton, 'mUnlinkFeatureButton') btn.click() # This is actually more checking that the database on delete action is properly set on the relation self.assertEqual(2, len([f for f in self.vl_link.getFeatures()])) self.assertEqual(2, self.table_view.model().rowCount()) def test_discover_relations(self): """ Test the automatic discovery of relations """ relations = self.relMgr.discoverRelations([], [self.vl_a, self.vl_b, self.vl_link]) relations = {r.name(): r for r in relations} self.assertEqual({'books_authors_fk_book_fkey', 'books_authors_fk_author_fkey'}, set(relations.keys())) ba2b = relations['books_authors_fk_book_fkey'] self.assertTrue(ba2b.isValid()) self.assertEqual('books_authors', ba2b.referencingLayer().name()) self.assertEqual('books', ba2b.referencedLayer().name()) self.assertEqual([0], ba2b.referencingFields()) self.assertEqual([0], ba2b.referencedFields()) ba2a = relations['books_authors_fk_author_fkey'] self.assertTrue(ba2a.isValid()) self.assertEqual('books_authors', ba2a.referencingLayer().name()) self.assertEqual('authors', ba2a.referencedLayer().name()) self.assertEqual([1], ba2a.referencingFields()) self.assertEqual([0], ba2a.referencedFields()) self.assertEqual([], self.relMgr.discoverRelations([self.rel_a, self.rel_b], [self.vl_a, self.vl_b, self.vl_link])) self.assertEqual(1, len(self.relMgr.discoverRelations([], [self.vl_a, self.vl_link]))) def startTransaction(self): """ Start a new transaction and set all layers into transaction mode. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] self.transaction = QgsTransaction.create(lyrs) self.transaction.begin() for l in lyrs: l.startEditing() def rollbackTransaction(self): """ Rollback all changes done in this transaction. We always rollback and never commit to have the database in a pristine state at the end of each test. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] for l in lyrs: l.commitChanges() self.transaction.rollback() def createWrapper(self, layer, filter=None): """ Basic setup of a relation widget wrapper. Will create a new wrapper and set its feature to the one and only book in the table. It will also assign some instance variables to help * self.widget The created widget * self.table_view The table view of the widget :return: The created wrapper """ if layer == self.vl_b: relation = self.rel_b nmrel = self.rel_a else: relation = self.rel_a nmrel = self.rel_b self.wrapper = QgsRelationWidgetWrapper(layer, relation) self.wrapper.setConfig({'nm-rel': nmrel.id()}) context = QgsAttributeEditorContext() context.setVectorLayerTools(self.vltools) self.wrapper.setContext(context) self.widget = self.wrapper.widget() self.widget.show() request = QgsFeatureRequest() if filter: request.setFilterExpression(filter) book = next(layer.getFeatures(request)) self.wrapper.setFeature(book) self.table_view = self.widget.findChild(QTableView) return self.wrapper
class TestQgsRelationEditWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Setup the involved layers and relations for a n:m relation :return: """ cls.mapCanvas = QgsMapCanvas() QgsGui.editorWidgetRegistry().initEditors(cls.mapCanvas) cls.dbconn = 'service=\'qgis_test\'' if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] # Create test layer cls.vl_b = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'books', 'postgres') cls.vl_a = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'authors', 'postgres') cls.vl_link = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'books_authors', 'postgres') QgsProject.instance().addMapLayer(cls.vl_b) QgsProject.instance().addMapLayer(cls.vl_a) QgsProject.instance().addMapLayer(cls.vl_link) cls.relMgr = QgsProject.instance().relationManager() cls.rel_a = QgsRelation() cls.rel_a.setReferencingLayer(cls.vl_link.id()) cls.rel_a.setReferencedLayer(cls.vl_a.id()) cls.rel_a.addFieldPair('fk_author', 'pk') cls.rel_a.setId('rel_a') assert(cls.rel_a.isValid()) cls.relMgr.addRelation(cls.rel_a) cls.rel_b = QgsRelation() cls.rel_b.setReferencingLayer(cls.vl_link.id()) cls.rel_b.setReferencedLayer(cls.vl_b.id()) cls.rel_b.addFieldPair('fk_book', 'pk') cls.rel_b.setId('rel_b') assert(cls.rel_b.isValid()) cls.relMgr.addRelation(cls.rel_b) # Our mock QgsVectorLayerTools, that allow injecting data where user input is expected cls.vltools = VlTools() assert(cls.vl_a.isValid()) assert(cls.vl_b.isValid()) assert(cls.vl_link.isValid()) def setUp(self): self.startTransaction() def tearDown(self): self.rollbackTransaction() del self.transaction def test_delete_feature(self): """ Check if a feature can be deleted properly """ self.createWrapper(self.vl_a, '"name"=\'Erich Gamma\'') self.assertEqual(self.table_view.model().rowCount(), 1) self.assertEqual(1, len([f for f in self.vl_b.getFeatures()])) fid = next(self.vl_b.getFeatures(QgsFeatureRequest().setFilterExpression('"name"=\'Design Patterns. Elements of Reusable Object-Oriented Software\''))).id() self.widget.featureSelectionManager().select([fid]) btn = self.widget.findChild(QToolButton, 'mDeleteFeatureButton') def clickOk(): # Click the "Delete features" button on the confirmation message # box widget = self.widget.findChild(QMessageBox) buttonBox = widget.findChild(QDialogButtonBox) deleteButton = next((b for b in buttonBox.buttons() if buttonBox.buttonRole(b) == QDialogButtonBox.AcceptRole)) deleteButton.click() QTimer.singleShot(1, clickOk) btn.click() # This is the important check that the feature is deleted self.assertEqual(0, len([f for f in self.vl_b.getFeatures()])) # This is actually more checking that the database on delete action is properly set on the relation self.assertEqual(0, len([f for f in self.vl_link.getFeatures()])) self.assertEqual(self.table_view.model().rowCount(), 0) def test_list(self): """ Simple check if several related items are shown """ wrapper = self.createWrapper(self.vl_b) # NOQA self.assertEqual(self.table_view.model().rowCount(), 4) @unittest.expectedFailure(os.environ.get('QT_VERSION', '5') == '4' and os.environ.get('TRAVIS_OS_NAME', '') == 'linux') # It's probably not related to this variables at all, but that's the closest we can get to the real source of this problem at the moment... def test_add_feature(self): """ Check if a new related feature is added """ self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') self.assertEqual(self.table_view.model().rowCount(), 0) self.vltools.setValues([None, 'The Hitchhiker\'s Guide to the Galaxy']) btn = self.widget.findChild(QToolButton, 'mAddFeatureButton') btn.click() # Book entry has been created self.assertEqual(2, len([f for f in self.vl_b.getFeatures()])) # Link entry has been created self.assertEqual(5, len([f for f in self.vl_link.getFeatures()])) self.assertEqual(self.table_view.model().rowCount(), 1) def test_link_feature(self): """ Check if an existing feature can be linked """ wrapper = self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') # NOQA f = QgsFeature(self.vl_b.fields()) f.setAttributes([self.vl_b.dataProvider().defaultValueClause(0), 'The Hitchhiker\'s Guide to the Galaxy']) self.vl_b.addFeature(f) def choose_linked_feature(): dlg = QApplication.activeModalWidget() dlg.setSelectedFeatures([f.id()]) dlg.accept() btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton') timer = QTimer() timer.setSingleShot(True) timer.setInterval(0) # will run in the event loop as soon as it's processed when the dialog is opened timer.timeout.connect(choose_linked_feature) timer.start() btn.click() # magically the above code selects the feature here... link_feature = next(self.vl_link.getFeatures(QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format(f[0])))) self.assertIsNotNone(link_feature[0]) self.assertEqual(self.table_view.model().rowCount(), 1) def test_unlink_feature(self): """ Check if a linked feature can be unlinked """ wrapper = self.createWrapper(self.vl_b) # NOQA # All authors are listed self.assertEqual(self.table_view.model().rowCount(), 4) it = self.vl_a.getFeatures( QgsFeatureRequest().setFilterExpression('"name" IN (\'Richard Helm\', \'Ralph Johnson\')')) self.widget.featureSelectionManager().select([f.id() for f in it]) self.assertEqual(2, self.widget.featureSelectionManager().selectedFeatureCount()) btn = self.widget.findChild(QToolButton, 'mUnlinkFeatureButton') btn.click() # This is actually more checking that the database on delete action is properly set on the relation self.assertEqual(2, len([f for f in self.vl_link.getFeatures()])) self.assertEqual(2, self.table_view.model().rowCount()) def test_discover_relations(self): """ Test the automatic discovery of relations """ relations = self.relMgr.discoverRelations([], [self.vl_a, self.vl_b, self.vl_link]) relations = {r.name(): r for r in relations} self.assertEqual({'books_authors_fk_book_fkey', 'books_authors_fk_author_fkey'}, set(relations.keys())) ba2b = relations['books_authors_fk_book_fkey'] self.assertTrue(ba2b.isValid()) self.assertEqual('books_authors', ba2b.referencingLayer().name()) self.assertEqual('books', ba2b.referencedLayer().name()) self.assertEqual([0], ba2b.referencingFields()) self.assertEqual([0], ba2b.referencedFields()) ba2a = relations['books_authors_fk_author_fkey'] self.assertTrue(ba2a.isValid()) self.assertEqual('books_authors', ba2a.referencingLayer().name()) self.assertEqual('authors', ba2a.referencedLayer().name()) self.assertEqual([1], ba2a.referencingFields()) self.assertEqual([0], ba2a.referencedFields()) self.assertEqual([], self.relMgr.discoverRelations([self.rel_a, self.rel_b], [self.vl_a, self.vl_b, self.vl_link])) self.assertEqual(1, len(self.relMgr.discoverRelations([], [self.vl_a, self.vl_link]))) def startTransaction(self): """ Start a new transaction and set all layers into transaction mode. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] self.transaction = QgsTransaction.create(lyrs) self.transaction.begin() for l in lyrs: l.startEditing() def rollbackTransaction(self): """ Rollback all changes done in this transaction. We always rollback and never commit to have the database in a pristine state at the end of each test. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] for l in lyrs: l.commitChanges() self.transaction.rollback() def createWrapper(self, layer, filter=None): """ Basic setup of a relation widget wrapper. Will create a new wrapper and set its feature to the one and only book in the table. It will also assign some instance variables to help * self.widget The created widget * self.table_view The table view of the widget :return: The created wrapper """ if layer == self.vl_b: relation = self.rel_b nmrel = self.rel_a else: relation = self.rel_a nmrel = self.rel_b self.wrapper = QgsRelationWidgetWrapper(layer, relation) self.wrapper.setConfig({'nm-rel': nmrel.id()}) context = QgsAttributeEditorContext() context.setVectorLayerTools(self.vltools) self.wrapper.setContext(context) self.widget = self.wrapper.widget() self.widget.show() request = QgsFeatureRequest() if filter: request.setFilterExpression(filter) book = next(layer.getFeatures(request)) self.wrapper.setFeature(book) self.table_view = self.widget.findChild(QTableView) return self.wrapper
class TestQgsRelationEditWidget(TestCase): @classmethod def setUpClass(cls): """ Setup the involved layers and relations for a n:m relation :return: """ QgsEditorWidgetRegistry.initEditors() cls.dbconn = u'dbname=\'qgis_test\' host=localhost port=5432 user=\'postgres\' password=\'postgres\'' if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] # Create test layer cls.vl_b = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'test', 'postgres') cls.vl_a = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'test', 'postgres') cls.vl_link = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'test', 'postgres') QgsMapLayerRegistry.instance().addMapLayer(cls.vl_b) QgsMapLayerRegistry.instance().addMapLayer(cls.vl_a) QgsMapLayerRegistry.instance().addMapLayer(cls.vl_link) relMgr = QgsProject.instance().relationManager() cls.rel_a = QgsRelation() cls.rel_a.setReferencingLayer(cls.vl_link.id()) cls.rel_a.setReferencedLayer(cls.vl_a.id()) cls.rel_a.addFieldPair('fk_author', 'pk') cls.rel_a.setRelationId('rel_a') assert(cls.rel_a.isValid()) relMgr.addRelation(cls.rel_a) cls.rel_b = QgsRelation() cls.rel_b.setReferencingLayer(cls.vl_link.id()) cls.rel_b.setReferencedLayer(cls.vl_b.id()) cls.rel_b.addFieldPair('fk_book', 'pk') cls.rel_b.setRelationId('rel_b') assert(cls.rel_b.isValid()) relMgr.addRelation(cls.rel_b) # Our mock QgsVectorLayerTools, that allow to inject data where user input is expected cls.vltools = VlTools() assert(cls.vl_a.isValid()) assert(cls.vl_b.isValid()) assert(cls.vl_link.isValid()) def setUp(self): self.startTransaction() def tearDown(self): self.rollbackTransaction() def test_delete_feature(self): """ Check if a feature can be deleted properly """ self.createWrapper(self.vl_a, '"name"=\'Erich Gamma\'') self.assertEquals(self.table_view.model().rowCount(), 1) self.assertEquals(1, len([f for f in self.vl_b.getFeatures()])) fid = self.vl_b.getFeatures(QgsFeatureRequest().setFilterExpression('"name"=\'Design Patterns. Elements of Reusable Object-Oriented Software\'')).next().id() self.widget.featureSelectionManager().select([fid]) btn = self.widget.findChild(QToolButton, 'mDeleteFeatureButton') btn.click() # This is the important check that the feature is deleted self.assertEquals(0, len([f for f in self.vl_b.getFeatures()])) # This is actually more checking that the database on delete action is properly set on the relation self.assertEquals(0, len([f for f in self.vl_link.getFeatures()])) self.assertEquals(self.table_view.model().rowCount(), 0) def test_list(self): """ Simple check if several related items are shown """ wrapper = self.createWrapper(self.vl_b) self.assertEquals(self.table_view.model().rowCount(), 4) @expectedFailure def test_add_feature(self): """ Check if a new related feature is added """ self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') self.assertEquals(self.table_view.model().rowCount(), 0) self.vltools.setValues([None, 'The Hitchhiker\'s Guide to the Galaxy']) btn = self.widget.findChild(QToolButton, 'mAddFeatureButton') btn.click() # Book entry has been created self.assertEquals(2, len([f for f in self.vl_b.getFeatures()])) # Link entry has been created self.assertEquals(5, len([f for f in self.vl_link.getFeatures()])) self.assertEquals(self.table_view.model().rowCount(), 1) def test_link_feature(self): """ Check if an existing feature can be linked """ wrapper = self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') f = QgsFeature(self.vl_b.fields()) f.setAttributes([self.vl_b.dataProvider().defaultValue(0), 'The Hitchhiker\'s Guide to the Galaxy']) self.vl_b.addFeature(f) def choose_linked_feature(): dlg = QApplication.activeModalWidget() dlg.setSelectedFeatures([f.id()]) dlg.accept() btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton') timer = QTimer() timer.setSingleShot(True) timer.setInterval(0) # will run in the event loop as soon as it's processed when the dialog is opened timer.timeout.connect(choose_linked_feature) timer.start() btn.click() # magically the above code selects the feature here... link_feature = self.vl_link.getFeatures(QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format(f[0]))).next() self.assertIsNotNone(link_feature[0]) self.assertEquals(self.table_view.model().rowCount(), 1) @expectedFailure def test_unlink_feature(self): """ Check if a linked feature can be unlinked """ wrapper = self.createWrapper(self.vl_b) wdg = wrapper.widget() # All authors are listed self.assertEquals(self.table_view.model().rowCount(), 4) it = self.vl_a.getFeatures( QgsFeatureRequest().setFilterExpression('"name" IN (\'Richard Helm\', \'Ralph Johnson\')')) self.widget.featureSelectionManager().select([f.id() for f in it]) btn = self.widget.findChild(QToolButton, 'mUnlinkFeatureButton') btn.click() # This is actually more checking that the database on delete action is properly set on the relation self.assertEquals(2, len([f for f in self.vl_link.getFeatures()])) self.assertEquals(2, self.table_view.model().rowCount()) def startTransaction(self): """ Start a new transaction and set all layers into transaction mode. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] self.transaction = QgsTransaction.create([l.id() for l in lyrs]) self.transaction.begin() for l in lyrs: l.startEditing() def rollbackTransaction(self): """ Rollback all changes done in this transaction. We always rollback and never commit to have the database in a pristine state at the end of each test. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] for l in lyrs: l.commitChanges() self.transaction.rollback() def createWrapper(self, layer, filter=None): """ Basic setup of a relation widget wrapper. Will create a new wrapper and set its feature to the one and only book in the table. It will also assign some instance variables to help * self.widget The created widget * self.table_view The table view of the widget :return: The created wrapper """ if layer == self.vl_b: relation = self.rel_b nmrel = self.rel_a else: relation = self.rel_a nmrel = self.rel_b parent = QWidget() self.wrapper = QgsRelationWidgetWrapper(layer, relation) self.wrapper.setConfig({'nm-rel': nmrel.id()}) context = QgsAttributeEditorContext() context.setVectorLayerTools(self.vltools) self.wrapper.setContext(context) self.widget = self.wrapper.widget() self.widget.show() request = QgsFeatureRequest() if filter: request.setFilterExpression(filter) book = layer.getFeatures(request).next() self.wrapper.setFeature(book) self.table_view = self.widget.findChild(QTableView) return self.wrapper
class TestQgsRelationEditWidget(unittest.TestCase): @classmethod def setUpClass(cls): """ Setup the involved layers and relations for a n:m relation :return: """ cls.mapCanvas = QgsMapCanvas() QgsGui.editorWidgetRegistry().initEditors(cls.mapCanvas) cls.dbconn = 'service=\'qgis_test\'' if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] # Create test layer cls.vl_books = QgsVectorLayer( cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'books', 'postgres') cls.vl_authors = QgsVectorLayer( cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'authors', 'postgres') cls.vl_editors = QgsVectorLayer( cls.dbconn + ' sslmode=disable key=\'fk_book,fk_author\' table="qgis_test"."editors" sql=', 'editors', 'postgres') cls.vl_link_books_authors = QgsVectorLayer( cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'books_authors', 'postgres') QgsProject.instance().addMapLayer(cls.vl_books) QgsProject.instance().addMapLayer(cls.vl_authors) QgsProject.instance().addMapLayer(cls.vl_editors) QgsProject.instance().addMapLayer(cls.vl_link_books_authors) cls.relMgr = QgsProject.instance().relationManager() cls.rel_a = QgsRelation() cls.rel_a.setReferencingLayer(cls.vl_link_books_authors.id()) cls.rel_a.setReferencedLayer(cls.vl_authors.id()) cls.rel_a.addFieldPair('fk_author', 'pk') cls.rel_a.setId('rel_a') assert (cls.rel_a.isValid()) cls.relMgr.addRelation(cls.rel_a) cls.rel_b = QgsRelation() cls.rel_b.setReferencingLayer(cls.vl_link_books_authors.id()) cls.rel_b.setReferencedLayer(cls.vl_books.id()) cls.rel_b.addFieldPair('fk_book', 'pk') cls.rel_b.setId('rel_b') assert (cls.rel_b.isValid()) cls.relMgr.addRelation(cls.rel_b) # Our mock QgsVectorLayerTools, that allow injecting data where user input is expected cls.vltools = VlTools() assert (cls.vl_authors.isValid()) assert (cls.vl_books.isValid()) assert (cls.vl_editors.isValid()) assert (cls.vl_link_books_authors.isValid()) def setUp(self): self.startTransaction() def tearDown(self): self.rollbackTransaction() del self.transaction def test_delete_feature(self): """ Check if a feature can be deleted properly """ self.createWrapper(self.vl_authors, '"name"=\'Erich Gamma\'') self.assertEqual(self.table_view.model().rowCount(), 1) self.assertEqual(1, len([f for f in self.vl_books.getFeatures()])) fid = next( self.vl_books.getFeatures(QgsFeatureRequest().setFilterExpression( '"name"=\'Design Patterns. Elements of Reusable Object-Oriented Software\'' ))).id() self.widget.featureSelectionManager().select([fid]) btn = self.widget.findChild(QToolButton, 'mDeleteFeatureButton') def clickOk(): # Click the "Delete features" button on the confirmation message # box widget = self.widget.findChild(QMessageBox) buttonBox = widget.findChild(QDialogButtonBox) deleteButton = next( (b for b in buttonBox.buttons() if buttonBox.buttonRole(b) == QDialogButtonBox.AcceptRole)) deleteButton.click() QTimer.singleShot(1, clickOk) btn.click() # This is the important check that the feature is deleted self.assertEqual(0, len([f for f in self.vl_books.getFeatures()])) # This is actually more checking that the database on delete action is properly set on the relation self.assertEqual( 0, len([f for f in self.vl_link_books_authors.getFeatures()])) self.assertEqual(self.table_view.model().rowCount(), 0) def test_list(self): """ Simple check if several related items are shown """ wrapper = self.createWrapper(self.vl_books) # NOQA self.assertEqual(self.table_view.model().rowCount(), 4) def test_add_feature(self): """ Check if a new related feature is added """ self.createWrapper(self.vl_authors, '"name"=\'Douglas Adams\'') self.assertEqual(self.table_view.model().rowCount(), 0) self.vltools.setValues([ None, 'The Hitchhiker\'s Guide to the Galaxy', 'Sputnik Editions', 1961 ]) btn = self.widget.findChild(QToolButton, 'mAddFeatureButton') btn.click() # Book entry has been created self.assertEqual(2, len([f for f in self.vl_books.getFeatures()])) # Link entry has been created self.assertEqual( 5, len([f for f in self.vl_link_books_authors.getFeatures()])) self.assertEqual(self.table_view.model().rowCount(), 1) def test_link_feature(self): """ Check if an existing feature can be linked """ wrapper = self.createWrapper(self.vl_authors, '"name"=\'Douglas Adams\'') # NOQA f = QgsFeature(self.vl_books.fields()) f.setAttributes([ self.vl_books.dataProvider().defaultValueClause(0), 'The Hitchhiker\'s Guide to the Galaxy', 'Sputnik Editions', 1961 ]) self.vl_books.addFeature(f) btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton') btn.click() dlg = self.widget.findChild(QDialog) dlg.setSelectedFeatures([f.id()]) dlg.accept() # magically the above code selects the feature here... link_feature = next( self.vl_link_books_authors.getFeatures( QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format( f[0])))) self.assertIsNotNone(link_feature[0]) self.assertEqual(self.table_view.model().rowCount(), 1) def test_unlink_feature(self): """ Check if a linked feature can be unlinked """ wrapper = self.createWrapper(self.vl_books) # NOQA # All authors are listed self.assertEqual(self.table_view.model().rowCount(), 4) it = self.vl_authors.getFeatures( QgsFeatureRequest().setFilterExpression( '"name" IN (\'Richard Helm\', \'Ralph Johnson\')')) self.widget.featureSelectionManager().select([f.id() for f in it]) self.assertEqual( 2, self.widget.featureSelectionManager().selectedFeatureCount()) btn = self.widget.findChild(QToolButton, 'mUnlinkFeatureButton') btn.click() # This is actually more checking that the database on delete action is properly set on the relation self.assertEqual( 2, len([f for f in self.vl_link_books_authors.getFeatures()])) self.assertEqual(2, self.table_view.model().rowCount()) def test_discover_relations(self): """ Test the automatic discovery of relations """ relations = self.relMgr.discoverRelations( [], [self.vl_authors, self.vl_books, self.vl_link_books_authors]) relations = {r.name(): r for r in relations} self.assertEqual( {'books_authors_fk_book_fkey', 'books_authors_fk_author_fkey'}, set(relations.keys())) ba2b = relations['books_authors_fk_book_fkey'] self.assertTrue(ba2b.isValid()) self.assertEqual('books_authors', ba2b.referencingLayer().name()) self.assertEqual('books', ba2b.referencedLayer().name()) self.assertEqual([0], ba2b.referencingFields()) self.assertEqual([0], ba2b.referencedFields()) ba2a = relations['books_authors_fk_author_fkey'] self.assertTrue(ba2a.isValid()) self.assertEqual('books_authors', ba2a.referencingLayer().name()) self.assertEqual('authors', ba2a.referencedLayer().name()) self.assertEqual([1], ba2a.referencingFields()) self.assertEqual([0], ba2a.referencedFields()) self.assertEqual( [], self.relMgr.discoverRelations( [self.rel_a, self.rel_b], [self.vl_authors, self.vl_books, self.vl_link_books_authors])) self.assertEqual( 1, len( self.relMgr.discoverRelations( [], [self.vl_authors, self.vl_link_books_authors]))) # composite keys relation relations = self.relMgr.discoverRelations( [], [self.vl_books, self.vl_editors]) self.assertEqual(len(relations), 1) relation = relations[0] self.assertEqual('books_fk_editor_fkey', relation.name()) self.assertTrue(relation.isValid()) self.assertEqual('books', relation.referencingLayer().name()) self.assertEqual('editors', relation.referencedLayer().name()) self.assertEqual([2, 3], relation.referencingFields()) self.assertEqual([0, 1], relation.referencedFields()) def test_selection(self): fbook = QgsFeature(self.vl_books.fields()) fbook.setAttributes([ self.vl_books.dataProvider().defaultValueClause(0), 'The Hitchhiker\'s Guide to the Galaxy', 'Sputnik Editions', 1961 ]) self.vl_books.addFeature(fbook) flink = QgsFeature(self.vl_link_books_authors.fields()) flink.setAttributes([fbook.id(), 5]) self.vl_link_books_authors.addFeature(flink) self.createWrapper(self.vl_authors, '"name"=\'Douglas Adams\'') self.zoomToButton = self.widget.findChild(QToolButton, "mDeleteFeatureButton") self.assertTrue(self.zoomToButton) self.assertTrue(not self.zoomToButton.isEnabled()) selectionMgr = self.widget.featureSelectionManager() self.assertTrue(selectionMgr) self.vl_books.select(fbook.id()) self.assertEqual([fbook.id()], selectionMgr.selectedFeatureIds()) self.assertTrue(self.zoomToButton.isEnabled()) selectionMgr.deselect([fbook.id()]) self.assertEqual([], selectionMgr.selectedFeatureIds()) self.assertTrue(not self.zoomToButton.isEnabled()) self.vl_books.select([1, fbook.id()]) self.assertEqual([fbook.id()], selectionMgr.selectedFeatureIds()) self.assertTrue(self.zoomToButton.isEnabled()) def test_add_feature_geometry(self): """ Test to add a feature with a geometry """ vl_pipes = QgsVectorLayer( self.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."pipes" (geom) sql=', 'pipes', 'postgres') vl_leaks = QgsVectorLayer( self.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."leaks" (geom) sql=', 'leaks', 'postgres') vl_leaks.startEditing() QgsProject.instance().addMapLayer(vl_pipes) QgsProject.instance().addMapLayer(vl_leaks) self.assertEqual(vl_pipes.featureCount(), 2) self.assertEqual(vl_leaks.featureCount(), 3) rel = QgsRelation() rel.setReferencingLayer(vl_leaks.id()) rel.setReferencedLayer(vl_pipes.id()) rel.addFieldPair('pipe', 'id') rel.setId('rel_pipe_leak') self.assertTrue(rel.isValid()) self.relMgr.addRelation(rel) # Mock vector layer tool to just set default value on created feature class DummyVlTools(QgsVectorLayerTools): def addFeature(self, layer, defaultValues, defaultGeometry): f = QgsFeature(layer.fields()) for idx, value in defaultValues.items(): f.setAttribute(idx, value) f.setGeometry(defaultGeometry) ok = layer.addFeature(f) return ok, f wrapper = QgsRelationWidgetWrapper(vl_leaks, rel) context = QgsAttributeEditorContext() vltool = DummyVlTools() context.setVectorLayerTools(vltool) context.setMapCanvas(self.mapCanvas) cadDockWidget = QgsAdvancedDigitizingDockWidget(self.mapCanvas) context.setCadDockWidget(cadDockWidget) wrapper.setContext(context) widget = wrapper.widget() widget.show() pipe = next(vl_pipes.getFeatures()) self.assertEqual(pipe.id(), 1) wrapper.setFeature(pipe) table_view = widget.findChild(QTableView) self.assertEqual(table_view.model().rowCount(), 1) btn = widget.findChild(QToolButton, 'mAddFeatureGeometryButton') self.assertTrue(btn.isVisible()) self.assertTrue(btn.isEnabled()) btn.click() self.assertTrue(self.mapCanvas.mapTool()) feature = QgsFeature(vl_leaks.fields()) feature.setGeometry(QgsGeometry.fromWkt('POINT(0 0.8)')) self.mapCanvas.mapTool().digitizingCompleted.emit(feature) self.assertEqual(table_view.model().rowCount(), 2) self.assertEqual(vl_leaks.featureCount(), 4) request = QgsFeatureRequest() request.addOrderBy("id", False) # get new created feature feat = next(vl_leaks.getFeatures('"id" is NULL')) self.assertTrue(feat.isValid()) self.assertTrue(feat.geometry().equals( QgsGeometry.fromWkt('POINT(0 0.8)'))) vl_leaks.rollBack() def startTransaction(self): """ Start a new transaction and set all layers into transaction mode. :return: None """ lyrs = [self.vl_authors, self.vl_books, self.vl_link_books_authors] self.transaction = QgsTransaction.create(lyrs) self.transaction.begin() for l in lyrs: l.startEditing() def rollbackTransaction(self): """ Rollback all changes done in this transaction. We always rollback and never commit to have the database in a pristine state at the end of each test. :return: None """ lyrs = [self.vl_authors, self.vl_books, self.vl_link_books_authors] for l in lyrs: l.commitChanges() self.transaction.rollback() def createWrapper(self, layer, filter=None): """ Basic setup of a relation widget wrapper. Will create a new wrapper and set its feature to the one and only book in the table. It will also assign some instance variables to help * self.widget The created widget * self.table_view The table view of the widget :return: The created wrapper """ if layer == self.vl_books: relation = self.rel_b nmrel = self.rel_a else: relation = self.rel_a nmrel = self.rel_b self.wrapper = QgsRelationWidgetWrapper(layer, relation) self.wrapper.setConfig({'nm-rel': nmrel.id()}) context = QgsAttributeEditorContext() context.setMapCanvas(self.mapCanvas) context.setVectorLayerTools(self.vltools) self.wrapper.setContext(context) self.widget = self.wrapper.widget() self.widget.show() request = QgsFeatureRequest() if filter: request.setFilterExpression(filter) book = next(layer.getFeatures(request)) self.wrapper.setFeature(book) self.table_view = self.widget.findChild(QTableView) return self.wrapper
class TestQgsRelationEditWidget(TestCase): @classmethod def setUpClass(cls): """ Setup the involved layers and relations for a n:m relation :return: """ QgsEditorWidgetRegistry.initEditors() cls.dbconn = u'dbname=\'qgis_test\' host=localhost port=5432 user=\'postgres\' password=\'postgres\'' if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] # Create test layer cls.vl_b = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'test', 'postgres') cls.vl_a = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'test', 'postgres') cls.vl_link = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'test', 'postgres') QgsMapLayerRegistry.instance().addMapLayer(cls.vl_b) QgsMapLayerRegistry.instance().addMapLayer(cls.vl_a) QgsMapLayerRegistry.instance().addMapLayer(cls.vl_link) relMgr = QgsProject.instance().relationManager() cls.rel_a = QgsRelation() cls.rel_a.setReferencingLayer(cls.vl_link.id()) cls.rel_a.setReferencedLayer(cls.vl_a.id()) cls.rel_a.addFieldPair('fk_author', 'pk') cls.rel_a.setRelationId('rel_a') assert(cls.rel_a.isValid()) relMgr.addRelation(cls.rel_a) cls.rel_b = QgsRelation() cls.rel_b.setReferencingLayer(cls.vl_link.id()) cls.rel_b.setReferencedLayer(cls.vl_b.id()) cls.rel_b.addFieldPair('fk_book', 'pk') cls.rel_b.setRelationId('rel_b') assert(cls.rel_b.isValid()) relMgr.addRelation(cls.rel_b) # Our mock QgsVectorLayerTools, that allow injecting data where user input is expected cls.vltools = VlTools() assert(cls.vl_a.isValid()) assert(cls.vl_b.isValid()) assert(cls.vl_link.isValid()) def setUp(self): self.startTransaction() def tearDown(self): self.rollbackTransaction() def test_delete_feature(self): """ Check if a feature can be deleted properly """ self.createWrapper(self.vl_a, '"name"=\'Erich Gamma\'') self.assertEquals(self.table_view.model().rowCount(), 1) self.assertEquals(1, len([f for f in self.vl_b.getFeatures()])) fid = self.vl_b.getFeatures(QgsFeatureRequest().setFilterExpression('"name"=\'Design Patterns. Elements of Reusable Object-Oriented Software\'')).next().id() self.widget.featureSelectionManager().select([fid]) btn = self.widget.findChild(QToolButton, 'mDeleteFeatureButton') btn.click() # This is the important check that the feature is deleted self.assertEquals(0, len([f for f in self.vl_b.getFeatures()])) # This is actually more checking that the database on delete action is properly set on the relation self.assertEquals(0, len([f for f in self.vl_link.getFeatures()])) self.assertEquals(self.table_view.model().rowCount(), 0) def test_list(self): """ Simple check if several related items are shown """ wrapper = self.createWrapper(self.vl_b) self.assertEquals(self.table_view.model().rowCount(), 4) @expectedFailure def test_add_feature(self): """ Check if a new related feature is added """ self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') self.assertEquals(self.table_view.model().rowCount(), 0) self.vltools.setValues([None, 'The Hitchhiker\'s Guide to the Galaxy']) btn = self.widget.findChild(QToolButton, 'mAddFeatureButton') btn.click() # Book entry has been created self.assertEquals(2, len([f for f in self.vl_b.getFeatures()])) # Link entry has been created self.assertEquals(5, len([f for f in self.vl_link.getFeatures()])) self.assertEquals(self.table_view.model().rowCount(), 1) def test_link_feature(self): """ Check if an existing feature can be linked """ wrapper = self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'') f = QgsFeature(self.vl_b.fields()) f.setAttributes([self.vl_b.dataProvider().defaultValue(0), 'The Hitchhiker\'s Guide to the Galaxy']) self.vl_b.addFeature(f) def choose_linked_feature(): dlg = QApplication.activeModalWidget() dlg.setSelectedFeatures([f.id()]) dlg.accept() btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton') timer = QTimer() timer.setSingleShot(True) timer.setInterval(0) # will run in the event loop as soon as it's processed when the dialog is opened timer.timeout.connect(choose_linked_feature) timer.start() btn.click() # magically the above code selects the feature here... link_feature = self.vl_link.getFeatures(QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format(f[0]))).next() self.assertIsNotNone(link_feature[0]) self.assertEquals(self.table_view.model().rowCount(), 1) @expectedFailure def test_unlink_feature(self): """ Check if a linked feature can be unlinked """ wrapper = self.createWrapper(self.vl_b) wdg = wrapper.widget() # All authors are listed self.assertEquals(self.table_view.model().rowCount(), 4) it = self.vl_a.getFeatures( QgsFeatureRequest().setFilterExpression('"name" IN (\'Richard Helm\', \'Ralph Johnson\')')) self.widget.featureSelectionManager().select([f.id() for f in it]) btn = self.widget.findChild(QToolButton, 'mUnlinkFeatureButton') btn.click() # This is actually more checking that the database on delete action is properly set on the relation self.assertEquals(2, len([f for f in self.vl_link.getFeatures()])) self.assertEquals(2, self.table_view.model().rowCount()) def startTransaction(self): """ Start a new transaction and set all layers into transaction mode. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] self.transaction = QgsTransaction.create([l.id() for l in lyrs]) self.transaction.begin() for l in lyrs: l.startEditing() def rollbackTransaction(self): """ Rollback all changes done in this transaction. We always rollback and never commit to have the database in a pristine state at the end of each test. :return: None """ lyrs = [self.vl_a, self.vl_b, self.vl_link] for l in lyrs: l.commitChanges() self.transaction.rollback() def createWrapper(self, layer, filter=None): """ Basic setup of a relation widget wrapper. Will create a new wrapper and set its feature to the one and only book in the table. It will also assign some instance variables to help * self.widget The created widget * self.table_view The table view of the widget :return: The created wrapper """ if layer == self.vl_b: relation = self.rel_b nmrel = self.rel_a else: relation = self.rel_a nmrel = self.rel_b parent = QWidget() self.wrapper = QgsRelationWidgetWrapper(layer, relation) self.wrapper.setConfig({'nm-rel': nmrel.id()}) context = QgsAttributeEditorContext() context.setVectorLayerTools(self.vltools) self.wrapper.setContext(context) self.widget = self.wrapper.widget() self.widget.show() request = QgsFeatureRequest() if filter: request.setFilterExpression(filter) book = layer.getFeatures(request).next() self.wrapper.setFeature(book) self.table_view = self.widget.findChild(QTableView) return self.wrapper