[processing] do not allow using unsupported file formats

Show warning message if user selects incompatible output file format

fixes #21089
This commit is contained in:
Víctor Olaya 2019-02-22 20:24:56 +01:00 committed by Nyall Dawson
parent cdc622ef4c
commit 13bff9620c
9 changed files with 152 additions and 12 deletions

View File

@ -138,6 +138,15 @@ formats for geometry-less layers can override this method to return a different
.. seealso:: :py:func:`supportsNonFileBasedOutput`
.. versionadded:: 3.4.3
%End
virtual bool isSupportedOutputValue( const QVariant &outputValue, const QgsProcessingDestinationParameter *parameter, QgsProcessingContext &context, QString &error /Out/ ) const;
%Docstring
Returns true if the specified ``outputValue`` is of a supported file format for the given destination ``parameter``.
If the output value is not supported, ``error`` will be set to a descriptive message explaining why.
.. versionadded:: 3.6
%End
virtual QString defaultVectorFileExtension( bool hasGeometry = true ) const;

View File

@ -142,3 +142,5 @@ class GdalParametersPanel(ParametersPanel):
self.text.setPlainText(" ".join(commands))
except AlgorithmDialogBase.InvalidParameterValue as e:
self.text.setPlainText(self.tr("Invalid value for parameter '{0}'").format(e.parameter.description()))
except AlgorithmDialogBase.InvalidOutputExtension as e:
self.text.setPlainText(e.message)

View File

