New DB table widget: new tests and fixes for file-based DBS

This commit is contained in:
Alessandro Pasotti 2020-03-11 17:31:50 +01:00
parent 1bf8cfe32f
commit 016f7ed43b
4 changed files with 252 additions and 72 deletions

View File

@ -46,17 +46,23 @@ Constructs a new QgsNewDatabaseTableNameWidget
:param parent: optional parent for this widget
%End
QString schema();
QString schema() const;
%Docstring
Returns the currently selected schema for the new table
Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table
%End
QString table();
QString uri() const;
%Docstring
Returns the (possibly blank) string representation of the new table data source URI.
The URI might be invalid in case the widget is not in a valid state.
%End
QString table() const;
%Docstring
Returns the current name of the new table
%End
QString dataProviderKey();
QString dataProviderKey() const;
%Docstring
Returns the currently selected data item provider key
%End
@ -66,11 +72,13 @@ Returns the currently selected data item provider key
Returns ``True`` if the widget contains a valid new table name
%End
QString validationError();
QString validationError() const;
%Docstring
Returns the validation error or an empty string is the widget status is valid
%End
signals:
void validationChanged( bool isValid );
@ -82,7 +90,7 @@ This signal is emitted whenever the validation status of the widget changes.
void schemaNameChanged( const QString &schemaName );
%Docstring
This signal is emitted when the user selects a schema.
This signal is emitted when the user selects a schema (or file path for filesystem-based DBs like spatialite or GPKG).
:param schemaName: the name of the selected schema
%End
@ -102,6 +110,13 @@ that has a different data provider than the previously selected one.
:param providerKey: the data provider key of the selected schema
%End
void uriChanged( const QString &uri );
%Docstring
This signal is emitted when the URI of the new table changes, whether or not it is a valid one.
:param uri: URI string representation
%End
};

View File

