1 /***************************************************************************
2     testqgsdualview.cpp
3      --------------------------------------
4     Date                 : 14.2.2013
5     Copyright            : (C) 2013 Matthias Kuhn
6     Email                : matthias at opengis dot ch
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 
19 #include <editorwidgets/core/qgseditorwidgetregistry.h>
20 #include <attributetable/qgsattributetableview.h>
21 #include <attributetable/qgsdualview.h>
22 #include <editform/qgsattributeeditorhtmlelement.h>
23 #include "qgsattributeform.h"
24 #include <qgsapplication.h>
25 #include "qgsfeatureiterator.h"
26 #include <qgsvectorlayer.h>
27 #include "qgsvectordataprovider.h"
28 #include <qgsmapcanvas.h>
29 #include <qgsfeature.h>
30 #include "qgsgui.h"
31 #include "qgsvectorlayercache.h"
32 #include "qgstest.h"
33 
34 class TestQgsDualView : public QObject
35 {
36     Q_OBJECT
37   public:
38     TestQgsDualView() = default;
39 
40   private slots:
41     void initTestCase(); // will be called before the first testfunction is executed.
42     void cleanupTestCase(); // will be called after the last testfunction was executed.
43     void init(); // will be called before each testfunction is executed.
44     void cleanup(); // will be called after every testfunction.
45 
46     void testColumnCount();
47 
48     void testColumnHeaders();
49 
50     void testData();
51     void testAttributeTableConfig();
52     void testFilterSelected();
53 
54     void testSelectAll();
55 
56     void testSort();
57 
58     void testAttributeFormSharedValueScanning();
59     void testNoGeom();
60 
61     void testHtmlWidget_data();
62     void testHtmlWidget();
63 
64   private:
65     QgsMapCanvas *mCanvas = nullptr;
66     QgsVectorLayer *mPointsLayer = nullptr;
67     QString mTestDataDir;
68     QgsDualView *mDualView = nullptr;
69 };
70 
initTestCase()71 void TestQgsDualView::initTestCase()
72 {
73   QgsApplication::init();
74   QgsApplication::initQgis();
75   QgsApplication::showSettings();
76 
77   QgsGui::editorWidgetRegistry()->initEditors();
78 
79   // Setup a map canvas with a vector layer loaded...
80   const QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
81   mTestDataDir = myDataDir + '/';
82 
83   //
84   // load a vector layer
85   //
86   const QString myPointsFileName = mTestDataDir + "points.shp";
87   const QFileInfo myPointFileInfo( myPointsFileName );
88   mPointsLayer = new QgsVectorLayer( myPointFileInfo.filePath(),
89                                      myPointFileInfo.completeBaseName(), QStringLiteral( "ogr" ) );
90 
91   mCanvas = new QgsMapCanvas();
92 }
93 
cleanupTestCase()94 void TestQgsDualView::cleanupTestCase()
95 {
96   delete mPointsLayer;
97   delete mCanvas;
98   QgsApplication::exitQgis();
99 }
100 
init()101 void TestQgsDualView::init()
102 {
103   mDualView = new QgsDualView();
104   mDualView->init( mPointsLayer, mCanvas );
105 }
106 
cleanup()107 void TestQgsDualView::cleanup()
108 {
109   delete mDualView;
110 }
111 
testColumnCount()112 void TestQgsDualView::testColumnCount()
113 {
114   QCOMPARE( mDualView->tableView()->model()->columnCount(), mPointsLayer->fields().count() );
115 }
116 
testColumnHeaders()117 void TestQgsDualView::testColumnHeaders()
118 {
119   for ( int i = 0; i < mPointsLayer->fields().count(); ++i )
120   {
121     const QgsField fld = mPointsLayer->fields().at( i );
122     QCOMPARE( mDualView->tableView()->model()->headerData( i, Qt::Horizontal ).toString(), fld.name() );
123   }
124 }
125 
testData()126 void TestQgsDualView::testData()
127 {
128   QgsFeature feature;
129   mPointsLayer->getFeatures( QgsFeatureRequest().setFilterFid( 0 ) ).nextFeature( feature );
130 
131   for ( int i = 0; i < mPointsLayer->fields().count(); ++i )
132   {
133     const QgsField fld = mPointsLayer->fields().at( i );
134 
135     const QModelIndex index = mDualView->tableView()->model()->index( 0, i );
136     QCOMPARE( mDualView->tableView()->model()->data( index ).toString(), fld.displayString( feature.attribute( i ) ) );
137   }
138 }
139 
testAttributeTableConfig()140 void TestQgsDualView::testAttributeTableConfig()
141 {
142   QCOMPARE( mDualView->attributeTableConfig().columns().count(), mPointsLayer->attributeTableConfig().columns().count() );
143 }
144 
testFilterSelected()145 void TestQgsDualView::testFilterSelected()
146 {
147   QgsFeature feature;
148   QList< QgsFeatureId > ids;
149   QgsFeatureIterator it = mPointsLayer->getFeatures( QgsFeatureRequest().setOrderBy( QgsFeatureRequest::OrderBy() << QgsFeatureRequest::OrderByClause( QStringLiteral( "Heading" ) ) ) );
150   while ( it.nextFeature( feature ) )
151     ids << feature.id();
152 
153   // select some features
154   QList< QgsFeatureId > selected;
155   selected << ids.at( 1 ) << ids.at( 3 );
156   mPointsLayer->selectByIds( qgis::listToSet( selected ) );
157 
158   mDualView->setFilterMode( QgsAttributeTableFilterModel::ShowSelected );
159   QCOMPARE( mDualView->tableView()->model()->rowCount(), 2 );
160 
161   const int headingIdx = mPointsLayer->fields().lookupField( QStringLiteral( "Heading" ) );
162   const QgsField fld = mPointsLayer->fields().at( headingIdx );
163   for ( int i = 0; i < selected.count(); ++i )
164   {
165     mPointsLayer->getFeatures( QgsFeatureRequest().setFilterFid( selected.at( i ) ) ).nextFeature( feature );
166     const QModelIndex index = mDualView->tableView()->model()->index( i, headingIdx );
167     QCOMPARE( mDualView->tableView()->model()->data( index ).toString(), fld.displayString( feature.attribute( headingIdx ) ) );
168   }
169 
170   // select none
171   mPointsLayer->removeSelection();
172   QCOMPARE( mDualView->tableView()->model()->rowCount(), 0 );
173 }
174 
testSelectAll()175 void TestQgsDualView::testSelectAll()
176 {
177 
178   QEventLoop loop;
179   connect( qobject_cast<QgsAttributeTableFilterModel *>( mDualView->mFilterModel ), &QgsAttributeTableFilterModel::visibleReloaded, &loop, &QEventLoop::quit );
180   mDualView->setFilterMode( QgsAttributeTableFilterModel::ShowVisible );
181   // Only show parts of the canvas, so only one selected feature is visible
182   mCanvas->setExtent( QgsRectangle( -139, 23, -100, 48 ) );
183   loop.exec();
184   mDualView->mTableView->selectAll();
185   QCOMPARE( mPointsLayer->selectedFeatureCount(), 10 );
186 
187   mPointsLayer->selectByIds( QgsFeatureIds() );
188   mCanvas->setExtent( QgsRectangle( -110, 40, -100, 48 ) );
189   loop.exec();
190   mDualView->mTableView->selectAll();
191   QCOMPARE( mPointsLayer->selectedFeatureCount(), 1 );
192 }
193 
testSort()194 void TestQgsDualView::testSort()
195 {
196   mDualView->setSortExpression( QStringLiteral( "Class" ) );
197 
198   QStringList classes;
199   classes << QStringLiteral( "B52" )
200           << QStringLiteral( "B52" )
201           << QStringLiteral( "B52" )
202           << QStringLiteral( "B52" )
203           << QStringLiteral( "Biplane" )
204           << QStringLiteral( "Biplane" )
205           << QStringLiteral( "Biplane" )
206           << QStringLiteral( "Biplane" )
207           << QStringLiteral( "Biplane" )
208           << QStringLiteral( "Jet" )
209           << QStringLiteral( "Jet" )
210           << QStringLiteral( "Jet" )
211           << QStringLiteral( "Jet" )
212           << QStringLiteral( "Jet" )
213           << QStringLiteral( "Jet" )
214           << QStringLiteral( "Jet" )
215           << QStringLiteral( "Jet" );
216 
217   for ( int i = 0; i < classes.length(); ++i )
218   {
219     const QModelIndex index = mDualView->tableView()->model()->index( i, 0 );
220     QCOMPARE( mDualView->tableView()->model()->data( index ).toString(), classes.at( i ) );
221   }
222 
223   QStringList headings;
224   headings << QStringLiteral( "0" )
225            <<  QStringLiteral( "0" )
226            <<  QStringLiteral( "12" )
227            <<  QStringLiteral( "34" )
228            <<  QStringLiteral( "80" )
229            <<  QStringLiteral( "85" )
230            <<  QStringLiteral( "90" )
231            <<  QStringLiteral( "90" )
232            <<  QStringLiteral( "95" )
233            <<  QStringLiteral( "100" )
234            <<  QStringLiteral( "140" )
235            <<  QStringLiteral( "160" )
236            <<  QStringLiteral( "180" )
237            <<  QStringLiteral( "240" )
238            <<  QStringLiteral( "270" )
239            <<  QStringLiteral( "300" )
240            <<  QStringLiteral( "340" );
241 
242   mDualView->setSortExpression( QStringLiteral( "Heading" ) );
243 
244   for ( int i = 0; i < headings.length(); ++i )
245   {
246     const QModelIndex index = mDualView->tableView()->model()->index( i, 1 );
247     QCOMPARE( mDualView->tableView()->model()->data( index ).toString(), headings.at( i ) );
248   }
249 }
250 
testAttributeFormSharedValueScanning()251 void TestQgsDualView::testAttributeFormSharedValueScanning()
252 {
253   // test QgsAttributeForm::scanForEqualAttributes
254 
255   QSet< int > mixedValueFields;
256   QHash< int, QVariant > fieldSharedValues;
257 
258   // make a temporary layer to check through
259   QgsVectorLayer *layer = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer&field=col2:integer&field=col3:integer&field=col4:integer" ), QStringLiteral( "test" ), QStringLiteral( "memory" ) );
260   QVERIFY( layer->isValid() );
261   QgsFeature f1( layer->dataProvider()->fields(), 1 );
262   f1.setAttribute( QStringLiteral( "col1" ), 1 );
263   f1.setAttribute( QStringLiteral( "col2" ), 1 );
264   f1.setAttribute( QStringLiteral( "col3" ), 3 );
265   f1.setAttribute( QStringLiteral( "col4" ), 1 );
266   QgsFeature f2( layer->dataProvider()->fields(), 2 );
267   f2.setAttribute( QStringLiteral( "col1" ), 1 );
268   f2.setAttribute( QStringLiteral( "col2" ), 2 );
269   f2.setAttribute( QStringLiteral( "col3" ), 3 );
270   f2.setAttribute( QStringLiteral( "col4" ), 2 );
271   QgsFeature f3( layer->dataProvider()->fields(), 3 );
272   f3.setAttribute( QStringLiteral( "col1" ), 1 );
273   f3.setAttribute( QStringLiteral( "col2" ), 2 );
274   f3.setAttribute( QStringLiteral( "col3" ), 3 );
275   f3.setAttribute( QStringLiteral( "col4" ), 2 );
276   QgsFeature f4( layer->dataProvider()->fields(), 4 );
277   f4.setAttribute( QStringLiteral( "col1" ), 1 );
278   f4.setAttribute( QStringLiteral( "col2" ), 1 );
279   f4.setAttribute( QStringLiteral( "col3" ), 3 );
280   f4.setAttribute( QStringLiteral( "col4" ), 2 );
281   layer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 << f4 );
282 
283   const QgsAttributeForm form( layer );
284 
285   QgsFeatureIterator it = layer->getFeatures();
286 
287   form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
288 
289   QCOMPARE( mixedValueFields, QSet< int >() << 1 << 3 );
290   QCOMPARE( fieldSharedValues.value( 0 ).toInt(), 1 );
291   QCOMPARE( fieldSharedValues.value( 2 ).toInt(), 3 );
292 
293   // add another feature so all attributes are different
294   QgsFeature f5( layer->dataProvider()->fields(), 5 );
295   f5.setAttribute( QStringLiteral( "col1" ), 11 );
296   f5.setAttribute( QStringLiteral( "col2" ), 11 );
297   f5.setAttribute( QStringLiteral( "col3" ), 13 );
298   f5.setAttribute( QStringLiteral( "col4" ), 12 );
299   layer->dataProvider()->addFeatures( QgsFeatureList() << f5 );
300 
301   it = layer->getFeatures();
302 
303   form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
304   QCOMPARE( mixedValueFields, QSet< int >() << 0 << 1 << 2 << 3 );
305   QVERIFY( fieldSharedValues.isEmpty() );
306 
307   // single feature, all attributes should be shared
308   it = layer->getFeatures( QgsFeatureRequest().setFilterFid( 4 ) );
309   form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
310   QCOMPARE( fieldSharedValues.value( 0 ).toInt(), 1 );
311   QCOMPARE( fieldSharedValues.value( 1 ).toInt(), 1 );
312   QCOMPARE( fieldSharedValues.value( 2 ).toInt(), 3 );
313   QCOMPARE( fieldSharedValues.value( 3 ).toInt(), 2 );
314   QVERIFY( mixedValueFields.isEmpty() );
315 }
316 
testNoGeom()317 void TestQgsDualView::testNoGeom()
318 {
319   //test that both the master model and cache for the dual view either both request geom or both don't request geom
320   std::unique_ptr< QgsDualView > dv( new QgsDualView() );
321 
322   // request with geometry
323   QgsFeatureRequest req;
324   dv->init( mPointsLayer, mCanvas, req );
325   // check that both master model AND cache are using geometry
326   QgsAttributeTableModel *model = dv->masterModel();
327   QVERIFY( model->layerCache()->cacheGeometry() );
328   QVERIFY( !( model->request().flags() & QgsFeatureRequest::NoGeometry ) );
329 
330   // request with NO geometry, but using filter rect (which should override and request geom)
331   req = QgsFeatureRequest().setFilterRect( QgsRectangle( 1, 2, 3, 4 ) );
332   dv.reset( new QgsDualView() );
333   dv->init( mPointsLayer, mCanvas, req );
334   model = dv->masterModel();
335   QVERIFY( model->layerCache()->cacheGeometry() );
336   QVERIFY( !( model->request().flags() & QgsFeatureRequest::NoGeometry ) );
337 
338   // request with NO geometry
339   req = QgsFeatureRequest().setFlags( QgsFeatureRequest::NoGeometry );
340   dv.reset( new QgsDualView() );
341   dv->init( mPointsLayer, mCanvas, req );
342   model = dv->masterModel();
343   QVERIFY( !model->layerCache()->cacheGeometry() );
344   QVERIFY( ( model->request().flags() & QgsFeatureRequest::NoGeometry ) );
345 }
346 
testHtmlWidget_data()347 void TestQgsDualView::testHtmlWidget_data()
348 {
349   QTest::addColumn<QString>( "expression" );
350   QTest::addColumn<bool>( "expectedCacheGeometry" );
351 
352   QTest::newRow( "with-geometry" ) << "geom_to_wkt($geometry)" << true;
353   QTest::newRow( "without-geometry" ) << "2+pk" << false;
354 }
355 
testHtmlWidget()356 void TestQgsDualView::testHtmlWidget()
357 {
358   // check that HTML widget set cache geometry when needed
359 
360   QFETCH( QString, expression );
361   QFETCH( bool, expectedCacheGeometry );
362 
363   QgsVectorLayer layer( QStringLiteral( "Point?crs=epsg:4326&field=pk:int" ), QStringLiteral( "layer" ), QStringLiteral( "memory" ) );
364   QgsProject::instance()->addMapLayer( &layer, false, false );
365   QgsFeature f( layer.fields() );
366   f.setAttribute( QStringLiteral( "pk" ), 1 );
367   f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT(0.5 0.5)" ) ) );
368   QVERIFY( f.isValid() );
369   QVERIFY( f.geometry().isGeosValid() );
370   QVERIFY( layer.dataProvider()->addFeature( f ) );
371 
372   QgsEditFormConfig editFormConfig = layer.editFormConfig();
373   editFormConfig.clearTabs();
374   QgsAttributeEditorHtmlElement *htmlElement = new QgsAttributeEditorHtmlElement( "HtmlWidget", nullptr );
375   htmlElement->setHtmlCode( QStringLiteral( "The text is '<script>document.write(expression.evaluate(\"%1\"));</script>'" ).arg( expression ) );
376   editFormConfig.addTab( htmlElement );
377   editFormConfig.setLayout( QgsEditFormConfig::TabLayout );
378   layer.setEditFormConfig( editFormConfig );
379 
380   QgsFeatureRequest request;
381   request.setFlags( QgsFeatureRequest::NoGeometry );
382 
383   QgsDualView dualView;
384   dualView.setView( QgsDualView::AttributeEditor );
385   dualView.init( &layer, mCanvas, request );
386   QCOMPARE( dualView.mLayerCache->cacheGeometry(), expectedCacheGeometry );
387 
388   QgsProject::instance()->removeMapLayer( &layer );
389 }
390 
391 QGSTEST_MAIN( TestQgsDualView )
392 #include "testqgsdualview.moc"
393