Skip to content

Commit 96b25ab

Browse files
volayanyalldawson
authored andcommitted
[processing] do not allow using unsupported file formats
Show warning message if user selects incompatible output file format fixes #21089 (cherry picked from commit 13bff96)
1 parent 201fdce commit 96b25ab

File tree

9 files changed

+152
-12
lines changed

9 files changed

+152
-12
lines changed

python/core/auto_generated/processing/qgsprocessingprovider.sip.in

+9
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ formats for geometry-less layers can override this method to return a different
138138
.. seealso:: :py:func:`supportsNonFileBasedOutput`
139139

140140
.. versionadded:: 3.4.3
141+
%End
142+
143+
virtual bool isSupportedOutputValue( const QVariant &outputValue, const QgsProcessingDestinationParameter *parameter, QgsProcessingContext &context, QString &error /Out/ ) const;
144+
%Docstring
145+
Returns true if the specified ``outputValue`` is of a supported file format for the given destination ``parameter``.
146+
147+
If the output value is not supported, ``error`` will be set to a descriptive message explaining why.
148+
149+
.. versionadded:: 3.6
141150
%End
142151

143152
virtual QString defaultVectorFileExtension( bool hasGeometry = true ) const;

python/plugins/processing/algs/gdal/GdalAlgorithmDialog.py

+2
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,5 @@ def parametersHaveChanged(self):
142142
self.text.setPlainText(" ".join(commands))
143143
except AlgorithmDialogBase.InvalidParameterValue as e:
144144
self.text.setPlainText(self.tr("Invalid value for parameter '{0}'").format(e.parameter.description()))
145+
except AlgorithmDialogBase.InvalidOutputExtension as e:
146+
self.text.setPlainText(e.message)

python/plugins/processing/gui/AlgorithmDialog.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
__revision__ = '$Format:%H$'
2727

28+
import os
2829
from pprint import pformat
2930
import time
3031

@@ -44,6 +45,7 @@
4445
QgsProcessingParameterFeatureSink,
4546
QgsProcessingParameterRasterDestination,
4647
QgsProcessingAlgorithm,
48+
QgsProcessingParameters,
4749
QgsProxyProgressTask,
4850
QgsTaskManager)
4951
from qgis.gui import (QgsGui,
@@ -154,11 +156,18 @@ def getParameterValues(self):
154156
if self.mainWidget().checkBoxes[param.name()].isChecked():
155157
dest_project = QgsProject.instance()
156158

157-
value = self.mainWidget().outputWidgets[param.name()].getValue()
159+
widget = self.mainWidget().outputWidgets[param.name()]
160+
value = widget.getValue()
161+
158162
if value and isinstance(value, QgsProcessingOutputLayerDefinition):
159163
value.destinationProject = dest_project
160164
if value:
161165
parameters[param.name()] = value
166+
if param.isDestination():
167+
context = dataobjects.createContext()
168+
ok, error = self.algorithm().provider().isSupportedOutputValue(value, param, context)
169+
if not ok:
170+
raise AlgorithmDialogBase.InvalidOutputExtension(widget, error)
162171

163172
return self.algorithm().preprocessParameters(parameters)
164173

@@ -294,6 +303,18 @@ def on_complete(ok, results):
294303
self.messageBar().clearWidgets()
295304
self.messageBar().pushMessage("", self.tr("Wrong or missing parameter value: {0}").format(e.parameter.description()),
296305
level=Qgis.Warning, duration=5)
306+
except AlgorithmDialogBase.InvalidOutputExtension as e:
307+
try:
308+
self.buttonBox().accepted.connect(lambda e=e:
309+
e.widget.setPalette(QPalette()))
310+
palette = e.widget.palette()
311+
palette.setColor(QPalette.Base, QColor(255, 255, 0))
312+
e.widget.setPalette(palette)
313+
except:
314+
pass
315+
self.messageBar().clearWidgets()
316+
self.messageBar().pushMessage("", e.message,
317+
level=Qgis.Warning, duration=5)
297318

298319
def finish(self, successful, result, context, feedback):
299320
keepOpen = not successful or ProcessingConfig.getSetting(ProcessingConfig.KEEP_DIALOG_OPEN)

python/plugins/processing/gui/AlgorithmDialogBase.py

+6
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,9 @@ class InvalidParameterValue(Exception):
3232

3333
def __init__(self, param, widget):
3434
(self.parameter, self.widget) = (param, widget)
35+
36+
class InvalidOutputExtension(Exception):
37+
38+
def __init__(self, widget, message):
39+
self.widget = widget
40+
self.message = message

src/core/processing/qgsprocessingprovider.cpp

+60
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,66 @@ QStringList QgsProcessingProvider::supportedOutputTableExtensions() const
107107
return supportedOutputVectorLayerExtensions();
108108
}
109109