@ -23,6 +23,10 @@
#include "qgsproviderregistry.h"
#include "qgsprovidermetadata.h"
// List of data item provider keys that are filesystem based
QStringList QgsNewDatabaseTableNameWidget::FILESYSTEM_BASED_DATAITEM_PROVIDERS { QStringLiteral( "GPKG" ), QStringLiteral( "SPATIALITE" ) };
QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
QgsBrowserGuiModel *browserModel,
const QStringList &providersFilter,
@ -44,8 +48,6 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
setupUi( this );
mValidationResults->setStyleSheet( QStringLiteral( "* { font-weight: bold; color: red; }" ) );
QStringList hiddenProviders
{
QStringLiteral( "special:Favorites" ),
@ -63,8 +65,8 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
hiddenProviders.push_back( provider->name() );
continue;
}
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( provider->dataProviderKey() ) };
if ( ! md )
QgsProviderMetadata *metadata { QgsProviderRegistry::instance()->providerMetadata( provider->dataProviderKey() ) };
if ( ! metadata )
{
hiddenProviders.push_back( provider->name() );
continue;
@ -90,7 +92,6 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
mBrowserProxyModel.setDataItemProviderKeyFilter( hiddenProviders );
mBrowserProxyModel.setShowLayers( false );
mBrowserTreeView->setHeaderHidden( true );
mBrowserTreeView->setExpandsOnDoubleClick( false );
mBrowserTreeView->setModel( &mBrowserProxyModel );
mBrowserTreeView->setBrowserModel( mBrowserModel );
@ -99,6 +100,7 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
{
mTableName = mNewTableName->text();
emit tableNameChanged( mTableName );
updateUri();
validate();
} );
@ -106,40 +108,39 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
{
if ( index.isValid() )
{
const QgsDataItem *dataItem( mBrowserProxyModel.dataItem( index ) );
if ( dataItem )
if ( const QgsDataItem *dataItem = mBrowserProxyModel.dataItem( index ) )
{
const QgsDataCollectionItem *collectionItem = qobject_cast<const QgsDataCollectionItem *>( dataItem );
if ( collectionItem )
if ( const QgsDataCollectionItem *collectionItem = qobject_cast<const QgsDataCollectionItem *>( dataItem ) )
{
const QString providerKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( dataItem->providerKey() ) };
if ( mShownProviders.contains( providerKey ) )
bool validationRequired { false };
const QString oldSchema { mSchemaName };
if ( mDataProviderKey != providerKey )
{
bool validationRequired { false };
const QString oldSchema { mSchemaName };
mSchemaName.clear();
mDataProviderKey = providerKey;
emit providerKeyChanged( providerKey );
validationRequired = true;
}
if ( mDataProviderKey != providerKey )
if ( collectionItem->layerCollection( ) )
{
mIsFilePath = FILESYSTEM_BASED_DATAITEM_PROVIDERS.contains( collectionItem->providerKey() );
// Data items for filesystem based items are in the form gpkg://path/to/file.gpkg
mSchemaName = mIsFilePath ? collectionItem->path().remove( QRegularExpression( QStringLiteral( "^[A-z]+:/" ) ) ) : collectionItem->name(); // it may be cleared
mConnectionName = mIsFilePath ? collectionItem->name() : collectionItem->parent()->name();
if ( oldSchema != mSchemaName )
{
mSchemaName.clear();
emit providerKeyChanged( providerKey );
mDataProviderKey = providerKey;
validate();
emit schemaNameChanged( mSchemaName );
validationRequired = true;
}
}
if ( collectionItem->layerCollection( ) )
{
mSchemaName = collectionItem->name(); // it may be cleared
if ( oldSchema != collectionItem->name() )
{
emit schemaNameChanged( mSchemaName );
validationRequired = true;
}
}
if ( validationRequired )
{
validate();
}
if ( validationRequired )
{
updateUri();
validate();
}
}
}
@ -147,20 +148,59 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget(
} );
validate();
}
QString QgsNewDatabaseTableNameWidget::schema()
void QgsNewDatabaseTableNameWidget::updateUri()
{
const QString oldUri { mUri };
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( mDataProviderKey ) };
if ( md )
{
QgsAbstractProviderConnection *conn { md->findConnection( mConnectionName ) };
if ( conn )
{
QVariantMap uriParts { md->decodeUri( conn->uri() ) };
uriParts[ QStringLiteral( "layerName" ) ] = mTableName;
uriParts[ QStringLiteral( "schema" ) ] = mSchemaName;
uriParts[ QStringLiteral( "table" ) ] = mTableName;
if ( mIsFilePath )
{
uriParts[ QStringLiteral( "dbname" ) ] = mSchemaName;
}
mUri = md->encodeUri( uriParts );
}
else
{
mUri = QString();
}
}
else
{
mUri = QString();
}
if ( mUri != oldUri )
{
emit uriChanged( mUri );
}
}
QString QgsNewDatabaseTableNameWidget::schema() const
{
return mSchemaName;
}
QString QgsNewDatabaseTableNameWidget::table()
QString QgsNewDatabaseTableNameWidget::uri() const
{
return mUri;
}
QString QgsNewDatabaseTableNameWidget::table() const
{
return mTableName;
}
QString QgsNewDatabaseTableNameWidget::dataProviderKey()
QString QgsNewDatabaseTableNameWidget::dataProviderKey() const
{
return mDataProviderKey;
}
@ -177,6 +217,9 @@ void QgsNewDatabaseTableNameWidget::validate()
mValidationError.clear();
// Whether to show it red
bool isError { false };
if ( ! mIsValid )
{
if ( mTableName.isEmpty() && mSchemaName.isEmpty() )
@ -187,6 +230,7 @@ void QgsNewDatabaseTableNameWidget::validate()
! mSchemaName.isEmpty() &&
tableNames( ).contains( mTableName ) )
{
isError = true;
mValidationError = tr( "A table named '%1' already exists" ).arg( mTableName );
}
else if ( mSchemaName.isEmpty() )
@ -206,6 +250,11 @@ void QgsNewDatabaseTableNameWidget::validate()
mValidationError = tr( "Select a database schema and enter a unique name for the new table" );
}
}
mValidationResults->setStyleSheet( isError ?
QStringLiteral( "* { color: red; }" ) :
QString() );
mValidationResults->setText( mValidationError );
mValidationResults->setVisible( ! mIsValid );
if ( wasValid != mIsValid )
@ -226,26 +275,29 @@ QStringList QgsNewDatabaseTableNameWidget::tableNames()
const QString dataProviderKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( dataItem->providerKey() ) };
if ( ! dataProviderKey.isEmpty() )
{
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) };
if ( md )
QgsProviderMetadata *metadata { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) };
if ( metadata )
{
QgsDataItem *parentDataItem { dataItem->parent() };
QgsDataItem *parentDataItem { mIsFilePath ? dataItem : dataItem->parent() };
if ( parentDataItem )
{
QgsAbstractProviderConnection *conn { md->findConnection( parentDataItem->name() ) };
const QString cacheKey { conn->uri() + dataItem->name() };
if ( mTableNamesCache.contains( cacheKey ) )
QgsAbstractProviderConnection *conn { metadata->findConnection( parentDataItem->name() ) };
if ( conn )
{
tableNames = mTableNamesCache.value( cacheKey );
}
else if ( conn && static_cast<QgsAbstractDatabaseProviderConnection *>( conn ) )
{
const auto tables { static_cast<QgsAbstractDatabaseProviderConnection *>( conn )->tables( dataItem->name() ) };
for ( const auto &tp : tables )
const QString cacheKey { conn->uri() + dataItem->name() };
if ( mTableNamesCache.contains( cacheKey ) )
{
tableNames.push_back( tp.tableName() );
tableNames = mTableNamesCache.value( cacheKey );
}
else if ( conn && static_cast<QgsAbstractDatabaseProviderConnection *>( conn ) )
{
const auto tables { static_cast<QgsAbstractDatabaseProviderConnection *>( conn )->tables( dataItem->name() ) };
for ( const auto &tp : tables )
{
tableNames.push_back( tp.tableName() );
}
mTableNamesCache[ cacheKey ] = tableNames;
}
mTableNamesCache[ cacheKey ] = tableNames;
}
}
}
@ -260,7 +312,7 @@ bool QgsNewDatabaseTableNameWidget::isValid() const
return mIsValid;
}
QString QgsNewDatabaseTableNameWidget::validationError()
QString QgsNewDatabaseTableNameWidget::validationError() const
{
return mValidationError;
}

