1 /***************************************************************************
2      testqgsattributetable.cpp
3      -------------------------
4     Date                 : 2016-02-14
5     Copyright            : (C) 2016 by Nyall Dawson
6     Email                : nyall dot dawson at gmail 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 #include "qgstest.h"
16 #include "qgisapp.h"
17 #include "qgsapplication.h"
18 #include "qgsfeatureiterator.h"
19 #include "qgsvectorlayer.h"
20 #include "qgsfeature.h"
21 #include "qgsgeometry.h"
22 #include "qgsvectordataprovider.h"
23 #include "qgsvectorlayertemporalproperties.h"
24 #include "qgsattributetabledialog.h"
25 #include "qgsproject.h"
26 #include "qgsmapcanvas.h"
27 #include "qgsunittypes.h"
28 #include "qgssettings.h"
29 #include "qgsvectorfilewriter.h"
30 #include "qgsfeaturelistmodel.h"
31 #include "qgsclipboard.h"
32 
33 #include "qgstest.h"
34 
35 /**
36  * \ingroup UnitTests
37  * This is a unit test for the attribute table dialog
38  */
39 class TestQgsAttributeTable : public QObject
40 {
41     Q_OBJECT
42   public:
43     TestQgsAttributeTable();
44 
45   private slots:
46     void initTestCase();// will be called before the first testfunction is executed.
47     void cleanupTestCase();// will be called after the last testfunction was executed.
init()48     void init() {} // will be called before each testfunction is executed.
cleanup()49     void cleanup() {} // will be called after every testfunction.
50     void testRegression15974();
51     void testFieldCalculation();
52     void testFieldCalculationArea();
53     void testNoGeom();
54     void testSelected();
55     void testSelectedOnTop();
56     void testSortByDisplayExpression();
57     void testOrderColumn();
58     void testFilteredFeatures();
59     void testVisibleTemporal();
60     void testCopySelectedRows();
61 
62 
63   private:
64     QgisApp *mQgisApp = nullptr;
65 };
66 
67 TestQgsAttributeTable::TestQgsAttributeTable() = default;
68 
69 //runs before all tests
initTestCase()70 void TestQgsAttributeTable::initTestCase()
71 {
72   qDebug() << "TestQgisAppClipboard::initTestCase()";
73   // init QGIS's paths - true means that all path will be inited from prefix
74   QgsApplication::init();
75   QgsApplication::initQgis();
76   mQgisApp = new QgisApp();
77 
78   // setup the test QSettings environment
79   QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) );
80   QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) );
81   QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST" ) );
82 }
83 
84 //runs after all tests
cleanupTestCase()85 void TestQgsAttributeTable::cleanupTestCase()
86 {
87   QgsApplication::exitQgis();
88 }
89 
testFieldCalculation()90 void TestQgsAttributeTable::testFieldCalculation()
91 {
92   //test field calculation
93 
94   //create a temporary layer
95   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
96   QVERIFY( tempLayer->isValid() );
97   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
98   f1.setAttribute( QStringLiteral( "pk" ), 1 );
99   f1.setAttribute( QStringLiteral( "col1" ), 0.0 );
100   QgsPolylineXY line3111;
101   line3111 << QgsPointXY( 2484588, 2425722 ) << QgsPointXY( 2482767, 2398853 );
102   QgsGeometry line3111G = QgsGeometry::fromPolylineXY( line3111 ) ;
103   f1.setGeometry( line3111G );
104   tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 );
105 
106   // set project CRS and ellipsoid
107   QgsCoordinateReferenceSystem srs( QStringLiteral( "EPSG:3111" ) );
108   QgsProject::instance()->setCrs( srs );
109   QgsProject::instance()->setEllipsoid( QStringLiteral( "WGS84" ) );
110   QgsProject::instance()->setDistanceUnits( QgsUnitTypes::DistanceMeters );
111 
112   // run length calculation
113   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
114   tempLayer->startEditing();
115   dlg->runFieldCalculation( tempLayer.get(), QStringLiteral( "col1" ), QStringLiteral( "$length" ) );
116   tempLayer->commitChanges();
117   // check result
118   QgsFeatureIterator fit = tempLayer->dataProvider()->getFeatures();
119   QgsFeature f;
120   QVERIFY( fit.nextFeature( f ) );
121   double expected = 26932.156;
122   QGSCOMPARENEAR( f.attribute( "col1" ).toDouble(), expected, 0.001 );
123 
124   // change project length unit, check calculation respects unit
125   QgsProject::instance()->setDistanceUnits( QgsUnitTypes::DistanceFeet );
126   std::unique_ptr< QgsAttributeTableDialog > dlg2( new QgsAttributeTableDialog( tempLayer.get() ) );
127   tempLayer->startEditing();
128   dlg2->runFieldCalculation( tempLayer.get(), QStringLiteral( "col1" ), QStringLiteral( "$length" ) );
129   tempLayer->commitChanges();
130   // check result
131   fit = tempLayer->dataProvider()->getFeatures();
132   QVERIFY( fit.nextFeature( f ) );
133   expected = 88360.0918635;
134   QGSCOMPARENEAR( f.attribute( "col1" ).toDouble(), expected, 0.001 );
135 }
136 
testFieldCalculationArea()137 void TestQgsAttributeTable::testFieldCalculationArea()
138 {
139   //test $area field calculation
140 
141   //create a temporary layer
142   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "Polygon?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
143   QVERIFY( tempLayer->isValid() );
144   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
145   f1.setAttribute( QStringLiteral( "pk" ), 1 );
146   f1.setAttribute( QStringLiteral( "col1" ), 0.0 );
147 
148   QgsPolylineXY polygonRing3111;
149   polygonRing3111 << QgsPointXY( 2484588, 2425722 ) << QgsPointXY( 2482767, 2398853 ) << QgsPointXY( 2520109, 2397715 ) << QgsPointXY( 2520792, 2425494 ) << QgsPointXY( 2484588, 2425722 );
150   QgsPolygonXY polygon3111;
151   polygon3111 << polygonRing3111;
152   QgsGeometry polygon3111G = QgsGeometry::fromPolygonXY( polygon3111 );
153   f1.setGeometry( polygon3111G );
154   tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 );
155 
156   // set project CRS and ellipsoid
157   QgsCoordinateReferenceSystem srs( QStringLiteral( "EPSG:3111" ) );
158   QgsProject::instance()->setCrs( srs );
159   QgsProject::instance()->setEllipsoid( QStringLiteral( "WGS84" ) );
160   QgsProject::instance()->setAreaUnits( QgsUnitTypes::AreaSquareMeters );
161 
162   // run area calculation
163   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
164   tempLayer->startEditing();
165   dlg->runFieldCalculation( tempLayer.get(), QStringLiteral( "col1" ), QStringLiteral( "$area" ) );
166   tempLayer->commitChanges();
167   // check result
168   QgsFeatureIterator fit = tempLayer->dataProvider()->getFeatures();
169   QgsFeature f;
170   QVERIFY( fit.nextFeature( f ) );
171   double expected = 1005721496.78008;
172   QGSCOMPARENEAR( f.attribute( "col1" ).toDouble(), expected, 1.0 );
173 
174   // change project area unit, check calculation respects unit
175   QgsProject::instance()->setAreaUnits( QgsUnitTypes::AreaSquareMiles );
176   std::unique_ptr< QgsAttributeTableDialog > dlg2( new QgsAttributeTableDialog( tempLayer.get() ) );
177   tempLayer->startEditing();
178   dlg2->runFieldCalculation( tempLayer.get(), QStringLiteral( "col1" ), QStringLiteral( "$area" ) );
179   tempLayer->commitChanges();
180   // check result
181   fit = tempLayer->dataProvider()->getFeatures();
182   QVERIFY( fit.nextFeature( f ) );
183   expected = 388.311240;
184   QGSCOMPARENEAR( f.attribute( "col1" ).toDouble(), expected, 0.001 );
185 }
186 
testNoGeom()187 void TestQgsAttributeTable::testNoGeom()
188 {
189   QgsSettings s;
190 
191   //test that by default the attribute table DOESN'T fetch geometries (because performance)
192   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
193   QVERIFY( tempLayer->isValid() );
194 
195   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowAll ) );
196 
197   QVERIFY( !dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
198   QVERIFY( dlg->mMainView->masterModel()->request().flags() & QgsFeatureRequest::NoGeometry );
199 
200   // but if we are requesting only visible features, then geometry must be fetched...
201 
202   dlg.reset( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowVisible ) );
203   QVERIFY( dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
204   QVERIFY( !( dlg->mMainView->masterModel()->request().flags() & QgsFeatureRequest::NoGeometry ) );
205 
206   // try changing existing dialog to no geometry mode
207   dlg->mFeatureFilterWidget->filterShowAll();
208   QVERIFY( !dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
209   QVERIFY( dlg->mMainView->masterModel()->request().flags() & QgsFeatureRequest::NoGeometry );
210 
211   // and back to a geometry mode
212   dlg->mFeatureFilterWidget->filterVisible();
213   QVERIFY( dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
214   QVERIFY( !( dlg->mMainView->masterModel()->request().flags() & QgsFeatureRequest::NoGeometry ) );
215 
216 }
217 
testVisibleTemporal()218 void TestQgsAttributeTable::testVisibleTemporal()
219 {
220   // test attribute table opening in show feature visible mode
221   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:4326&field=pk:int&field=col1:date" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
222   QVERIFY( tempLayer->isValid() );
223 
224   QgsPolylineXY line;
225   line << QgsPointXY( 0, 0 ) << QgsPointXY( 1, 1 );
226   QgsGeometry geometry = QgsGeometry::fromPolylineXY( line ) ;
227   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
228   f1.setGeometry( geometry );
229   f1.setAttributes( QgsAttributes() << 1 << QDate( 2020, 1, 1 ) );
230   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
231   f2.setGeometry( geometry );
232   f2.setAttributes( QgsAttributes() << 2 << QDate( 2020, 3, 1 ) );
233   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
234   line.clear();
235   line << QgsPointXY( -3, -3 ) << QgsPointXY( -2, -2 );
236   geometry = QgsGeometry::fromPolylineXY( line );
237   f3.setGeometry( geometry );
238   f3.setAttributes( QgsAttributes() << 3 << QDate( 2020, 1, 1 ) );
239   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
240 
241   QgsVectorLayerTemporalProperties *temporalProperties = qobject_cast< QgsVectorLayerTemporalProperties *>( tempLayer->temporalProperties() );
242   temporalProperties->setIsActive( true );
243   temporalProperties->setMode( QgsVectorLayerTemporalProperties::ModeFeatureDateTimeStartAndEndFromFields );
244   temporalProperties->setStartField( QStringLiteral( "col1" ) );
245 
246   mQgisApp->mapCanvas()->setDestinationCrs( QgsCoordinateReferenceSystem( "EPSG:4326" ) );
247   mQgisApp->mapCanvas()->resize( 500, 500 );
248   mQgisApp->mapCanvas()->setLayers( QList< QgsMapLayer *>() << tempLayer.get() );
249   mQgisApp->mapCanvas()->setExtent( QgsRectangle( -1, -1, 1, 1 ) );
250   mQgisApp->mapCanvas()->setTemporalRange( QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), QDateTime( QDate( 2020, 2, 1 ) ) ) );
251 
252   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowVisible ) );
253 
254   // feature id 2 is filtered out due to being out of temporal range
255   // feature id 3 is filtered out due to being out of visible extent
256   QCOMPARE( dlg->mMainView->filteredFeatures(), QgsFeatureIds() << 1 );
257 }
258 
testSelected()259 void TestQgsAttributeTable::testSelected()
260 {
261   // test attribute table opening in show selected mode
262   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
263   QVERIFY( tempLayer->isValid() );
264 
265   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
266   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
267   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
268   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
269 
270   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowSelected ) );
271 
272   QVERIFY( !dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
273   //should be nothing - because no selection!
274   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
275   QVERIFY( dlg->mMainView->masterModel()->request().filterFids().isEmpty() );
276 
277   // make a selection
278   tempLayer->selectByIds( QgsFeatureIds() << 1 << 3 );
279   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
280   QCOMPARE( dlg->mMainView->masterModel()->request().filterFids(), QgsFeatureIds() << 1 << 3 );
281 
282   // another test - start with selection when dialog created
283   dlg.reset( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowSelected ) );
284   QVERIFY( !dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
285   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
286   QCOMPARE( dlg->mMainView->masterModel()->request().filterFids(), QgsFeatureIds() << 1 << 3 );
287   // remove selection
288   tempLayer->removeSelection();
289   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
290   QVERIFY( dlg->mMainView->masterModel()->request().filterFids().isEmpty() );
291 }
292 
testSelectedOnTop()293 void TestQgsAttributeTable::testSelectedOnTop()
294 {
295   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
296   QVERIFY( tempLayer->isValid() );
297 
298   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
299   f1.setAttribute( 0, 1 );
300   f1.setAttribute( 1, 3.2 );
301   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
302   f2.setAttribute( 0, 2 );
303   f2.setAttribute( 1, 1.8 );
304   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
305   f3.setAttribute( 0, 3 );
306   f3.setAttribute( 1, 5.0 );
307   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
308 
309   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
310 
311   dlg->mMainView->setSortExpression( "pk" );
312   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
313   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
314   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
315 
316   tempLayer->selectByIds( QgsFeatureIds() << 2 );
317   dlg->mMainView->setSelectedOnTop( true );
318 
319   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
320   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
321   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
322 
323   dlg->mMainView->setSelectedOnTop( false );
324 
325   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
326   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
327   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
328 
329   tempLayer->selectByIds( QgsFeatureIds() << 3 );
330 
331   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
332   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
333   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
334 
335   dlg->mMainView->setSelectedOnTop( true );
336 
337   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
338   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
339   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
340 
341 }
342 
testSortByDisplayExpression()343 void TestQgsAttributeTable::testSortByDisplayExpression()
344 {
345   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
346   QVERIFY( tempLayer->isValid() );
347 
348   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
349   f1.setAttribute( 0, 1 );
350   f1.setAttribute( 1, 3.2 );
351   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
352   f2.setAttribute( 0, 2 );
353   f2.setAttribute( 1, 1.8 );
354   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
355   f3.setAttribute( 0, 3 );
356   f3.setAttribute( 1, 5.0 );
357   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
358 
359   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
360 
361   dlg->mMainView->mFeatureListView->setDisplayExpression( "pk" );
362   QgsFeatureListModel *listModel = dlg->mMainView->mFeatureListModel;
363   QCOMPARE( listModel->rowCount(), 3 );
364 
365   QCOMPARE( listModel->index( 0, 0 ).data( Qt::DisplayRole ), QVariant( 1 ) );
366   QCOMPARE( listModel->index( 1, 0 ).data( Qt::DisplayRole ), QVariant( 2 ) );
367   QCOMPARE( listModel->index( 2, 0 ).data( Qt::DisplayRole ), QVariant( 3 ) );
368 
369   dlg->mMainView->mFeatureListView->setDisplayExpression( "col1" );
370   QCOMPARE( listModel->index( 0, 0 ).data( Qt::DisplayRole ), QVariant( 1.8 ) );
371   QCOMPARE( listModel->index( 1, 0 ).data( Qt::DisplayRole ), QVariant( 3.2 ) );
372   QCOMPARE( listModel->index( 2, 0 ).data( Qt::DisplayRole ), QVariant( 5.0 ) );
373 }
374 
testRegression15974()375 void TestQgsAttributeTable::testRegression15974()
376 {
377   // Test duplicated rows in attribute table + two crashes.
378   QString path = QDir::tempPath() + "/testshp15974.shp";
379   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "polygon?crs=epsg:4326&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
380   QVERIFY( tempLayer->isValid() );
381   QgsVectorFileWriter::SaveVectorOptions saveOptions;
382   saveOptions.fileEncoding = QStringLiteral( "system" );
383   saveOptions.driverName = QStringLiteral( "ESRI Shapefile" );
384   QgsVectorFileWriter::writeAsVectorFormatV2( tempLayer.get(), path, tempLayer->transformContext(), saveOptions );
385   std::unique_ptr< QgsVectorLayer> shpLayer( new QgsVectorLayer( path, QStringLiteral( "test" ),  QStringLiteral( "ogr" ) ) );
386   QgsFeature f1( shpLayer->dataProvider()->fields(), 1 );
387   QgsGeometry geom;
388   geom = QgsGeometry().fromWkt( QStringLiteral( "polygon((0 0, 0 1, 1 1, 1 0, 0 0))" ) );
389   QVERIFY( geom.isGeosValid() );
390   f1.setGeometry( geom );
391   QgsFeature f2( shpLayer->dataProvider()->fields(), 2 );
392   f2.setGeometry( geom );
393   QgsFeature f3( shpLayer->dataProvider()->fields(), 3 );
394   f3.setGeometry( geom );
395   QVERIFY( shpLayer->startEditing() );
396   QVERIFY( shpLayer->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
397   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( shpLayer.get() ) );
398   QCOMPARE( shpLayer->featureCount(), 3L );
399   mQgisApp->saveEdits( shpLayer.get() );
400   QCOMPARE( shpLayer->featureCount(), 3L );
401   QCOMPARE( dlg->mMainView->masterModel()->rowCount(), 3 );
402   QCOMPARE( dlg->mMainView->mLayerCache->cachedFeatureIds().count(), 3 );
403   QCOMPARE( dlg->mMainView->featureCount(), 3 );
404   // All the following instructions made the test pass, before the connections to invalidate()
405   // were introduced in QgsDualView::initModels
406   // dlg->mMainView->mFilterModel->setSourceModel( dlg->mMainView->masterModel() );
407   // dlg->mMainView->mFilterModel->invalidate();
408   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 );
409 }
410 
testOrderColumn()411 void TestQgsAttributeTable::testOrderColumn()
412 {
413   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:int&field=col2:int" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
414   QVERIFY( tempLayer->isValid() );
415 
416   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
417   f1.setAttribute( 0, 1 );
418   f1.setAttribute( 1, 13 );
419   f1.setAttribute( 2, 7 );
420   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 ) );
421 
422   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
423 
424   // Issue https://github.com/qgis/QGIS/issues/28493
425   // When we reorder column (last column becomes first column), and we select an entire row
426   // the currentIndex is no longer the first column, and consequently it breaks edition
427 
428   QgsAttributeTableConfig config = QgsAttributeTableConfig();
429   config.update( tempLayer->dataProvider()->fields() );
430   QVector<QgsAttributeTableConfig::ColumnConfig> columns = config.columns();
431 
432   // move last column in first position
433   columns.move( 2, 0 );
434   config.setColumns( columns );
435 
436   dlg->mMainView->setAttributeTableConfig( config );
437 
438   QgsAttributeTableFilterModel *filterModel = static_cast<QgsAttributeTableFilterModel *>( dlg->mMainView->mTableView->model() );
439   filterModel->sort( 0, Qt::AscendingOrder );
440 
441   QModelIndex index = filterModel->mapToSource( filterModel->sourceModel()->index( 0, 0 ) );
442   QCOMPARE( index.row(), 0 );
443   QCOMPARE( index.column(), 2 );
444 
445   index = filterModel->mapFromSource( filterModel->sourceModel()->index( 0, 0 ) );
446   QCOMPARE( index.row(), 0 );
447   QCOMPARE( index.column(), 1 );
448 
449   qDebug() << filterModel->mapFromSource( filterModel->sourceModel()->index( 0, 0 ) );
450 
451   // column 0 is indeed column 2 since we move it
452   QCOMPARE( filterModel->sortColumn(), 2 );
453 }
454 
testFilteredFeatures()455 void TestQgsAttributeTable::testFilteredFeatures()
456 {
457   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:int&field=col2:int" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
458   QVERIFY( tempLayer->isValid() );
459 
460   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
461   f1.setAttribute( 0, 1 );
462   f1.setAttribute( 1, 2 );
463   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
464   f2.setAttribute( 0, 2 );
465   f2.setAttribute( 1, 4 );
466   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
467   f3.setAttribute( 0, 3 );
468   f3.setAttribute( 1, 6 );
469   QgsFeature f4( tempLayer->dataProvider()->fields(), 4 );
470   f4.setAttribute( 0, 4 );
471   f4.setAttribute( 1, 8 );
472 
473   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
474 
475   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowAll ) );
476 
477   QEventLoop loop;
478   connect( qobject_cast<QgsAttributeTableFilterModel *>( dlg->mMainView->mFilterModel ), &QgsAttributeTableFilterModel::featuresFiltered, &loop, &QEventLoop::quit );
479 
480   // show all (three features)
481   dlg->mFeatureFilterWidget->filterShowAll();
482   QCOMPARE( dlg->mMainView->featureCount(), 3 );
483   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 );
484 
485   // add a feature
486   tempLayer->startEditing();
487   QVERIFY( tempLayer->addFeatures( QgsFeatureList() << f4 ) );
488   //still show all (four features)
489   QCOMPARE( tempLayer->featureCount(), 4L );
490   QCOMPARE( dlg->mMainView->featureCount(), 4 );
491   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 4 );
492 
493   // bigger 5 (two of four features)
494   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>5" ), QgsAttributeForm::ReplaceFilter, true );
495   QCOMPARE( dlg->mMainView->featureCount(), 4 );
496   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 );
497   // bigger 7 (one of four features)
498   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>7" ), QgsAttributeForm::ReplaceFilter, true );
499   QCOMPARE( dlg->mMainView->featureCount(), 4 );
500   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 1 );
501   // bigger 9 (no of four features)
502   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>9" ), QgsAttributeForm::ReplaceFilter, true );
503   QCOMPARE( dlg->mMainView->featureCount(), 4 );
504   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 0 );
505 
506   //add two features
507   QgsFeature f5( tempLayer->dataProvider()->fields(), 5 );
508   f5.setAttribute( 0, 5 );
509   f5.setAttribute( 1, 10 );
510   QgsFeature f6( tempLayer->dataProvider()->fields(), 6 );
511   f6.setAttribute( 0, 6 );
512   f6.setAttribute( 1, 12 );
513   QVERIFY( tempLayer->addFeatures( QgsFeatureList() << f5 << f6 ) );
514   tempLayer->commitChanges();
515   loop.exec();
516   //no filter change -> now two of six features
517   QCOMPARE( dlg->mMainView->featureCount(), 6 );
518   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 );
519 
520   //remove a feature not affecting the filter
521   tempLayer->startEditing();
522   QVERIFY( tempLayer->deleteFeature( f2.id() ) );
523   //no filter change -> now two of five features
524   QCOMPARE( dlg->mMainView->featureCount(), 5 );
525   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 );
526 
527   //remove a feature affecting the filter
528   QVERIFY( tempLayer->deleteFeature( f5.id() ) );
529   //no filter change -> now one of four features
530   QCOMPARE( dlg->mMainView->featureCount(), 4 );
531   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 1 );
532 
533   // smaller 11 (three of four features)
534   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1<11" ), QgsAttributeForm::ReplaceFilter, true );
535   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 );
536 }
537 
testCopySelectedRows()538 void TestQgsAttributeTable::testCopySelectedRows()
539 {
540   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:int&field=col2:int" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
541   QVERIFY( tempLayer->isValid() );
542 
543   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
544   f1.setAttribute( 0, 1 );
545   f1.setAttribute( 1, 2 );
546 
547   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
548   f2.setAttribute( 0, 2 );
549   f2.setAttribute( 1, 4 );
550 
551   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) );
552 
553   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowAll ) );
554 
555   tempLayer->selectByIds( QgsFeatureIds() << 1 << 2 );
556 
557   dlg->mActionCopySelectedRows_triggered();
558 
559   QgsClipboard *clipboard = QgisApp::instance()->clipboard();
560   QVERIFY( clipboard );
561   QVERIFY( !clipboard->isEmpty() );
562   QCOMPARE( clipboard->fields().names(), QStringList() << "pk" << "col1" << "col2" );
563 
564   QgsFeatureList features = clipboard->copyOf();
565   QCOMPARE( features.count(), 2 );
566   QCOMPARE( features.at( 0 ).attribute( 0 ), QVariant( 1 ) );
567   QCOMPARE( features.at( 0 ).attribute( "col1" ), QVariant( 2 ) );
568   QCOMPARE( features.at( 0 ).attribute( "col2" ), QVariant() );
569   QCOMPARE( features.at( 1 ).attribute( "pk" ), QVariant( 2 ) );
570   QCOMPARE( features.at( 1 ).attribute( "col1" ), QVariant( 4 ) );
571   QCOMPARE( features.at( 1 ).attribute( 2 ), QVariant() );
572 
573   QCOMPARE( clipboard->crs().authid(), QStringLiteral( "EPSG:3111" ) );
574 }
575 
576 QGSTEST_MAIN( TestQgsAttributeTable )
577 #include "testqgsattributetable.moc"
578