@ -25,6 +25,7 @@ __copyright__ = '(C) 2012, Victor Olaya'
__revision__ = '$Format:%H$'
import os
from pprint import pformat
import time
@ -44,6 +45,7 @@ from qgis.core import (Qgis,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterRasterDestination,
QgsProcessingAlgorithm,
QgsProcessingParameters,
QgsProxyProgressTask,
QgsTaskManager)
from qgis.gui import (QgsGui,
@ -154,11 +156,18 @@ class AlgorithmDialog(QgsProcessingAlgorithmDialogBase):
if self.mainWidget().checkBoxes[param.name()].isChecked():
dest_project = QgsProject.instance()
value = self.mainWidget().outputWidgets[param.name()].getValue()
widget = self.mainWidget().outputWidgets[param.name()]
value = widget.getValue()
if value and isinstance(value, QgsProcessingOutputLayerDefinition):
value.destinationProject = dest_project
if value:
parameters[param.name()] = value
if param.isDestination():
context = dataobjects.createContext()
ok, error = self.algorithm().provider().isSupportedOutputValue(value, param, context)
if not ok:
raise AlgorithmDialogBase.InvalidOutputExtension(widget, error)
return self.algorithm().preprocessParameters(parameters)
@ -294,6 +303,18 @@ class AlgorithmDialog(QgsProcessingAlgorithmDialogBase):
self.messageBar().clearWidgets()
self.messageBar().pushMessage("", self.tr("Wrong or missing parameter value: {0}").format(e.parameter.description()),
level=Qgis.Warning, duration=5)
except AlgorithmDialogBase.InvalidOutputExtension as e:
try:
self.buttonBox().accepted.connect(lambda e=e:
e.widget.setPalette(QPalette()))
palette = e.widget.palette()
palette.setColor(QPalette.Base, QColor(255, 255, 0))
e.widget.setPalette(palette)
except:
pass
self.messageBar().clearWidgets()
self.messageBar().pushMessage("", e.message,
level=Qgis.Warning, duration=5)
def finish(self, successful, result, context, feedback):
keepOpen = not successful or ProcessingConfig.getSetting(ProcessingConfig.KEEP_DIALOG_OPEN)

View File

@ -32,3 +32,9 @@ class AlgorithmDialogBase:
def __init__(self, param, widget):
(self.parameter, self.widget) = (param, widget)
class InvalidOutputExtension(Exception):
def __init__(self, widget, message):
self.widget = widget
self.message = message

View File

@ -107,6 +107,66 @@ QStringList QgsProcessingProvider::supportedOutputTableExtensions() const
return supportedOutputVectorLayerExtensions();
}
bool QgsProcessingProvider::isSupportedOutputValue( const QVariant &outputValue, const QgsProcessingDestinationParameter *parameter, QgsProcessingContext &context, QString &error ) const
{
QString outputPath = QgsProcessingParameters::parameterAsOutputLayer( parameter, outputValue, context );
error.clear();
if ( parameter->type() == QgsProcessingParameterVectorDestination::typeName()
|| parameter->type() == QgsProcessingParameterFeatureSink::typeName() )
{
if ( outputPath.isEmpty() || outputPath.startsWith( QLatin1String( "memory:" ) ) )
{
if ( !supportsNonFileBasedOutput() )
{
error = tr( "This algorithm only supports disk-based outputs" );
return false;
}
return true;
}
QString providerKey;
QString uri;
QString layerName;
QMap<QString, QVariant> options;
bool useWriter = false;
QString format;
QString extension;
QgsProcessingUtils::parseDestinationString( outputPath, providerKey, uri, layerName, format, options, useWriter, extension );
if ( providerKey != QLatin1String( "ogr" ) )
{
if ( !supportsNonFileBasedOutput() )
{
error = tr( "This algorithm only supports disk-based outputs" );
return false;
}
return true;
}
if ( !supportedOutputVectorLayerExtensions().contains( extension, Qt::CaseInsensitive ) )
{
error = tr( "%1 files are not supported as outputs for this algorithm" ).arg( extension );
return false;
}
return true;
}
else if ( parameter->type() == QgsProcessingParameterRasterDestination::typeName() )
{
QFileInfo fi( outputPath );
const QString extension = fi.completeSuffix();
if ( !supportedOutputRasterLayerExtensions().contains( extension, Qt::CaseInsensitive ) )
{
error = tr( "%1 files are not supported as outputs for this algorithm" ).arg( extension );
return false;
}
return true;
}
else
{
return true;
}
}
QString QgsProcessingProvider::defaultVectorFileExtension( bool hasGeometry ) const
{
QgsSettings settings;

View File

@ -140,6 +140,15 @@ class CORE_EXPORT QgsProcessingProvider : public QObject
*/
virtual QStringList supportedOutputTableExtensions() const;
/**
* Returns true if the specified \a outputValue is of a supported file format for the given destination \a parameter.
*
* If the output value is not supported, \a error will be set to a descriptive message explaining why.
*
* \since QGIS 3.6
*/
virtual bool isSupportedOutputValue( const QVariant &outputValue, const QgsProcessingDestinationParameter *parameter, QgsProcessingContext &context, QString &error SIP_OUT ) const;
/**
* Returns the default file extension to use for vector outputs created by the
* provider.

View File

@ -481,8 +481,9 @@ QString QgsProcessingUtils::stringToPythonLiteral( const QString &string )
return s;
}
void QgsProcessingUtils::parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter )
void QgsProcessingUtils::parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter, QString &extension )
{
extension.clear();
QRegularExpression splitRx( QStringLiteral( "^(.{3,}?):(.*)$" ) );
QRegularExpressionMatch match = splitRx.match( destination );
if ( match.hasMatch() )
@ -504,7 +505,12 @@ void QgsProcessingUtils::parseDestinationString( QString &destination, QString &
options.insert( QStringLiteral( "layerName" ), layerName );
}
uri = dsUri.database();
format = QgsVectorFileWriter::driverForExtension( QFileInfo( uri ).completeSuffix() );
extension = QFileInfo( uri ).completeSuffix();
format = QgsVectorFileWriter::driverForExtension( extension );
}
else
{
extension = QFileInfo( uri ).completeSuffix();
}
options.insert( QStringLiteral( "update" ), true );
}
@ -516,7 +522,6 @@ void QgsProcessingUtils::parseDestinationString( QString &destination, QString &
providerKey = QStringLiteral( "ogr" );
QRegularExpression splitRx( QStringLiteral( "^(.*)\\.(.*?)$" ) );
QRegularExpressionMatch match = splitRx.match( destination );
QString extension;
if ( match.hasMatch() )
{
extension = match.captured( 2 );
@ -574,8 +579,9 @@ QgsFeatureSink *QgsProcessingUtils::createFeatureSink( QString &destination, Qgs
QString uri;
QString layerName;
QString format;
QString extension;
bool useWriter = false;
parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QgsFields newFields = fields;
if ( useWriter && providerKey == QLatin1String( "ogr" ) )

View File

@ -318,9 +318,10 @@ class CORE_EXPORT QgsProcessingUtils
*/
static QgsMapLayer *loadMapLayerFromString( const QString &string, LayerHint typeHint = UnknownType );
static void parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter );
static void parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter, QString &extension );
friend class TestQgsProcessing;
friend class QgsProcessingProvider;
};

View File