110+
bool QgsProcessingProvider::isSupportedOutputValue( const QVariant &outputValue, const QgsProcessingDestinationParameter *parameter, QgsProcessingContext &context, QString &error ) const
111+
{
112+
QString outputPath = QgsProcessingParameters::parameterAsOutputLayer( parameter, outputValue, context );
113+
error.clear();
114+
if ( parameter->type() == QgsProcessingParameterVectorDestination::typeName()
115+
|| parameter->type() == QgsProcessingParameterFeatureSink::typeName() )
116+
{
117+
if ( outputPath.isEmpty() || outputPath.startsWith( QLatin1String( "memory:" ) ) )
118+
{
119+
if ( !supportsNonFileBasedOutput() )
120+
{
121+
error = tr( "This algorithm only supports disk-based outputs" );
122+
return false;
123+
}
124+
return true;
125+
}
126+
127+
QString providerKey;
128+
QString uri;
129+
QString layerName;
130+
QMap<QString, QVariant> options;
131+
bool useWriter = false;
132+
QString format;
133+
QString extension;
134+
QgsProcessingUtils::parseDestinationString( outputPath, providerKey, uri, layerName, format, options, useWriter, extension );
135+
136+
if ( providerKey != QLatin1String( "ogr" ) )
137+
{
138+
if ( !supportsNonFileBasedOutput() )
139+
{
140+
error = tr( "This algorithm only supports disk-based outputs" );
141+
return false;
142+
}
143+
return true;
144+
}
145+
146+
if ( !supportedOutputVectorLayerExtensions().contains( extension, Qt::CaseInsensitive ) )
147+
{
148+
error = tr( "%1 files are not supported as outputs for this algorithm" ).arg( extension );
149+
return false;
150+
}
151+
return true;
152+
}
153+
else if ( parameter->type() == QgsProcessingParameterRasterDestination::typeName() )
154+
{
155+
QFileInfo fi( outputPath );
156+
const QString extension = fi.completeSuffix();
157+
if ( !supportedOutputRasterLayerExtensions().contains( extension, Qt::CaseInsensitive ) )
158+
{
159+
error = tr( "%1 files are not supported as outputs for this algorithm" ).arg( extension );
160+
return false;
161+
}
162+
return true;
163+
}
164+
else
165+
{
166+
return true;
167+
}
168+
}
169+
110170
QString QgsProcessingProvider::defaultVectorFileExtension( bool hasGeometry ) const
111171
{
112172
QgsSettings settings;

src/core/processing/qgsprocessingprovider.h

+9
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ class CORE_EXPORT QgsProcessingProvider : public QObject
140140
*/
141141
virtual QStringList supportedOutputTableExtensions() const;
142142

143+
/**
144+
* Returns true if the specified \a outputValue is of a supported file format for the given destination \a parameter.
145+
*
146+
* If the output value is not supported, \a error will be set to a descriptive message explaining why.
147+
*
148+
* \since QGIS 3.6
149+
*/
150+
virtual bool isSupportedOutputValue( const QVariant &outputValue, const QgsProcessingDestinationParameter *parameter, QgsProcessingContext &context, QString &error SIP_OUT ) const;
151+
143152
/**
144153
* Returns the default file extension to use for vector outputs created by the
145154
* provider.

src/core/processing/qgsprocessingutils.cpp

+10-4
Original file line numberDiff line numberDiff line change
@@ -481,8 +481,9 @@ QString QgsProcessingUtils::stringToPythonLiteral( const QString &string )
481481
return s;
482482
}
483483

484-
void QgsProcessingUtils::parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter )
484+
void QgsProcessingUtils::parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter, QString &extension )
485485
{
486+
extension.clear();
486487
QRegularExpression splitRx( QStringLiteral( "^(.{3,}?):(.*)$" ) );
487488
QRegularExpressionMatch match = splitRx.match( destination );
488489
if ( match.hasMatch() )
@@ -504,7 +505,12 @@ void QgsProcessingUtils::parseDestinationString( QString &destination, QString &
504505
options.insert( QStringLiteral( "layerName" ), layerName );
505506
}
506507
uri = dsUri.database();
507-
format = QgsVectorFileWriter::driverForExtension( QFileInfo( uri ).completeSuffix() );
508+
extension = QFileInfo( uri ).completeSuffix();
509+
format = QgsVectorFileWriter::driverForExtension( extension );
510+
}
511+
else
512+
{
513+
extension = QFileInfo( uri ).completeSuffix();
508514
}
509515
options.insert( QStringLiteral( "update" ), true );
510516
}
@@ -516,7 +522,6 @@ void QgsProcessingUtils::parseDestinationString( QString &destination, QString &
516522
providerKey = QStringLiteral( "ogr" );
517523
QRegularExpression splitRx( QStringLiteral( "^(.*)\\.(.*?)$" ) );
518524
QRegularExpressionMatch match = splitRx.match( destination );
519-
QString extension;
520525
if ( match.hasMatch() )
521526
{
522527
extension = match.captured( 2 );
@@ -574,8 +579,9 @@ QgsFeatureSink *QgsProcessingUtils::createFeatureSink( QString &destination, Qgs
574579
QString uri;
575580
QString layerName;
576581
QString format;
582+
QString extension;
577583
bool useWriter = false;
578-
parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
584+
parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
579585

580586
QgsFields newFields = fields;
581587
if ( useWriter && providerKey == QLatin1String( "ogr" ) )

src/core/processing/qgsprocessingutils.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,10 @@ class CORE_EXPORT QgsProcessingUtils
318318
*/
319319
static QgsMapLayer *loadMapLayerFromString( const QString &string, LayerHint typeHint = UnknownType );
320320

321-
static void parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter );
321+
static void parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap<QString, QVariant> &options, bool &useWriter, QString &extension );
322322

