1 /***************************************************************************
2 testqgsrelationreferencewidget.cpp
3 --------------------------------------
4 Date : 21 07 2017
5 Copyright : (C) 2017 Paul Blottiere
6 Email : paul dot blottiere at oslandia dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16
17 #include "qgstest.h"
18 #include <QSignalSpy>
19
20 #include <editorwidgets/core/qgseditorwidgetregistry.h>
21 #include <qgsapplication.h>
22 #include "qgseditorwidgetwrapper.h"
23 #include <editorwidgets/qgsrelationreferencewidget.h>
24 #include <editorwidgets/qgsrelationreferencewidgetwrapper.h>
25 #include <qgsproject.h>
26 #include <qgsattributeform.h>
27 #include <qgsrelationmanager.h>
28 #include <attributetable/qgsattributetablefiltermodel.h>
29 #include "qgsfeaturelistcombobox.h"
30 #include "qgsfeaturefiltermodel.h"
31 #include "qgsgui.h"
32 #include "qgsmapcanvas.h"
33 #include "qgsvectorlayertools.h"
34 #include "qgsadvanceddigitizingdockwidget.h"
35 #include "qgsmaptooldigitizefeature.h"
36
getComboBoxItems(const QComboBox * cb)37 QStringList getComboBoxItems( const QComboBox *cb )
38 {
39 QStringList items;
40 for ( int i = 0; i < cb->count(); i++ )
41 items << cb->itemText( i );
42
43 return items;
44 }
45
46 class TestQgsRelationReferenceWidget : public QObject
47 {
48 Q_OBJECT
49 public:
50 TestQgsRelationReferenceWidget() = default;
51
52 private slots:
53 void initTestCase(); // will be called before the first testfunction is executed.
54 void cleanupTestCase(); // will be called after the last testfunction was executed.
55 void init(); // will be called before each testfunction is executed.
56 void cleanup(); // will be called after every testfunction.
57
58 void testChainFilter();
59 void testChainFilter_data();
60 void testChainFilterFirstInit_data();
61 void testChainFilterFirstInit();
62 void testChainFilterRefreshed();
63 void testChainFilterDeleteForeignKey();
64 void testInvalidRelation();
65 void testSetGetForeignKey();
66 void testIdentifyOnMap();
67 void testAddEntry();
68 void testAddEntryNoGeom();
69 void testDependencies(); // Test relation datasource, id etc. config storage
70 void testSetFilterExpression();
71 void testSetFilterExpressionWithOrClause();
72
73 private:
74 std::unique_ptr<QgsVectorLayer> mLayer1;
75 std::unique_ptr<QgsVectorLayer> mLayer2;
76 std::unique_ptr<QgsRelation> mRelation;
77 QgsMapCanvas *mMapCanvas = nullptr;
78 QgsAdvancedDigitizingDockWidget *mCadWidget = nullptr;
79 };
80
initTestCase()81 void TestQgsRelationReferenceWidget::initTestCase()
82 {
83 QgsApplication::init();
84 QgsApplication::initQgis();
85 QgsGui::editorWidgetRegistry()->initEditors();
86 mMapCanvas = new QgsMapCanvas();
87 mCadWidget = new QgsAdvancedDigitizingDockWidget( mMapCanvas );
88 }
89
cleanupTestCase()90 void TestQgsRelationReferenceWidget::cleanupTestCase()
91 {
92 delete mCadWidget;
93 delete mMapCanvas;
94 QgsApplication::exitQgis();
95 }
96
init()97 void TestQgsRelationReferenceWidget::init()
98 {
99 // create layer
100 mLayer1.reset( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=fk:int" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) ) );
101 QgsProject::instance()->addMapLayer( mLayer1.get(), false, false );
102
103 mLayer2.reset( new QgsVectorLayer( QStringLiteral( "LineString?field=pk:int&field=material:string&field=diameter:int&field=raccord:string" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) ) );
104 mLayer2->setDisplayExpression( QStringLiteral( "pk" ) );
105 QgsProject::instance()->addMapLayer( mLayer2.get(), false, false );
106
107 // create relation
108 mRelation.reset( new QgsRelation() );
109 mRelation->setId( QStringLiteral( "vl1.vl2" ) );
110 mRelation->setName( QStringLiteral( "vl1.vl2" ) );
111 mRelation->setReferencingLayer( mLayer1->id() );
112 mRelation->setReferencedLayer( mLayer2->id() );
113 mRelation->addFieldPair( QStringLiteral( "fk" ), QStringLiteral( "pk" ) );
114 QVERIFY( mRelation->isValid() );
115 QgsProject::instance()->relationManager()->addRelation( *mRelation );
116
117 // add features
118 QgsFeature ft0( mLayer1->fields() );
119 ft0.setAttribute( QStringLiteral( "pk" ), 0 );
120 ft0.setAttribute( QStringLiteral( "fk" ), 0 );
121 mLayer1->startEditing();
122 mLayer1->addFeature( ft0 );
123 mLayer1->commitChanges();
124
125 QgsFeature ft1( mLayer1->fields() );
126 ft1.setAttribute( QStringLiteral( "pk" ), 1 );
127 ft1.setAttribute( QStringLiteral( "fk" ), 1 );
128 mLayer1->startEditing();
129 mLayer1->addFeature( ft1 );
130 mLayer1->commitChanges();
131
132 QgsFeature ft2( mLayer2->fields() );
133 ft2.setAttribute( QStringLiteral( "pk" ), 10 );
134 ft2.setAttribute( QStringLiteral( "material" ), "iron" );
135 ft2.setAttribute( QStringLiteral( "diameter" ), 120 );
136 ft2.setAttribute( QStringLiteral( "raccord" ), "brides" );
137 mLayer2->startEditing();
138 mLayer2->addFeature( ft2 );
139 mLayer2->commitChanges();
140
141 QgsFeature ft3( mLayer2->fields() );
142 ft3.setAttribute( QStringLiteral( "pk" ), 11 );
143 ft3.setAttribute( QStringLiteral( "material" ), "iron" );
144 ft3.setAttribute( QStringLiteral( "diameter" ), 120 );
145 ft3.setAttribute( QStringLiteral( "raccord" ), "sleeve" );
146 mLayer2->startEditing();
147 mLayer2->addFeature( ft3 );
148 mLayer2->commitChanges();
149
150 QgsFeature ft4( mLayer2->fields() );
151 ft4.setAttribute( QStringLiteral( "pk" ), 12 );
152 ft4.setAttribute( QStringLiteral( "material" ), "steel" );
153 ft4.setAttribute( QStringLiteral( "diameter" ), 120 );
154 ft4.setAttribute( QStringLiteral( "raccord" ), "collar" );
155 mLayer2->startEditing();
156 mLayer2->addFeature( ft4 );
157 mLayer2->commitChanges();
158 }
159
cleanup()160 void TestQgsRelationReferenceWidget::cleanup()
161 {
162 QgsProject::instance()->removeMapLayer( mLayer1.get() );
163 QgsProject::instance()->removeMapLayer( mLayer2.get() );
164 }
165
testChainFilter_data()166 void TestQgsRelationReferenceWidget::testChainFilter_data()
167 {
168 QTest::addColumn<bool>( "allowNull" );
169
170 QTest::newRow( "allowNull=true" ) << true;
171 QTest::newRow( "allowNull=false" ) << false;
172 }
173
testChainFilter()174 void TestQgsRelationReferenceWidget::testChainFilter()
175 {
176 QFETCH( bool, allowNull );
177
178 // init a relation reference widget
179 QStringList filterFields = { "material", "diameter", "raccord" };
180
181 QWidget parentWidget;
182 QgsRelationReferenceWidget w( &parentWidget );
183
184 QEventLoop loop;
185 connect( qobject_cast<QgsFeatureFilterModel *>( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit );
186
187 w.setChainFilters( true );
188 w.setFilterFields( filterFields );
189 w.setRelation( *mRelation, allowNull );
190 w.init();
191
192 // check default status for comboboxes
193 QList<QComboBox *> cbs = w.mFilterComboBoxes;
194 QCOMPARE( cbs.count(), 3 );
195 for ( const QComboBox *cb : std::as_const( cbs ) )
196 {
197 if ( cb->currentText() == QLatin1String( "raccord" ) )
198 QCOMPARE( cb->count(), 5 );
199 else if ( cb->currentText() == QLatin1String( "material" ) )
200 QCOMPARE( cb->count(), 4 );
201 else if ( cb->currentText() == QLatin1String( "diameter" ) )
202 QCOMPARE( cb->count(), 3 );
203 }
204
205 loop.exec();
206 QStringList items = getComboBoxItems( w.mComboBox );
207 QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) );
208
209 // set first filter
210 cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "iron" ) ) );
211 loop.exec();
212 QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) );
213
214 cbs[1]->setCurrentIndex( cbs[1]->findText( QStringLiteral( "120" ) ) );
215 loop.exec();
216 QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) );
217
218 for ( const QComboBox *cb : std::as_const( cbs ) )
219 {
220 if ( cb->itemText( 0 ) == QLatin1String( "material" ) )
221 QCOMPARE( cb->count(), 4 );
222 else if ( cb->itemText( 0 ) == QLatin1String( "diameter" ) )
223 QCOMPARE( cb->count(), 2 );
224 else if ( cb->itemText( 0 ) == QLatin1String( "raccord" ) )
225 {
226 QStringList items = getComboBoxItems( cb );
227
228 QCOMPARE( cb->count(), 3 );
229 QCOMPARE( items.contains( "collar" ), false );
230 // collar should not be available in combobox as there's no existing
231 // feature with the filter expression:
232 // "material" == 'iron' AND "diameter" == '120' AND "raccord" = 'collar'
233 }
234 }
235
236
237 // set the filter for "raccord" and then reset filter for "diameter". As
238 // chain filter is activated, the filter on "raccord" field should be reset
239
240 cbs[0]->setCurrentIndex( 0 );
241 loop.exec();
242 QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) );
243 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" );
244
245 if ( allowNull )
246 {
247 w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "10" ) ) );
248 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
249 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" );
250 }
251
252 cbs[0]->setCurrentIndex( cbs[0]->findText( "iron" ) );
253 loop.exec();
254 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
255 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
256
257 // prefer 12 over NULL
258 cbs[0]->setCurrentIndex( cbs[0]->findText( "steel" ) );
259 loop.exec();
260 QCOMPARE( w.mComboBox->currentText(), QString( "12" ) );
261 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" );
262
263 if ( allowNull )
264 {
265 w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "12" ) ) );
266 QCOMPARE( w.mComboBox->currentText(), QString( "12" ) );
267 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" );
268 }
269
270 // reset IRON, prefer 10 over NULL
271 cbs[0]->setCurrentIndex( cbs[0]->findText( "iron" ) );
272 loop.exec();
273 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
274 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
275
276 if ( allowNull )
277 {
278 w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "10" ) ) );
279 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
280 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
281 }
282
283 cbs[1]->setCurrentIndex( cbs[1]->findText( "120" ) );
284 loop.exec();
285 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
286 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
287
288 cbs[2]->setCurrentIndex( cbs[2]->findText( QStringLiteral( "brides" ) ) );
289 loop.exec();
290 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
291 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" );
292
293 cbs[1]->setCurrentIndex( cbs[1]->findText( QStringLiteral( "diameter" ) ) );
294 loop.exec();
295 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
296 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
297
298 // combobox should propose NULL (if allowNull is true), 10 and 11 because the filter is now:
299 // "material" == 'iron'
300 QCOMPARE( w.mComboBox->count(), allowNull ? 3 : 2 );
301
302 // if there's no filter at all, all features' id should be proposed
303 cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "material" ) ) );
304 loop.exec();
305 QCOMPARE( w.mComboBox->count(), allowNull ? 4 : 3 );
306 QCOMPARE( w.mComboBox->currentText(), QString( "10" ) );
307 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" );
308
309 // change item to check that currently selected item remains
310 w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "11" ) ) );
311 cbs[0]->setCurrentIndex( cbs[0]->findText( "iron" ) );
312 loop.exec();
313 QCOMPARE( w.mComboBox->currentText(), QString( "11" ) );
314 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
315
316 // reset all filter
317 cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "material" ) ) );
318 loop.exec();
319 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" );
320
321 // set value with foreign key -> all the comboboxes matches feature values
322 w.setForeignKeys( QVariantList() << "11" );
323 loop.exec();
324 QCOMPARE( cbs[0]->currentText(), QString( "iron" ) );
325 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
326 QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) );
327 QCOMPARE( w.mComboBox->currentText(), QString( "11" ) );
328 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "11" );
329
330 // remove filter on raccord
331 cbs[2]->setCurrentIndex( cbs[2]->findText( "raccord" ) );
332 loop.exec();
333 QCOMPARE( w.mComboBox->currentText(), QString( "11" ) );
334 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
335
336 // change material, prever 12 over NULL
337 cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "steel" ) ) );
338 loop.exec();
339 QCOMPARE( w.mComboBox->currentText(), QString( "12" ) );
340 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" );
341 }
342
testChainFilterFirstInit_data()343 void TestQgsRelationReferenceWidget::testChainFilterFirstInit_data()
344 {
345 QTest::addColumn<bool>( "allowNull" );
346
347 QTest::newRow( "allowNull=true" ) << true;
348 QTest::newRow( "allowNull=false" ) << false;
349 }
350
testChainFilterFirstInit()351 void TestQgsRelationReferenceWidget::testChainFilterFirstInit()
352 {
353 QFETCH( bool, allowNull );
354
355 // init a relation reference widget
356 QStringList filterFields = { "material", "diameter", "raccord" };
357
358 QWidget parentWidget;
359 QgsRelationReferenceWidget w( &parentWidget );
360 w.setChainFilters( true );
361 w.setFilterFields( filterFields );
362 w.setRelation( *mRelation, allowNull );
363 w.init();
364
365 // check default status for comboboxes
366 QList<QComboBox *> cbs = w.mFilterComboBoxes;
367 QCOMPARE( cbs.count(), 3 );
368 for ( const QComboBox *cb : std::as_const( cbs ) )
369 {
370 if ( cb->currentText() == QLatin1String( "raccord" ) )
371 QCOMPARE( cb->count(), 5 );
372 else if ( cb->currentText() == QLatin1String( "material" ) )
373 QCOMPARE( cb->count(), 4 );
374 else if ( cb->currentText() == QLatin1String( "diameter" ) )
375 QCOMPARE( cb->count(), 3 );
376 }
377
378 // set the filter for "raccord" and then reset filter for "diameter". As
379 // chain filter is activated, the filter on "raccord" field should be reset
380 QEventLoop loop;
381 connect( qobject_cast<QgsFeatureFilterModel *>( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit );
382
383 // set value with foreign key -> all the comboboxes matches feature values
384 w.setForeignKeys( QVariantList() << "11" );
385 loop.exec();
386 QCOMPARE( cbs[0]->currentText(), QString( "iron" ) );
387 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
388 QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) );
389 QCOMPARE( w.mComboBox->currentText(), QString( "11" ) );
390 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "11" );
391
392 // remove filter on raccord
393 cbs[2]->setCurrentIndex( cbs[2]->findText( "raccord" ) );
394 loop.exec();
395 QCOMPARE( w.mComboBox->currentText(), QString( "11" ) );
396 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" );
397
398 // change material prever 12 over NULL
399 cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "steel" ) ) );
400 loop.exec();
401 QCOMPARE( w.mComboBox->currentText(), QString( "12" ) );
402 QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" );
403 }
404
405
testChainFilterRefreshed()406 void TestQgsRelationReferenceWidget::testChainFilterRefreshed()
407 {
408 // init a relation reference widget
409 QStringList filterFields = { "material", "diameter", "raccord" };
410
411 QgsRelationReferenceWidget w( new QWidget() );
412 w.setChainFilters( true );
413 w.setFilterFields( filterFields );
414 w.setRelation( *mRelation, true );
415 w.init();
416
417 // check default status for comboboxes
418 QList<QComboBox *> cbs = w.mFilterComboBoxes;
419 QCOMPARE( cbs.count(), 3 );
420 QCOMPARE( cbs[0]->currentText(), QString( "material" ) );
421 QCOMPARE( cbs[1]->currentText(), QString( "diameter" ) );
422 QCOMPARE( cbs[2]->currentText(), QString( "raccord" ) );
423
424 // update foreign key
425 w.setForeignKeys( QVariantList() << QVariant( 12 ) );
426 QCOMPARE( cbs[0]->currentText(), QString( "steel" ) );
427 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
428 QCOMPARE( cbs[2]->currentText(), QString( "collar" ) );
429
430 w.setForeignKeys( QVariantList() << QVariant( 10 ) );
431 QCOMPARE( cbs[0]->currentText(), QString( "iron" ) );
432 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
433 QCOMPARE( cbs[2]->currentText(), QString( "brides" ) );
434
435 w.setForeignKeys( QVariantList() << QVariant( 11 ) );
436 QCOMPARE( cbs[0]->currentText(), QString( "iron" ) );
437 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
438 QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) );
439 }
440
testChainFilterDeleteForeignKey()441 void TestQgsRelationReferenceWidget::testChainFilterDeleteForeignKey()
442 {
443 // init a relation reference widget
444 QStringList filterFields = { "material", "diameter", "raccord" };
445
446 QgsRelationReferenceWidget w( new QWidget() );
447 w.setChainFilters( true );
448 w.setFilterFields( filterFields );
449 w.setRelation( *mRelation, true );
450 w.init();
451
452 // check the default status of filter comboboxes
453 QList<QComboBox *> cbs = w.mFilterComboBoxes;
454
455 QCOMPARE( cbs[0]->currentText(), QString( "material" ) );
456 QCOMPARE( cbs[0]->isEnabled(), true );
457
458 QCOMPARE( cbs[1]->currentText(), QString( "diameter" ) );
459 QCOMPARE( cbs[1]->isEnabled(), false );
460
461 QCOMPARE( cbs[2]->currentText(), QString( "raccord" ) );
462 QCOMPARE( cbs[2]->isEnabled(), false );
463
464 // set a foreign key
465 w.setForeignKeys( QVariantList() << QVariant( 11 ) );
466
467 QCOMPARE( cbs[0]->currentText(), QString( "iron" ) );
468 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
469 QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) );
470
471 // delete the foreign key
472 w.deleteForeignKeys();
473
474 QCOMPARE( cbs[0]->currentText(), QString( "material" ) );
475 QCOMPARE( cbs[0]->isEnabled(), true );
476
477 QCOMPARE( cbs[1]->currentText(), QString( "diameter" ) );
478 QCOMPARE( cbs[1]->isEnabled(), false );
479
480 QCOMPARE( cbs[2]->currentText(), QString( "raccord" ) );
481 QCOMPARE( cbs[2]->isEnabled(), false );
482
483 // set a foreign key
484 w.setForeignKeys( QVariantList() << QVariant( 11 ) );
485
486 QCOMPARE( cbs[0]->currentText(), QString( "iron" ) );
487 QCOMPARE( cbs[1]->currentText(), QString( "120" ) );
488 QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) );
489
490 // set a null foreign key
491 w.setForeignKeys( QVariantList() << QVariant( QVariant::Int ) );
492 QCOMPARE( cbs[0]->currentText(), QString( "material" ) );
493 QCOMPARE( cbs[0]->isEnabled(), true );
494 QCOMPARE( cbs[1]->currentText(), QString( "diameter" ) );
495 QCOMPARE( cbs[1]->isEnabled(), false );
496 QCOMPARE( cbs[2]->currentText(), QString( "raccord" ) );
497 QCOMPARE( cbs[2]->isEnabled(), false );
498 }
499
testInvalidRelation()500 void TestQgsRelationReferenceWidget::testInvalidRelation()
501 {
502 QgsVectorLayer vl( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=fk:int" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
503
504 QgsRelationReferenceWidget editor( new QWidget() );
505
506 // initWidget with an invalid relation
507 QgsRelationReferenceWidgetWrapper ww( &vl, 10, &editor, mMapCanvas, nullptr, nullptr );
508
509 QgsAttributeEditorContext context = ww.context();
510 context.setCadDockWidget( mCadWidget );
511 ww.setContext( context );
512
513 ww.initWidget( nullptr );
514 }
515
testSetGetForeignKey()516 void TestQgsRelationReferenceWidget::testSetGetForeignKey()
517 {
518 QWidget parentWidget;
519 QgsRelationReferenceWidget w( &parentWidget );
520 w.setRelation( *mRelation, true );
521 w.init();
522
523 QSignalSpy spy( &w, &QgsRelationReferenceWidget::foreignKeysChanged );
524
525 w.setForeignKeys( QVariantList() << 0 );
526 QCOMPARE( w.foreignKeys().at( 0 ), QVariant( 0 ) );
527 QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "(0)" ) );
528 QCOMPARE( spy.count(), 1 );
529
530 w.setForeignKeys( QVariantList() << 11 );
531 QCOMPARE( w.foreignKeys().at( 0 ), QVariant( 11 ) );
532 QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "(11)" ) );
533 QCOMPARE( spy.count(), 2 );
534
535 w.setForeignKeys( QVariantList() << 12 );
536 QCOMPARE( w.foreignKeys().at( 0 ), QVariant( 12 ) );
537 QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "(12)" ) );
538 QCOMPARE( spy.count(), 3 );
539
540 w.setForeignKeys( QVariantList() << QVariant() );
541 QVERIFY( w.foreignKeys().at( 0 ).isNull() );
542 QVERIFY( w.foreignKeys().at( 0 ).isValid() );
543 QCOMPARE( spy.count(), 4 );
544 }
545
546 // Test issue https://github.com/qgis/QGIS/issues/29884
547 // Relation reference widget wrong feature when "on map identification"
testIdentifyOnMap()548 void TestQgsRelationReferenceWidget::testIdentifyOnMap()
549 {
550 QWidget parentWidget;
551 QgsRelationReferenceWidget w( &parentWidget );
552 QVERIFY( mLayer1->startEditing() );
553 w.setRelation( *mRelation, true );
554 w.setAllowMapIdentification( true );
555 w.init();
556 QEventLoop loop;
557 // Populate model (I tried to listen to signals but the module reload() runs twice
558 // (the first load triggers a second one which does the population of the combo)
559 // and I haven't fin a way to properly wait for it.
560 QTimer::singleShot( 300, this, [&] { loop.quit(); } );
561 loop.exec();
562 QgsFeature feature;
563 mLayer2->getFeatures( QStringLiteral( "pk = %1" ).arg( 11 ) ).nextFeature( feature );
564 QVERIFY( feature.isValid() );
565 QCOMPARE( feature.attribute( QStringLiteral( "pk" ) ).toInt(), 11 );
566 w.featureIdentified( feature );
567 QCOMPARE( w.mComboBox->currentData( Qt::DisplayRole ).toInt(), 11 );
568
569 mLayer2->getFeatures( QStringLiteral( "pk = %1" ).arg( 10 ) ).nextFeature( feature );
570 QVERIFY( feature.isValid() );
571 QCOMPARE( feature.attribute( QStringLiteral( "pk" ) ).toInt(), 10 );
572 w.featureIdentified( feature );
573 QCOMPARE( w.mComboBox->currentData( Qt::DisplayRole ).toInt(), 10 );
574
575 w.setReadOnlySelector( true );
576 QVERIFY( !w.mComboBox->isEnabled() );
577
578 mLayer1->rollBack();
579 }
580
581 // Monkey patch gui vector layer tool in order to simple add a new feature in
582 // referenced layer
583 class DummyVectorLayerTools : public QgsVectorLayerTools // clazy:exclude=missing-qobject-macro
584 {
addFeature(QgsVectorLayer * layer,const QgsAttributeMap &,const QgsGeometry &,QgsFeature * feat=nullptr) const585 bool addFeature( QgsVectorLayer *layer, const QgsAttributeMap &, const QgsGeometry &, QgsFeature *feat = nullptr ) const override
586 {
587 feat->setAttribute( QStringLiteral( "pk" ), 13 );
588 feat->setAttribute( QStringLiteral( "material" ), QStringLiteral( "steel" ) );
589 feat->setAttribute( QStringLiteral( "diameter" ), 140 );
590 feat->setAttribute( QStringLiteral( "raccord" ), "collar" );
591 layer->addFeature( *feat );
592 return true;
593 }
594
startEditing(QgsVectorLayer *) const595 bool startEditing( QgsVectorLayer * ) const override {return true;}
596
stopEditing(QgsVectorLayer *,bool=true) const597 bool stopEditing( QgsVectorLayer *, bool = true ) const override {return true;}
598
saveEdits(QgsVectorLayer *) const599 bool saveEdits( QgsVectorLayer * ) const override {return true;}
600 };
601
testAddEntry()602 void TestQgsRelationReferenceWidget::testAddEntry()
603 {
604 // check that a new added entry in referenced layer populate correctly the
605 // referencing combobox
606 QgsMapCanvas canvas;
607 QgsRelationReferenceWidget w( &canvas );
608 QVERIFY( mLayer1->startEditing() );
609 w.setRelation( *mRelation, true );
610 w.init();
611
612 QgsAdvancedDigitizingDockWidget cadDockWidget( &canvas );
613 QgsAttributeEditorContext context;
614 DummyVectorLayerTools tools;
615 context.setVectorLayerTools( &tools );
616 context.setCadDockWidget( &cadDockWidget );
617 w.setEditorContext( context, &canvas, nullptr );
618 w.addEntry();
619
620 QVERIFY( w.mCurrentMapTool );
621 QgsFeature feat( mLayer1->fields() );
622 w.mMapToolDigitize->digitized( feat );
623
624 QCOMPARE( w.mComboBox->identifierValues().at( 0 ).toInt(), 13 );
625 }
626
testAddEntryNoGeom()627 void TestQgsRelationReferenceWidget::testAddEntryNoGeom()
628 {
629 QgsVectorLayer mLayer1( QStringLiteral( "Point?crs=epsg:3111&field=pk:int&field=fk:int" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
630 QgsProject::instance()->addMapLayer( &mLayer1, false, false );
631
632 QgsVectorLayer mLayer2( QStringLiteral( "None?field=pk:int&field=material:string" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) );
633 QgsProject::instance()->addMapLayer( &mLayer2, false, false );
634
635 // create relation
636 QgsRelation mRelation;
637 mRelation.setId( QStringLiteral( "vl1.vl2" ) );
638 mRelation.setName( QStringLiteral( "vl1.vl2" ) );
639 mRelation.setReferencingLayer( mLayer1.id() );
640 mRelation.setReferencedLayer( mLayer2.id() );
641 mRelation.addFieldPair( QStringLiteral( "fk" ), QStringLiteral( "pk" ) );
642 QVERIFY( mRelation.isValid() );
643 QgsProject::instance()->relationManager()->addRelation( mRelation );
644
645 // add feature
646 QgsFeature ft0( mLayer1.fields() );
647 ft0.setAttribute( QStringLiteral( "pk" ), 0 );
648 ft0.setAttribute( QStringLiteral( "fk" ), 0 );
649 mLayer1.startEditing();
650 mLayer1.addFeature( ft0 );
651 mLayer1.commitChanges();
652
653 // check that a new added entry in referenced layer populate correctly the
654 // referencing combobox
655 QgsMapCanvas canvas;
656 QgsRelationReferenceWidget w( &canvas );
657 QVERIFY( mLayer1.startEditing() );
658 w.setRelation( mRelation, true );
659 w.init();
660
661 QgsAdvancedDigitizingDockWidget cadDockWidget( &canvas );
662 QgsAttributeEditorContext context;
663 DummyVectorLayerTools tools;
664 context.setVectorLayerTools( &tools );
665 context.setCadDockWidget( &cadDockWidget );
666 w.setEditorContext( context, &canvas, nullptr );
667 w.addEntry();
668
669 QVERIFY( !w.mCurrentMapTool );
670
671 QCOMPARE( w.mComboBox->identifierValues().at( 0 ).toInt(), 13 );
672 }
673
testDependencies()674 void TestQgsRelationReferenceWidget::testDependencies()
675 {
676 QgsVectorLayer mLayer1( QStringLiteral( "Point?crs=epsg:3111&field=pk:int&field=fk:int" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) );
677 QgsProject::instance()->addMapLayer( &mLayer1, false, false );
678
679 QgsVectorLayer mLayer2( QStringLiteral( "None?field=pk:int&field=material:string" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) );
680 QgsProject::instance()->addMapLayer( &mLayer2, false, false );
681
682 // create relation
683 QgsRelation mRelation;
684 mRelation.setId( QStringLiteral( "vl1.vl2" ) );
685 mRelation.setName( QStringLiteral( "vl1.vl2" ) );
686 mRelation.setReferencingLayer( mLayer1.id() );
687 mRelation.setReferencedLayer( mLayer2.id() );
688 mRelation.addFieldPair( QStringLiteral( "fk" ), QStringLiteral( "pk" ) );
689 QVERIFY( mRelation.isValid() );
690 QgsProject::instance()->relationManager()->addRelation( mRelation );
691
692 // check that a new added entry in referenced layer populate correctly
693 // widget config
694 QgsMapCanvas canvas;
695 QgsRelationReferenceWidget w( &canvas );
696 w.setRelation( mRelation, true );
697 w.init();
698
699 QCOMPARE( w.referencedLayerId(), mLayer2.id() );
700 QCOMPARE( w.referencedLayerName(), mLayer2.name() );
701 QCOMPARE( w.referencedLayerDataSource(), mLayer2.publicSource() );
702 QCOMPARE( w.referencedLayerProviderKey(), mLayer2.providerType() );
703
704 }
705
testSetFilterExpression()706 void TestQgsRelationReferenceWidget::testSetFilterExpression()
707 {
708
709 // init a relation reference widget
710 QStringList filterFields = { "material", "diameter", "raccord" };
711
712 QWidget parentWidget;
713 QgsRelationReferenceWidget w( &parentWidget );
714
715 QEventLoop loop;
716 connect( qobject_cast<QgsFeatureFilterModel *>( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit );
717
718 w.setChainFilters( true );
719 w.setFilterFields( filterFields );
720 w.setRelation( *mRelation, true );
721 w.setFilterExpression( QStringLiteral( " \"material\" = 'iron' " ) );
722 w.init();
723
724 loop.exec();
725 QStringList items = getComboBoxItems( w.mComboBox );
726 QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "NULL" ) );
727 // in case there is no filter, the number of filtered features will be 4
728 QCOMPARE( w.mComboBox->count(), 3 );
729 }
730
731
732
testSetFilterExpressionWithOrClause()733 void TestQgsRelationReferenceWidget::testSetFilterExpressionWithOrClause()
734 {
735
736 // init a relation reference widget
737 QStringList filterFields = { "material", "diameter", "raccord" };
738
739 QWidget parentWidget;
740 QgsRelationReferenceWidget w( &parentWidget );
741
742 QEventLoop loop;
743 connect( qobject_cast<QgsFeatureFilterModel *>( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit );
744
745 w.setChainFilters( true );
746 w.setFilterFields( filterFields );
747 w.setRelation( *mRelation, true );
748 w.setFilterExpression( QStringLiteral( " \"raccord\" = 'sleeve' OR FALSE " ) );
749 w.init();
750
751 QStringList items = getComboBoxItems( w.mComboBox );
752
753 loop.exec();
754
755 // in case there is no filter, the number of filtered features will be 4
756 QCOMPARE( w.mComboBox->count(), 2 );
757
758 QList<QComboBox *> cbs = w.mFilterComboBoxes;
759 cbs[0]->setCurrentIndex( cbs[0]->findText( "steel" ) );
760
761 loop.exec();
762
763 QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "NULL" ) );
764 // in case there is no field filter, the number of filtered features will be 2
765 QCOMPARE( w.mComboBox->count(), 1 );
766 }
767
768 QGSTEST_MAIN( TestQgsRelationReferenceWidget )
769 #include "testqgsrelationreferencewidget.moc"
770