@ -1514,59 +1514,66 @@ void TestQgsProcessing::parseDestinationString()
QString layerName;
QString format;
QVariantMap options;
QString extension;
bool useWriter = false;
// simple shapefile output
QString destination = QStringLiteral( "d:/test.shp" );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QCOMPARE( destination, QStringLiteral( "d:/test.shp" ) );
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
QCOMPARE( uri, QStringLiteral( "d:/test.shp" ) );
QCOMPARE( format, QStringLiteral( "ESRI Shapefile" ) );
QCOMPARE( extension, QStringLiteral( "shp" ) );
QVERIFY( useWriter );
// postgis output
destination = QStringLiteral( "postgis:dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QCOMPARE( providerKey, QStringLiteral( "postgres" ) );
QCOMPARE( uri, QStringLiteral( "dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ) );
QVERIFY( !useWriter );
QVERIFY( extension.isEmpty() );
// postgres key should also work
destination = QStringLiteral( "postgres:dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QCOMPARE( providerKey, QStringLiteral( "postgres" ) );
QCOMPARE( uri, QStringLiteral( "dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ) );
QVERIFY( !useWriter );
QVERIFY( extension.isEmpty() );
// full uri shp output
options.clear();
destination = QStringLiteral( "ogr:d:/test.shp" );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
QCOMPARE( uri, QStringLiteral( "d:/test.shp" ) );
QCOMPARE( options.value( QStringLiteral( "update" ) ).toBool(), true );
QVERIFY( !options.contains( QStringLiteral( "layerName" ) ) );
QVERIFY( !useWriter );
QCOMPARE( extension, QStringLiteral( "shp" ) );
// full uri geopackage output
options.clear();
destination = QStringLiteral( "ogr:d:/test.gpkg" );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
QCOMPARE( uri, QStringLiteral( "d:/test.gpkg" ) );
QCOMPARE( options.value( QStringLiteral( "update" ) ).toBool(), true );
QVERIFY( !options.contains( QStringLiteral( "layerName" ) ) );
QVERIFY( !useWriter );
QCOMPARE( extension, QStringLiteral( "gpkg" ) );
// full uri geopackage table output with layer name
options.clear();
destination = QStringLiteral( "ogr:dbname='d:/package.gpkg' table=\"mylayer\" (geom) sql=" );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
QCOMPARE( uri, QStringLiteral( "d:/package.gpkg" ) );
QCOMPARE( options.value( QStringLiteral( "update" ) ).toBool(), true );
QCOMPARE( options.value( QStringLiteral( "layerName" ) ).toString(), QStringLiteral( "mylayer" ) );
QVERIFY( !useWriter );
QCOMPARE( extension, QStringLiteral( "gpkg" ) );
}
void TestQgsProcessing::createFeatureSink()
@ -5311,6 +5318,16 @@ void TestQgsProcessing::parameterVectorOut()
def.reset( new QgsProcessingParameterVectorDestination( "with_geom", QString(), QgsProcessing::TypeVectorAnyGeometry, QString(), true ) );
DummyProvider3 provider;
QString error;
QVERIFY( !provider.isSupportedOutputValue( "d:/test.shp", def.get(), context, error ) );
QVERIFY( !provider.isSupportedOutputValue( "d:/test.SHP", def.get(), context, error ) );
QVERIFY( !provider.isSupportedOutputValue( "ogr:d:/test.shp", def.get(), context, error ) );
QVERIFY( !provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.SHP" ), def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( "d:/test.mif", def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( "d:/test.MIF", def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( "ogr:d:/test.MIF", def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.MIF" ), def.get(), context, error ) );
provider.loadAlgorithms();
def->mOriginalProvider = &provider;
QCOMPARE( def->supportedOutputVectorLayerExtensions().count(), 2 );
@ -5423,6 +5440,15 @@ void TestQgsProcessing::parameterRasterOut()
QCOMPARE( fromCode->flags(), def->flags() );
QCOMPARE( fromCode->defaultValue(), def->defaultValue() );
DummyProvider3 provider;
QString error;
QVERIFY( !provider.isSupportedOutputValue( "d:/test.tif", def.get(), context, error ) );
QVERIFY( !provider.isSupportedOutputValue( "d:/test.TIF", def.get(), context, error ) );
QVERIFY( !provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.tif" ), def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( "d:/test.mig", def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( "d:/test.MIG", def.get(), context, error ) );
QVERIFY( provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.MIG" ), def.get(), context, error ) );
// test layers to load on completion
def.reset( new QgsProcessingParameterRasterDestination( "x", QStringLiteral( "desc" ), QStringLiteral( "default.tif" ), true ) );
QgsProcessingOutputLayerDefinition fs = QgsProcessingOutputLayerDefinition( QStringLiteral( "test.tif" ) );