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 #include "qgsvectorlayercache.h"
33 
34 /**
35  * \ingroup UnitTests
36  * This is a unit test for the attribute table dialog
37  */
38 class TestQgsAttributeTable : public QObject
39 {
40     Q_OBJECT
41   public:
42     TestQgsAttributeTable();
43 
44   private slots:
45     void initTestCase();// will be called before the first testfunction is executed.
46     void cleanupTestCase();// will be called after the last testfunction was executed.
init()47     void init() {} // will be called before each testfunction is executed.
cleanup()48     void cleanup() {} // will be called after every testfunction.
49     void testRegression15974();
50     void testFieldCalculation();
51     void testFieldCalculationArea();
52     void testNoGeom();
53     void testSelected();
54     void testEdited();
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   const QgsGeometry line3111G = QgsGeometry::fromPolylineXY( line3111 ) ;
103   f1.setGeometry( line3111G );
104   tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 );
105 
106   // set project CRS and ellipsoid
107   const 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   const QgsGeometry polygon3111G = QgsGeometry::fromPolygonXY( polygon3111 );
153   f1.setGeometry( polygon3111G );
154   tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 );
155 
156   // set project CRS and ellipsoid
157   const 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 = 1005755617.819130;
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.324420;
184   QGSCOMPARENEAR( f.attribute( "col1" ).toDouble(), expected, 0.001 );
185 }
186 
testNoGeom()187 void TestQgsAttributeTable::testNoGeom()
188 {
189   const 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( Qgis::VectorTemporalMode::FeatureDateTimeStartAndEndFromFields );
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 ), QTime( 0, 0, 0 ) ), QDateTime( QDate( 2020, 2, 1 ), QTime( 0, 0, 0 ) ) ) );
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   const QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
266   const QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
267   const 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 
testEdited()293 void TestQgsAttributeTable::testEdited()
294 {
295   // test attribute table opening in edited features mode
296   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
297   QVERIFY( tempLayer->isValid() );
298 
299   const QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
300   const QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
301   const QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
302   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
303 
304   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowEdited ) );
305 
306   QVERIFY( !dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
307   //should be nothing - because no edited features!
308   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
309   QVERIFY( dlg->mMainView->masterModel()->request().filterFids().isEmpty() );
310 
311   // make some edits
312   tempLayer->startEditing();
313   QVERIFY( tempLayer->changeAttributeValue( 1, 1, 5.5 ) );
314   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
315   QCOMPARE( dlg->mMainView->masterModel()->request().filterFids(), QgsFeatureIds() << 1 );
316   QgsGeometry geom = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 0, 1 1)" ) );
317   QVERIFY( tempLayer->changeGeometry( 3, geom ) );
318   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
319   QCOMPARE( dlg->mMainView->masterModel()->request().filterFids(), QgsFeatureIds() << 1 << 3 );
320 
321   // another test - start with edited features when dialog created
322   dlg.reset( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowEdited ) );
323   QVERIFY( !dlg->mMainView->masterModel()->layerCache()->cacheGeometry() );
324   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
325   QCOMPARE( dlg->mMainView->masterModel()->request().filterFids(), QgsFeatureIds() << 1 << 3 );
326   // remove edits
327   tempLayer->rollBack();
328   QCOMPARE( dlg->mMainView->masterModel()->request().filterType(), QgsFeatureRequest::FilterFids );
329   QVERIFY( dlg->mMainView->masterModel()->request().filterFids().isEmpty() );
330 }
331 
testSelectedOnTop()332 void TestQgsAttributeTable::testSelectedOnTop()
333 {
334   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
335   QVERIFY( tempLayer->isValid() );
336 
337   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
338   f1.setAttribute( 0, 1 );
339   f1.setAttribute( 1, 3.2 );
340   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
341   f2.setAttribute( 0, 2 );
342   f2.setAttribute( 1, 1.8 );
343   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
344   f3.setAttribute( 0, 3 );
345   f3.setAttribute( 1, 5.0 );
346   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
347 
348   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
349 
350   dlg->mMainView->setSortExpression( "pk" );
351   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
352   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
353   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
354 
355   tempLayer->selectByIds( QgsFeatureIds() << 2 );
356   dlg->mMainView->setSelectedOnTop( true );
357 
358   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
359   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
360   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
361 
362   dlg->mMainView->setSelectedOnTop( false );
363 
364   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
365   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
366   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
367 
368   tempLayer->selectByIds( QgsFeatureIds() << 3 );
369 
370   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
371   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
372   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
373 
374   dlg->mMainView->setSelectedOnTop( true );
375 
376   QCOMPARE( dlg->mMainView->mFilterModel->index( 0, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 3 ) );
377   QCOMPARE( dlg->mMainView->mFilterModel->index( 1, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 1 ) );
378   QCOMPARE( dlg->mMainView->mFilterModel->index( 2, 0 ).data( QgsAttributeTableModel::FeatureIdRole ), QVariant( 2 ) );
379 
380 }
381 
testSortByDisplayExpression()382 void TestQgsAttributeTable::testSortByDisplayExpression()
383 {
384   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
385   QVERIFY( tempLayer->isValid() );
386 
387   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
388   f1.setAttribute( 0, 1 );
389   f1.setAttribute( 1, 3.2 );
390   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
391   f2.setAttribute( 0, 2 );
392   f2.setAttribute( 1, 1.8 );
393   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
394   f3.setAttribute( 0, 3 );
395   f3.setAttribute( 1, 5.0 );
396   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
397 
398   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
399 
400   dlg->mMainView->mFeatureListView->setDisplayExpression( "pk" );
401   QgsFeatureListModel *listModel = dlg->mMainView->mFeatureListModel;
402   QCOMPARE( listModel->rowCount(), 3 );
403 
404   QCOMPARE( listModel->index( 0, 0 ).data( Qt::DisplayRole ), QVariant( 1 ) );
405   QCOMPARE( listModel->index( 1, 0 ).data( Qt::DisplayRole ), QVariant( 2 ) );
406   QCOMPARE( listModel->index( 2, 0 ).data( Qt::DisplayRole ), QVariant( 3 ) );
407 
408   dlg->mMainView->mFeatureListView->setDisplayExpression( "col1" );
409   QCOMPARE( listModel->index( 0, 0 ).data( Qt::DisplayRole ), QVariant( 1.8 ) );
410   QCOMPARE( listModel->index( 1, 0 ).data( Qt::DisplayRole ), QVariant( 3.2 ) );
411   QCOMPARE( listModel->index( 2, 0 ).data( Qt::DisplayRole ), QVariant( 5.0 ) );
412 }
413 
testRegression15974()414 void TestQgsAttributeTable::testRegression15974()
415 {
416   // Test duplicated rows in attribute table + two crashes.
417   const QString path = QDir::tempPath() + "/testshp15974.shp";
418   std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "polygon?crs=epsg:4326&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
419   QVERIFY( tempLayer->isValid() );
420   QgsVectorFileWriter::SaveVectorOptions saveOptions;
421   saveOptions.fileEncoding = QStringLiteral( "system" );
422   saveOptions.driverName = QStringLiteral( "ESRI Shapefile" );
423   QgsVectorFileWriter::writeAsVectorFormatV3( tempLayer.get(), path, tempLayer->transformContext(), saveOptions );
424   std::unique_ptr< QgsVectorLayer> shpLayer( new QgsVectorLayer( path, QStringLiteral( "test" ),  QStringLiteral( "ogr" ) ) );
425   QgsFeature f1( shpLayer->dataProvider()->fields(), 1 );
426   QgsGeometry geom;
427   geom = QgsGeometry::fromWkt( QStringLiteral( "polygon((0 0, 0 1, 1 1, 1 0, 0 0))" ) );
428   QVERIFY( geom.isGeosValid() );
429   f1.setGeometry( geom );
430   QgsFeature f2( shpLayer->dataProvider()->fields(), 2 );
431   f2.setGeometry( geom );
432   QgsFeature f3( shpLayer->dataProvider()->fields(), 3 );
433   f3.setGeometry( geom );
434   QVERIFY( shpLayer->startEditing() );
435   QVERIFY( shpLayer->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
436   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( shpLayer.get() ) );
437   QCOMPARE( shpLayer->featureCount(), 3L );
438   mQgisApp->saveEdits( shpLayer.get() );
439   QCOMPARE( shpLayer->featureCount(), 3L );
440   QCOMPARE( dlg->mMainView->masterModel()->rowCount(), 3 );
441   QCOMPARE( dlg->mMainView->mLayerCache->cachedFeatureIds().count(), 3 );
442   QCOMPARE( dlg->mMainView->featureCount(), 3 );
443   // All the following instructions made the test pass, before the connections to invalidate()
444   // were introduced in QgsDualView::initModels
445   // dlg->mMainView->mFilterModel->setSourceModel( dlg->mMainView->masterModel() );
446   // dlg->mMainView->mFilterModel->invalidate();
447   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 );
448 }
449 
testOrderColumn()450 void TestQgsAttributeTable::testOrderColumn()
451 {
452   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" ) ) );
453   QVERIFY( tempLayer->isValid() );
454 
455   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
456   f1.setAttribute( 0, 1 );
457   f1.setAttribute( 1, 13 );
458   f1.setAttribute( 2, 7 );
459   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 ) );
460 
461   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get() ) );
462 
463   // Issue https://github.com/qgis/QGIS/issues/28493
464   // When we reorder column (last column becomes first column), and we select an entire row
465   // the currentIndex is no longer the first column, and consequently it breaks edition
466 
467   QgsAttributeTableConfig config = QgsAttributeTableConfig();
468   config.update( tempLayer->dataProvider()->fields() );
469   QVector<QgsAttributeTableConfig::ColumnConfig> columns = config.columns();
470 
471   // move last column in first position
472   columns.move( 2, 0 );
473   config.setColumns( columns );
474 
475   dlg->mMainView->setAttributeTableConfig( config );
476 
477   QgsAttributeTableFilterModel *filterModel = static_cast<QgsAttributeTableFilterModel *>( dlg->mMainView->mTableView->model() );
478   filterModel->sort( 0, Qt::AscendingOrder );
479 
480   QModelIndex index = filterModel->mapToSource( filterModel->sourceModel()->index( 0, 0 ) );
481   QCOMPARE( index.row(), 0 );
482   QCOMPARE( index.column(), 2 );
483 
484   index = filterModel->mapFromSource( filterModel->sourceModel()->index( 0, 0 ) );
485   QCOMPARE( index.row(), 0 );
486   QCOMPARE( index.column(), 1 );
487 
488   qDebug() << filterModel->mapFromSource( filterModel->sourceModel()->index( 0, 0 ) );
489 
490   // column 0 is indeed column 2 since we move it
491   QCOMPARE( filterModel->sortColumn(), 2 );
492 }
493 
testFilteredFeatures()494 void TestQgsAttributeTable::testFilteredFeatures()
495 {
496   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" ) ) );
497   QVERIFY( tempLayer->isValid() );
498 
499   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
500   f1.setAttribute( 0, 1 );
501   f1.setAttribute( 1, 2 );
502   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
503   f2.setAttribute( 0, 2 );
504   f2.setAttribute( 1, 4 );
505   QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
506   f3.setAttribute( 0, 3 );
507   f3.setAttribute( 1, 6 );
508   QgsFeature f4( tempLayer->dataProvider()->fields(), 4 );
509   f4.setAttribute( 0, 4 );
510   f4.setAttribute( 1, 8 );
511 
512   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
513 
514   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowAll ) );
515 
516   QEventLoop loop;
517   connect( qobject_cast<QgsAttributeTableFilterModel *>( dlg->mMainView->mFilterModel ), &QgsAttributeTableFilterModel::featuresFiltered, &loop, &QEventLoop::quit );
518 
519   // show all (three features)
520   dlg->mFeatureFilterWidget->filterShowAll();
521   QCOMPARE( dlg->mMainView->featureCount(), 3 );
522   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 );
523 
524   // add a feature
525   tempLayer->startEditing();
526   QVERIFY( tempLayer->addFeatures( QgsFeatureList() << f4 ) );
527   //still show all (four features)
528   QCOMPARE( tempLayer->featureCount(), 4L );
529   QCOMPARE( dlg->mMainView->featureCount(), 4 );
530   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 4 );
531 
532   // bigger 5 (two of four features)
533   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>5" ), QgsAttributeForm::ReplaceFilter, true );
534   QCOMPARE( dlg->mMainView->featureCount(), 4 );
535   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 );
536   // bigger 7 (one of four features)
537   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>7" ), QgsAttributeForm::ReplaceFilter, true );
538   QCOMPARE( dlg->mMainView->featureCount(), 4 );
539   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 1 );
540   // bigger 9 (no of four features)
541   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>9" ), QgsAttributeForm::ReplaceFilter, true );
542   QCOMPARE( dlg->mMainView->featureCount(), 4 );
543   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 0 );
544 
545   //add two features
546   QgsFeature f5( tempLayer->dataProvider()->fields(), 5 );
547   f5.setAttribute( 0, 5 );
548   f5.setAttribute( 1, 10 );
549   QgsFeature f6( tempLayer->dataProvider()->fields(), 6 );
550   f6.setAttribute( 0, 6 );
551   f6.setAttribute( 1, 12 );
552   QVERIFY( tempLayer->addFeatures( QgsFeatureList() << f5 << f6 ) );
553   tempLayer->commitChanges();
554   loop.exec();
555   //no filter change -> now two of six features
556   QCOMPARE( dlg->mMainView->featureCount(), 6 );
557   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 );
558 
559   //remove a feature not affecting the filter
560   tempLayer->startEditing();
561   QVERIFY( tempLayer->deleteFeature( f2.id() ) );
562   //no filter change -> now two of five features
563   QCOMPARE( dlg->mMainView->featureCount(), 5 );
564   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 );
565 
566   //remove a feature affecting the filter
567   QVERIFY( tempLayer->deleteFeature( f5.id() ) );
568   //no filter change -> now one of four features
569   QCOMPARE( dlg->mMainView->featureCount(), 4 );
570   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 1 );
571 
572   // smaller 11 (three of four features)
573   dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1<11" ), QgsAttributeForm::ReplaceFilter, true );
574   QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 );
575 }
576 
testCopySelectedRows()577 void TestQgsAttributeTable::testCopySelectedRows()
578 {
579   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" ) ) );
580   QVERIFY( tempLayer->isValid() );
581 
582   QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
583   f1.setAttribute( 0, 1 );
584   f1.setAttribute( 1, 2 );
585 
586   QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
587   f2.setAttribute( 0, 2 );
588   f2.setAttribute( 1, 4 );
589 
590   QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) );
591 
592   std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowAll ) );
593 
594   tempLayer->selectByIds( QgsFeatureIds() << 1 << 2 );
595 
596   dlg->mActionCopySelectedRows_triggered();
597 
598   QgsClipboard *clipboard = QgisApp::instance()->clipboard();
599   QVERIFY( clipboard );
600   QVERIFY( !clipboard->isEmpty() );
601   QCOMPARE( clipboard->fields().names(), QStringList() << "pk" << "col1" << "col2" );
602 
603   const QgsFeatureList features = clipboard->copyOf();
604   QCOMPARE( features.count(), 2 );
605   QCOMPARE( features.at( 0 ).attribute( 0 ), 1 );
606   QCOMPARE( features.at( 0 ).attribute( "col1" ), 2 );
607   QCOMPARE( features.at( 0 ).attribute( "col2" ), QVariant() );
608   QCOMPARE( features.at( 1 ).attribute( "pk" ), 2 );
609   QCOMPARE( features.at( 1 ).attribute( "col1" ), 4 );
610   QCOMPARE( features.at( 1 ).attribute( 2 ), QVariant() );
611 
612   QCOMPARE( clipboard->crs().authid(), "EPSG:3111" );
613 }
614 
615 QGSTEST_MAIN( TestQgsAttributeTable )
616 #include "testqgsattributetable.moc"
617