View File

@ -59,19 +59,25 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs
QWidget *parent = nullptr );
/**
* Returns the currently selected schema for the new table
* Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table
*/
QString schema();
QString schema() const;
/**
* Returns the (possibly blank) string representation of the new table data source URI.
* The URI might be invalid in case the widget is not in a valid state.
*/
QString uri() const;
/**
* Returns the current name of the new table
*/
QString table();
QString table() const;
/**
* Returns the currently selected data item provider key
*/
QString dataProviderKey();
QString dataProviderKey() const;
/**
* Returns TRUE if the widget contains a valid new table name
@ -81,7 +87,9 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs
/**
* Returns the validation error or an empty string is the widget status is valid
*/
QString validationError();
QString validationError() const;
signals:
@ -93,7 +101,7 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs
void validationChanged( bool isValid );
/**
* This signal is emitted when the user selects a schema.
* This signal is emitted when the user selects a schema (or file path for filesystem-based DBs like spatialite or GPKG).
*
* \param schemaName the name of the selected schema
*/
@ -114,16 +122,28 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs
*/
void providerKeyChanged( const QString &providerKey );
/**
* This signal is emitted when the URI of the new table changes, whether or not it is a valid one.
*
* \param uri URI string representation
*/
void uriChanged( const QString &uri );
private:
QgsBrowserProxyModel mBrowserProxyModel;
QgsBrowserGuiModel *mBrowserModel = nullptr;
void updateUri();
void validate();
QStringList tableNames();
QgsBrowserProxyModel mBrowserProxyModel;
QgsBrowserGuiModel *mBrowserModel = nullptr;
QString mDataProviderKey;
QString mTableName;
QString mSchemaName;
QString mConnectionName;
bool mIsFilePath = false;
QString mUri;
//! List of data provider keys of shown providers
QSet<QString> mShownProviders;
bool mIsValid = false;
@ -131,6 +151,7 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs
//! Table names cache
QMap<QString, QStringList> mTableNamesCache;
static QStringList FILESYSTEM_BASED_DATAITEM_PROVIDERS;
// For testing:
friend class TestQgsNewDatabaseTableNameWidget;

View File

