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