323323
friend class TestQgsProcessing;
324+
friend class QgsProcessingProvider;
324325

325326
};
326327

tests/src/analysis/testqgsprocessing.cpp

+32-6
Original file line numberDiff line numberDiff line change
@@ -1514,59 +1514,66 @@ void TestQgsProcessing::parseDestinationString()
15141514
QString layerName;
15151515
QString format;
15161516
QVariantMap options;
1517+
QString extension;
15171518
bool useWriter = false;
15181519

15191520
// simple shapefile output
15201521
QString destination = QStringLiteral( "d:/test.shp" );
1521-
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
1522+
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
15221523
QCOMPARE( destination, QStringLiteral( "d:/test.shp" ) );
15231524
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
15241525
QCOMPARE( uri, QStringLiteral( "d:/test.shp" ) );
15251526
QCOMPARE( format, QStringLiteral( "ESRI Shapefile" ) );
1527+
QCOMPARE( extension, QStringLiteral( "shp" ) );
15261528
QVERIFY( useWriter );
15271529

15281530
// postgis output
15291531
destination = QStringLiteral( "postgis:dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" );
1530-
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
1532+
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
15311533
QCOMPARE( providerKey, QStringLiteral( "postgres" ) );
15321534
QCOMPARE( uri, QStringLiteral( "dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ) );
15331535
QVERIFY( !useWriter );
1536+
QVERIFY( extension.isEmpty() );
15341537
// postgres key should also work
15351538
destination = QStringLiteral( "postgres:dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" );
1536-
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
1539+
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
15371540
QCOMPARE( providerKey, QStringLiteral( "postgres" ) );
15381541
QCOMPARE( uri, QStringLiteral( "dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ) );
15391542
QVERIFY( !useWriter );
1543+
QVERIFY( extension.isEmpty() );
15401544

15411545
// full uri shp output
15421546
options.clear();
15431547
destination = QStringLiteral( "ogr:d:/test.shp" );
1544-
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
1548+
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
15451549
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
15461550
QCOMPARE( uri, QStringLiteral( "d:/test.shp" ) );
15471551
QCOMPARE( options.value( QStringLiteral( "update" ) ).toBool(), true );
15481552
QVERIFY( !options.contains( QStringLiteral( "layerName" ) ) );
15491553
QVERIFY( !useWriter );
1554+
QCOMPARE( extension, QStringLiteral( "shp" ) );
15501555

15511556
// full uri geopackage output
15521557
options.clear();
15531558
destination = QStringLiteral( "ogr:d:/test.gpkg" );
1554-
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
1559+
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
15551560
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
15561561
QCOMPARE( uri, QStringLiteral( "d:/test.gpkg" ) );
15571562
QCOMPARE( options.value( QStringLiteral( "update" ) ).toBool(), true );
15581563
QVERIFY( !options.contains( QStringLiteral( "layerName" ) ) );
15591564
QVERIFY( !useWriter );
1565+
QCOMPARE( extension, QStringLiteral( "gpkg" ) );
15601566

15611567
// full uri geopackage table output with layer name
15621568
options.clear();
15631569
destination = QStringLiteral( "ogr:dbname='d:/package.gpkg' table=\"mylayer\" (geom) sql=" );
1564-
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter );
1570+
QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension );
15651571
QCOMPARE( providerKey, QStringLiteral( "ogr" ) );
15661572
QCOMPARE( uri, QStringLiteral( "d:/package.gpkg" ) );
15671573
QCOMPARE( options.value( QStringLiteral( "update" ) ).toBool(), true );
15681574
QCOMPARE( options.value( QStringLiteral( "layerName" ) ).toString(), QStringLiteral( "mylayer" ) );
15691575
QVERIFY( !useWriter );
1576+
QCOMPARE( extension, QStringLiteral( "gpkg" ) );
15701577
}
15711578

15721579
void TestQgsProcessing::createFeatureSink()
@@ -5311,6 +5318,16 @@ void TestQgsProcessing::parameterVectorOut()
53115318

53125319
def.reset( new QgsProcessingParameterVectorDestination( "with_geom", QString(), QgsProcessing::TypeVectorAnyGeometry, QString(), true ) );
53135320
DummyProvider3 provider;
5321+
QString error;
5322+
QVERIFY( !provider.isSupportedOutputValue( "d:/test.shp", def.get(), context, error ) );
5323+
QVERIFY( !provider.isSupportedOutputValue( "d:/test.SHP", def.get(), context, error ) );
5324+
QVERIFY( !provider.isSupportedOutputValue( "ogr:d:/test.shp", def.get(), context, error ) );
5325+
QVERIFY( !provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.SHP" ), def.get(), context, error ) );
5326+
QVERIFY( provider.isSupportedOutputValue( "d:/test.mif", def.get(), context, error ) );
5327+
QVERIFY( provider.isSupportedOutputValue( "d:/test.MIF", def.get(), context, error ) );
5328+
QVERIFY( provider.isSupportedOutputValue( "ogr:d:/test.MIF", def.get(), context, error ) );
5329+
QVERIFY( provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.MIF" ), def.get(), context, error ) );
5330+
53145331
provider.loadAlgorithms();
53155332
def->mOriginalProvider = &provider;
53165333
QCOMPARE( def->supportedOutputVectorLayerExtensions().count(), 2 );
@@ -5423,6 +5440,15 @@ void TestQgsProcessing::parameterRasterOut()
54235440
QCOMPARE( fromCode->flags(), def->flags() );
54245441
QCOMPARE( fromCode->defaultValue(), def->defaultValue() );
54255442

5443+
DummyProvider3 provider;
5444+
QString error;
5445+
QVERIFY( !provider.isSupportedOutputValue( "d:/test.tif", def.get(), context, error ) );
5446+
QVERIFY( !provider.isSupportedOutputValue( "d:/test.TIF", def.get(), context, error ) );
5447+
QVERIFY( !provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.tif" ), def.get(), context, error ) );
5448+
QVERIFY( provider.isSupportedOutputValue( "d:/test.mig", def.get(), context, error ) );
5449+
QVERIFY( provider.isSupportedOutputValue( "d:/test.MIG", def.get(), context, error ) );
5450+
QVERIFY( provider.isSupportedOutputValue( QgsProcessingOutputLayerDefinition( "d:/test.MIG" ), def.get(), context, error ) );
5451+
54265452
// test layers to load on completion
54275453
def.reset( new QgsProcessingParameterRasterDestination( "x", QStringLiteral( "desc" ), QStringLiteral( "default.tif" ), true ) );
54285454
QgsProcessingOutputLayerDefinition fs = QgsProcessingOutputLayerDefinition( QStringLiteral( "test.tif" ) );

0 commit comments

Comments
 (0)