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