@ -18,6 +18,7 @@
#include <QObject>
#include <QDialog>
#include <QSignalSpy>
#include <QTemporaryDir>
#include "qgsnewdatabasetablenamewidget.h"
#include "qgsprovidermetadata.h"
@ -37,20 +38,50 @@ class TestQgsNewDatabaseTableNameWidget: public QObject
void cleanup(); // will be called after every testfunction.
void testWidgetFilters();
void testWidgetSignals();
void testWidgetSignalsPostgres();
void testWidgetSignalsGeopackage();
private:
std::unique_ptr<QgsAbstractProviderConnection> mPgConn;
std::unique_ptr<QgsAbstractProviderConnection> mGpkgConn;
QTemporaryDir mDir;
QString mGpkgPath;
};
void TestQgsNewDatabaseTableNameWidget::initTestCase()
{
QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) );
QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) );
QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST-NEW-DBTABLE-WIDGET" ) );
QgsApplication::init();
QgsApplication::initQgis();
// Add some connections to test with
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "postgres" ) ) };
QgsAbstractProviderConnection *conn { md->createConnection( qgetenv( "QGIS_PGTEST_DB" ) ) };
md->saveConnection( conn, QStringLiteral( "PG_1" ) );
conn = md->createConnection( qgetenv( " QGIS_PGTEST_DB" ) );
md->saveConnection( conn, QStringLiteral( "PG_2" ) );
mPgConn.reset( md->createConnection( qgetenv( "QGIS_PGTEST_DB" ), { } ) );
md->saveConnection( mPgConn.get(), QStringLiteral( "PG_1" ) );
md->saveConnection( mPgConn.get(), QStringLiteral( "PG_2" ) );
md = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) );
QString errCause;
QMap<int, int> m;
mGpkgPath = mDir.filePath( QStringLiteral( "test.gpkg" ) );
QMap<QString, QVariant> options { { QStringLiteral( "layerName" ), QString( "test_layer" ) } };
QVERIFY( md->createEmptyLayer( mGpkgPath,
QgsFields(),
QgsWkbTypes::Type::Point,
QgsCoordinateReferenceSystem::fromEpsgId( 4326 ),
true,
m,
errCause,
&options ) == QgsVectorLayerExporter::ExportError::NoError );
QVERIFY( errCause.isEmpty() );
mGpkgConn.reset( md->createConnection( mDir.filePath( QStringLiteral( "test.gpkg" ) ), { } ) );
md->saveConnection( mGpkgConn.get(), QStringLiteral( "GPKG_1" ) );
}
void TestQgsNewDatabaseTableNameWidget::cleanupTestCase()
@ -77,7 +108,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetFilters()
}
void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
void TestQgsNewDatabaseTableNameWidget::testWidgetSignalsPostgres()
{
std::unique_ptr<QgsNewDatabaseTableNameWidget> w { qgis::make_unique<QgsNewDatabaseTableNameWidget>( nullptr, QStringList{ "postgres" } ) };
@ -92,6 +123,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QSignalSpy schemaSpy( w.get(), SIGNAL( schemaNameChanged( QString ) ) );
QSignalSpy tableSpy( w.get(), SIGNAL( tableNameChanged( QString ) ) );
QSignalSpy providerSpy( w.get(), SIGNAL( providerKeyChanged( QString ) ) );
QSignalSpy uriSpy( w.get(), SIGNAL( uriChanged( QString ) ) );
index = w->mBrowserProxyModel.mapToSource( w->mBrowserProxyModel.index( 0, 0 ) );
QVERIFY( index.isValid() );
@ -103,6 +135,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QVERIFY( ! w->isValid() );
QCOMPARE( providerSpy.count(), 1 );
QCOMPARE( uriSpy.count(), 0 );
QCOMPARE( tableSpy.count(), 0 );
QCOMPARE( schemaSpy.count(), 0 );
QCOMPARE( validationSpy.count(), 0 );
@ -110,7 +143,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QCOMPARE( arguments.at( 0 ).toString(), QString( "postgres" ) );
// Find qgis_test schema item
index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/qgis_test" ), Qt::MatchFlag::MatchStartsWith );
index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/qgis_test" ) );
QVERIFY( index.isValid() );
w->mBrowserTreeView->scrollTo( w->mBrowserProxyModel.mapFromSource( index ) );
rect = w->mBrowserTreeView->visualRect( w->mBrowserProxyModel.mapFromSource( index ) );
@ -121,8 +154,11 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QCOMPARE( validationSpy.count(), 0 );
QCOMPARE( schemaSpy.count(), 1 );
QCOMPARE( uriSpy.count(), 1 );
arguments = schemaSpy.takeLast();
QCOMPARE( arguments.at( 0 ).toString(), QString( "qgis_test" ) );
arguments = uriSpy.takeLast();
QVERIFY( ! arguments.at( 0 ).toString().isEmpty() );
w->mNewTableName->setText( QStringLiteral( "someNewTableData" ) );
QCOMPARE( tableSpy.count(), 1 );
@ -137,6 +173,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QCOMPARE( w->table(), QString( "someNewTableData" ) );
QCOMPARE( w->schema(), QString( "qgis_test" ) );
QCOMPARE( w->dataProviderKey(), QString( "postgres" ) );
QVERIFY( w->uri().contains( R"("qgis_test"."someNewTableData")" ) );
// Test unique and make it invalid again so we get a status change
w->mNewTableName->setText( QStringLiteral( "someData" ) );
@ -149,7 +186,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QCOMPARE( arguments.at( 0 ).toBool(), false );
// Now select another schema
index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/public" ), Qt::MatchFlag::MatchStartsWith );
index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/public" ) );
QVERIFY( index.isValid() );
w->mBrowserTreeView->scrollTo( w->mBrowserProxyModel.mapFromSource( index ) );
rect = w->mBrowserTreeView->visualRect( w->mBrowserProxyModel.mapFromSource( index ) );
@ -168,6 +205,61 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals()
QCOMPARE( w->table(), QString( "someData" ) );
QCOMPARE( w->schema(), QString( "public" ) );
QCOMPARE( w->dataProviderKey(), QString( "postgres" ) );
QVERIFY( w->uri().contains( R"("public"."someData")" ) );
}
void TestQgsNewDatabaseTableNameWidget::testWidgetSignalsGeopackage()
{
std::unique_ptr<QgsNewDatabaseTableNameWidget> w { qgis::make_unique<QgsNewDatabaseTableNameWidget>( nullptr, QStringList{ "ogr" } ) };
auto index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1" ) );
QVERIFY( index.isValid() );
w->mBrowserModel->dataItem( index )->populate( true );
w->mBrowserTreeView->expandAll();
QVERIFY( ! w->isValid() );
QSignalSpy validationSpy( w.get(), SIGNAL( validationChanged( bool ) ) );
QSignalSpy schemaSpy( w.get(), SIGNAL( schemaNameChanged( QString ) ) );
QSignalSpy tableSpy( w.get(), SIGNAL( tableNameChanged( QString ) ) );
QSignalSpy providerSpy( w.get(), SIGNAL( providerKeyChanged( QString ) ) );
QSignalSpy uriSpy( w.get(), SIGNAL( uriChanged( QString ) ) );
/*
QDialog d;
QVBoxLayout l;
l.addWidget( w.get() );
d.setLayout( &l );
d.exec();
//*/
uriSpy.clear();
index = w->mBrowserModel->findPath( QStringLiteral( "gpkg:/%1" ).arg( mGpkgPath ) );
QVERIFY( index.isValid() );
w->mBrowserTreeView->scrollTo( w->mBrowserProxyModel.mapFromSource( index ) );
auto rect = w->mBrowserTreeView->visualRect( w->mBrowserProxyModel.mapFromSource( index ) );
QVERIFY( rect.isValid() );
QTest::mouseClick( w->mBrowserTreeView->viewport(), Qt::LeftButton, 0, rect.center() );
QVERIFY( ! w->isValid() );
QCOMPARE( schemaSpy.count(), 1 );
auto arguments = schemaSpy.takeLast();
QCOMPARE( arguments.at( 0 ).toString(), mGpkgPath );
QCOMPARE( uriSpy.count(), 1 );
arguments = uriSpy.takeLast();
QCOMPARE( arguments.at( 0 ).toString(), mGpkgPath );
w->mNewTableName->setText( QStringLiteral( "newTableName" ) );
QVERIFY( w->isValid() );
QCOMPARE( validationSpy.count(), 1 );
arguments = validationSpy.takeLast();
QCOMPARE( arguments.at( 0 ).toBool(), true );
// Test getters
QCOMPARE( w->table(), QString( "newTableName" ) );
QCOMPARE( w->schema(), mGpkgPath );
QCOMPARE( w->dataProviderKey(), QString( "ogr" ) );
QCOMPARE( w->uri(), mGpkgPath + QStringLiteral( "|layername=newTableName" ) );
}