From b1da0810c2dbb125f148f3a04d1df6cb0874d100 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 2 Sep 2020 18:47:10 +0200 Subject: [PATCH 1/9] WFS-T 1.1.0 client implementation --- src/providers/wfs/qgswfsprovider.cpp | 107 +++- tests/src/python/test_provider_wfs.py | 72 +++ .../wfst-1-1/describefeaturetype_polygons.xml | 15 + .../provider/wfst-1-1/getcapabilities.xml | 474 ++++++++++++++++++ ...ransaction_add_features_polygons_empty.xml | 8 + .../wfst-1-1/transaction_response_empty.xml | 19 + .../transaction_response_feature_added.xml | 20 + .../transaction_response_feature_changed.xml | 14 + .../transaction_response_feature_deleted.xml | 14 + 9 files changed, 729 insertions(+), 14 deletions(-) create mode 100644 tests/testdata/provider/wfst-1-1/describefeaturetype_polygons.xml create mode 100644 tests/testdata/provider/wfst-1-1/getcapabilities.xml create mode 100644 tests/testdata/provider/wfst-1-1/transaction_add_features_polygons_empty.xml create mode 100644 tests/testdata/provider/wfst-1-1/transaction_response_empty.xml create mode 100644 tests/testdata/provider/wfst-1-1/transaction_response_feature_added.xml create mode 100644 tests/testdata/provider/wfst-1-1/transaction_response_feature_changed.xml create mode 100644 tests/testdata/provider/wfst-1-1/transaction_response_feature_deleted.xml diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 541e342a079..178ef58684a 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -1264,6 +1264,8 @@ bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields QByteArray response = describeFeatureType.response(); + QgsDebugMsgLevel( response, 4 ); + QDomDocument describeFeatureDocument; QString errorMsg; if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) ) @@ -1580,6 +1582,8 @@ bool QgsWFSProvider::sendTransactionDocument( const QDomDocument &doc, QDomDocum return false; } + QgsDebugMsgLevel( doc.toString(), 4 ); + QgsWFSTransactionRequest request( mShared->mURI ); return request.send( doc, serverResponse ); } @@ -1587,10 +1591,16 @@ bool QgsWFSProvider::sendTransactionDocument( const QDomDocument &doc, QDomDocum QDomElement QgsWFSProvider::createTransactionElement( QDomDocument &doc ) const { QDomElement transactionElem = doc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Transaction" ) ); - // QString WfsVersion = mShared->mWFSVersion; - // For now: hardcoded to 1.0.0 - QString WfsVersion = QStringLiteral( "1.0.0" ); - transactionElem.setAttribute( QStringLiteral( "version" ), WfsVersion ); + const QString WfsVersion = mShared->mWFSVersion; + // only 1.1.0 and 1.0.0 are supported + if ( WfsVersion == QStringLiteral( "1.1.0" ) ) + { + transactionElem.setAttribute( QStringLiteral( "version" ), WfsVersion ); + } + else + { + transactionElem.setAttribute( QStringLiteral( "version" ), QStringLiteral( "1.0.0" ) ); + } transactionElem.setAttribute( QStringLiteral( "service" ), QStringLiteral( "WFS" ) ); transactionElem.setAttribute( QStringLiteral( "xmlns:xsi" ), QStringLiteral( "http://www.w3.org/2001/XMLSchema-instance" ) ); @@ -1634,19 +1644,70 @@ bool QgsWFSProvider::transactionSuccess( const QDomDocument &serverResponse ) co return false; } - QDomNodeList transactionResultList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionResult" ) ); - if ( transactionResultList.size() < 1 ) + const QString WfsVersion = mShared->mWFSVersion; + + if ( WfsVersion == QStringLiteral( "1.1.0" ) ) { + const QDomNodeList transactionSummaryList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionSummary" ) ); + if ( transactionSummaryList.size() < 1 ) + { + return false; + } + + QDomElement transactionElement { transactionSummaryList.at( 0 ).toElement() }; + QDomNodeList totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalInserted" ) ); + QDomNodeList totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalUpdated" ) ); + QDomNodeList totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalDeleted" ) ); + if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 ) + { + return true; + } + if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 ) + { + return true; + } + if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 ) + { + return true; + } + + // Handle wrong QGIS server response (capital initial letter) + totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalInserted" ) ); + totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalUpdated" ) ); + totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalDeleted" ) ); + if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 ) + { + return true; + } + if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 ) + { + return true; + } + if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 ) + { + return true; + } + return false; + + } + else + { + const QDomNodeList transactionResultList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionResult" ) ); + if ( transactionResultList.size() < 1 ) + { + return false; + } + + const QDomNodeList statusList = transactionResultList.at( 0 ).toElement().elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Status" ) ); + if ( statusList.size() < 1 ) + { + return false; + } + + return statusList.at( 0 ).firstChildElement().localName() == QLatin1String( "SUCCESS" ); } - QDomNodeList statusList = transactionResultList.at( 0 ).toElement().elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Status" ) ); - if ( statusList.size() < 1 ) - { - return false; - } - - return statusList.at( 0 ).firstChildElement().localName() == QLatin1String( "SUCCESS" ); } QStringList QgsWFSProvider::insertedFeatureIds( const QDomDocument &serverResponse ) const @@ -1663,7 +1724,17 @@ QStringList QgsWFSProvider::insertedFeatureIds( const QDomDocument &serverRespon return ids; } - QDomNodeList insertResultList = rootElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "InsertResult" ) ); + // Handles WFS 1.1.0 + QString insertResultTagName; + if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) + { + insertResultTagName = QStringLiteral( "InsertResults" ); + } + else + { + insertResultTagName = QStringLiteral( "InsertResult" ); + } + QDomNodeList insertResultList = rootElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, insertResultTagName ); for ( int i = 0; i < insertResultList.size(); ++i ) { QDomNodeList featureIdList = insertResultList.at( i ).toElement().elementsByTagNameNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) ); @@ -1850,6 +1921,14 @@ void QgsWFSProvider::handleException( const QDomDocument &serverResponse ) return; } + // WFS 1.1.0 + if ( exceptionElem.tagName() == QLatin1String( "TransactionResponse" ) ) + { + pushError( tr( "Unsuccessful service response: no features were added, deleted or changed." ) ); + return; + } + + if ( exceptionElem.tagName() == QLatin1String( "ExceptionReport" ) ) { QDomElement exception = exceptionElem.firstChildElement( QStringLiteral( "Exception" ) ); diff --git a/tests/src/python/test_provider_wfs.py b/tests/src/python/test_provider_wfs.py index f5143349263..3d044444082 100644 --- a/tests/src/python/test_provider_wfs.py +++ b/tests/src/python/test_provider_wfs.py @@ -43,6 +43,9 @@ from qgis.testing import (start_app, unittest ) from providertestbase import ProviderTestCase +from utilities import unitTestDataPath + +TEST_DATA_DIR = unitTestDataPath() def sanitize(endpoint, x): @@ -4705,6 +4708,75 @@ Can't recognize service requested. self.assertEqual(len(logger.messages()), 1, logger.messages()) self.assertTrue("InvalidFormat: Can't recognize service requested." in logger.messages()[0].decode('UTF-8'), logger.messages()) + def testWFST11(self): + """Test WFS-T 1.1 (read-write)""" + + endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_T_1_1_transaction' + + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'getcapabilities.xml'), sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'describefeaturetype_polygons.xml'), sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=ws1:polygons')) + + vl = QgsVectorLayer("url='http://" + endpoint + "' typename='ws1:polygons' version='1.1.0'", 'test', 'WFS') + self.assertTrue(vl.isValid()) + self.assertEqual(vl.featureCount(), 0) + + self.assertEqual(vl.dataProvider().capabilities(), + QgsVectorDataProvider.AddFeatures + | QgsVectorDataProvider.ChangeAttributeValues + | QgsVectorDataProvider.ChangeGeometries + | QgsVectorDataProvider.DeleteFeatures + | QgsVectorDataProvider.SelectAtId) + + # Transaction response failure (no modifications) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_empty.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=')) + + (ret, _) = vl.dataProvider().addFeatures([QgsFeature()]) + self.assertFalse(ret) + + self.assertEqual(vl.featureCount(), 0) + + self.assertFalse(vl.dataProvider().deleteFeatures([0])) + + self.assertEqual(vl.featureCount(), 0) + + self.assertFalse(vl.dataProvider().changeGeometryValues({0: QgsGeometry.fromWkt('Polygon ((9 45, 10 45, 10 46, 9 46, 9 45))')})) + + self.assertFalse(vl.dataProvider().changeAttributeValues({0: {0: 0}})) + + # Test add features for real + # Transaction response with 1 feature added + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one19,45 10,45 10,46 9,46 9,45')) + + feat = QgsFeature(vl.fields()) + feat.setAttribute('name', 'one') + feat.setAttribute('value', 1) + feat.setGeometry(QgsGeometry.fromWkt('Polygon ((9 45, 10 45, 10 46, 9 46, 9 45))')) + (ret, features) = vl.dataProvider().addFeatures([feat]) + self.assertEqual(features[0].attributes(), ['one', 1]) + self.assertEqual(vl.featureCount(), 1) + + # Test change attributes + # Transaction response with 1 feature changed + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:nameone-one-onews1:value111')) + + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {0: 'one-one-one', 1: 111}})) + self.assertEqual(next(vl.dataProvider().getFeatures()).attributes(), ['one-one-one', 111]) + + # Test change geometry + # Transaction response with 1 feature changed + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry10,46 11,46 11,47 10,47 10,46')) + + new_geom = QgsGeometry.fromWkt('Polygon ((10 46, 11 46, 11 47, 10 47, 10 46))') + self.assertTrue(vl.dataProvider().changeGeometryValues({1: new_geom})) + self.assertEqual(next(vl.dataProvider().getFeatures()).geometry().asWkt(), new_geom.asWkt()) + + # Test delete feature + # Transaction response with 1 feature deleted + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_deleted.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=')) + + self.assertTrue(vl.dataProvider().deleteFeatures([1])) + self.assertEqual(vl.featureCount(), 0) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/provider/wfst-1-1/describefeaturetype_polygons.xml b/tests/testdata/provider/wfst-1-1/describefeaturetype_polygons.xml new file mode 100644 index 00000000000..c3c90476f93 --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/describefeaturetype_polygons.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/testdata/provider/wfst-1-1/getcapabilities.xml b/tests/testdata/provider/wfst-1-1/getcapabilities.xml new file mode 100644 index 00000000000..78c6ba98661 --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/getcapabilities.xml @@ -0,0 +1,474 @@ + + + + + + + WFS + 1.1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + Query + Insert + Update + Delete + + + ws1:polygons + polygons + + + features + polygons + + urn:x-ogc:def:crs:EPSG:4326 + + -180.0 -90.0 + 180.0 90.0 + + + + + + + gml:Envelope + gml:Point + gml:LineString + gml:Polygon + + + + + + + + + + + + + + + + + + + LessThan + GreaterThan + LessThanEqualTo + GreaterThanEqualTo + EqualTo + NotEqualTo + Like + Between + NullCheck + + + + + + abs + abs_2 + abs_3 + abs_4 + acos + AddCoverages + Affine + Aggregate + area + area2 + AreaGrid + asin + atan + atan2 + attributeCount + BandMerge + BandSelect + BarnesSurface + between + boundary + boundaryDimension + boundedBy + Bounds + buffer + BufferFeatureCollection + bufferWithSegments + Categorize + ceil + centroid + classify + ClassifyByRange + Clip + CollectGeometries + Collection_Average + Collection_Bounds + Collection_Count + Collection_Max + Collection_Median + Collection_Min + Collection_Nearest + Collection_Sum + Collection_Unique + Concatenate + contains + Contour + contrast + convert + convexHull + ConvolveCoverage + cos + Count + CoverageClassStats + CropCoverage + crosses + darken + dateDifference + dateFormat + dateParse + densify + desaturate + difference + dimension + disjoint + disjoint3D + distance + distance3D + double2bool + endAngle + endPoint + env + envelope + EqualArea + EqualInterval + equalsExact + equalsExactTolerance + equalTo + exp + exteriorRing + Feature + FeatureClassStats + floor + geometry + geometryType + geomFromWKT + geomLength + GeorectifyCoverage + GetFullCoverage + getGeometryN + getX + getY + getz + grayscale + greaterEqualThan + greaterThan + Grid + Heatmap + hsl + id + IEEEremainder + if_then_else + Import + in + in10 + in2 + in3 + in4 + in5 + in6 + in7 + in8 + in9 + inArray + InclusionFeatureCollection + int2bbool + int2ddouble + interiorPoint + interiorRingN + Interpolate + intersection + IntersectionFeatureCollection + intersects + intersects3D + isClosed + isCoverage + isEmpty + isInstanceOf + isLike + isNull + isometric + isRing + isSimple + isValid + isWithinDistance + isWithinDistance3D + Jenks + Jiffle + jsonPointer + labelPoint + lapply + length + lessEqualThan + lessThan + lighten + list + listMultiply + litem + literate + log + LRSGeocode + LRSMeasure + LRSSegment + max + max_2 + max_3 + max_4 + min + min_2 + min_3 + min_4 + mincircle + minimumdiameter + minrectangle + mix + modulo + MultiplyCoverages + Nearest + NormalizeCoverage + not + notEqualTo + numberFormat + numberFormat2 + numGeometries + numInteriorRing + numPoints + octagonalenvelope + offset + overlaps + PagedUnique + parameter + parseBoolean + parseDouble + parseInt + parseLong + pgNearest + pi + PointBuffers + pointN + PointStacker + PolygonExtraction + polygonize + PolyLabeller + pow + property + PropertyExists + Quantile + Query + random + RangeLookup + RasterAsPointCollection + RasterZonalStatistics + RasterZonalStatistics2 + Recode + RectangularClip + relate + relatePattern + reproject + ReprojectGeometry + rescaleToPixels + rint + round + round_2 + roundDouble + saturate + ScaleCoverage + setCRS + shade + simplify + sin + size + Snap + spin + splitPolygon + sqrt + StandardDeviation + startAngle + startPoint + StoreCoverage + strAbbreviate + strCapitalize + strConcat + strDefaultIfBlank + strEndsWith + strEqualsIgnoreCase + strIndexOf + stringTemplate + strLastIndexOf + strLength + strMatches + strPosition + strReplace + strStartsWith + strStripAccents + strSubstring + strSubstringStart + strToLowerCase + strToUpperCase + strTrim + strTrim2 + strURLEncode + StyleCoverage + symDifference + tan + tint + toDegrees + toRadians + touches + toWKT + Transform + TransparencyFill + union + UnionFeatureCollection + Unique + UniqueInterval + VectorToRaster + VectorZonalStatistics + vertices + within + + + + + + + + + + diff --git a/tests/testdata/provider/wfst-1-1/transaction_add_features_polygons_empty.xml b/tests/testdata/provider/wfst-1-1/transaction_add_features_polygons_empty.xml new file mode 100644 index 00000000000..0c0efb89416 --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/transaction_add_features_polygons_empty.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/tests/testdata/provider/wfst-1-1/transaction_response_empty.xml b/tests/testdata/provider/wfst-1-1/transaction_response_empty.xml new file mode 100644 index 00000000000..1286de3896d --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/transaction_response_empty.xml @@ -0,0 +1,19 @@ + + + + 0 + 0 + 0 + + + + + + + \ No newline at end of file diff --git a/tests/testdata/provider/wfst-1-1/transaction_response_feature_added.xml b/tests/testdata/provider/wfst-1-1/transaction_response_feature_added.xml new file mode 100644 index 00000000000..d5842f95476 --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/transaction_response_feature_added.xml @@ -0,0 +1,20 @@ + + + + 1 + 0 + 0 + + + + + + + + \ No newline at end of file diff --git a/tests/testdata/provider/wfst-1-1/transaction_response_feature_changed.xml b/tests/testdata/provider/wfst-1-1/transaction_response_feature_changed.xml new file mode 100644 index 00000000000..fb79a9f65ca --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/transaction_response_feature_changed.xml @@ -0,0 +1,14 @@ + + + + 0 + 1 + 0 + + \ No newline at end of file diff --git a/tests/testdata/provider/wfst-1-1/transaction_response_feature_deleted.xml b/tests/testdata/provider/wfst-1-1/transaction_response_feature_deleted.xml new file mode 100644 index 00000000000..458ecd758ce --- /dev/null +++ b/tests/testdata/provider/wfst-1-1/transaction_response_feature_deleted.xml @@ -0,0 +1,14 @@ + + + + 0 + 0 + 1 + + \ No newline at end of file From 1b80ca33351af9d3a769abc2383d9e6711a2a98e Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 3 Sep 2020 09:45:24 +0200 Subject: [PATCH 2/9] WFS-T 1.1.0 uses GML3 --- src/providers/wfs/qgswfsprovider.cpp | 22 ++++++++++++++++++++-- tests/src/python/test_provider_wfs.py | 4 ++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 178ef58684a..81a15aa382d 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -877,7 +877,16 @@ bool QgsWFSProvider::addFeatures( QgsFeatureList &flist, Flags flags ) { the_geom.convertToMultiType(); } - QDomElement gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc ); + QDomElement gmlElem; + // WFS 1.1.0 uses GML 3 + if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) + { + gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML3" ) ); + } + else + { + gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML2" ) ); + } if ( !gmlElem.isNull() ) { gmlElem.setAttribute( QStringLiteral( "srsName" ), crs().authid() ); @@ -1045,7 +1054,16 @@ bool QgsWFSProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) nameElem.appendChild( nameText ); propertyElem.appendChild( nameElem ); QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) ); - QDomElement gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc ); + QDomElement gmlElem; + // WFS 1.1.0 uses GML 3 + if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) + { + gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML3" ) ); + } + else + { + gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML2" ) ); + } gmlElem.setAttribute( QStringLiteral( "srsName" ), crs().authid() ); valueElem.appendChild( gmlElem ); propertyElem.appendChild( valueElem ); diff --git a/tests/src/python/test_provider_wfs.py b/tests/src/python/test_provider_wfs.py index 3d044444082..ce3e55a6e4d 100644 --- a/tests/src/python/test_provider_wfs.py +++ b/tests/src/python/test_provider_wfs.py @@ -4745,7 +4745,7 @@ Can't recognize service requested. # Test add features for real # Transaction response with 1 feature added - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one19,45 10,45 10,46 9,46 9,45')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one19 45 10 45 10 46 9 46 9 45')) feat = QgsFeature(vl.fields()) feat.setAttribute('name', 'one') @@ -4764,7 +4764,7 @@ Can't recognize service requested. # Test change geometry # Transaction response with 1 feature changed - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry10,46 11,46 11,47 10,47 10,46')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry10 46 11 46 11 47 10 47 10 46')) new_geom = QgsGeometry.fromWkt('Polygon ((10 46, 11 46, 11 47, 10 47, 10 46))') self.assertTrue(vl.dataProvider().changeGeometryValues({1: new_geom})) From 6ec7919b4357a47b5924cbcde0e03d5024fc25d1 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 14 Oct 2020 15:07:44 +0200 Subject: [PATCH 3/9] WFS-T 1.1.0 thanks to ESRI use coordinates Apparently ESRI mapserver does not like pos and posList for coordinates in GML3 but only accepts "coordinates". --- src/providers/wfs/qgswfsprovider.cpp | 11 ++++++----- tests/src/python/test_provider_wfs.py | 12 ++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 81a15aa382d..fa3354ccc0e 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -878,12 +878,13 @@ bool QgsWFSProvider::addFeatures( QgsFeatureList &flist, Flags flags ) the_geom.convertToMultiType(); } QDomElement gmlElem; - // WFS 1.1.0 uses GML 3 + // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 + /* if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) { gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML3" ) ); } - else + else */ { gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML2" ) ); } @@ -1055,12 +1056,12 @@ bool QgsWFSProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) propertyElem.appendChild( nameElem ); QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) ); QDomElement gmlElem; - // WFS 1.1.0 uses GML 3 - if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) + // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 + /* if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) { gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML3" ) ); } - else + else */ { gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML2" ) ); } diff --git a/tests/src/python/test_provider_wfs.py b/tests/src/python/test_provider_wfs.py index ce3e55a6e4d..e06d856a0fc 100644 --- a/tests/src/python/test_provider_wfs.py +++ b/tests/src/python/test_provider_wfs.py @@ -4728,24 +4728,19 @@ Can't recognize service requested. | QgsVectorDataProvider.SelectAtId) # Transaction response failure (no modifications) - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_empty.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_empty.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<_Insert><_Transaction>')) (ret, _) = vl.dataProvider().addFeatures([QgsFeature()]) self.assertFalse(ret) - self.assertEqual(vl.featureCount(), 0) - self.assertFalse(vl.dataProvider().deleteFeatures([0])) - self.assertEqual(vl.featureCount(), 0) - self.assertFalse(vl.dataProvider().changeGeometryValues({0: QgsGeometry.fromWkt('Polygon ((9 45, 10 45, 10 46, 9 46, 9 45))')})) - self.assertFalse(vl.dataProvider().changeAttributeValues({0: {0: 0}})) # Test add features for real # Transaction response with 1 feature added - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one19 45 10 45 10 46 9 46 9 45')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one<_name>1<_value>9,45 10,45 10,46 9,46 9,45<_gml:coordinates><_gml:LinearRing><_gml:outerBoundaryIs><_gml:Polygon><_geometry><_polygons><_Insert><_Transaction>')) feat = QgsFeature(vl.fields()) feat.setAttribute('name', 'one') @@ -4764,9 +4759,10 @@ Can't recognize service requested. # Test change geometry # Transaction response with 1 feature changed - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry10 46 11 46 11 47 10 47 10 46')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry<_Name>10,46 11,46 11,47 10,47 10,46<_gml:coordinates><_gml:LinearRing><_gml:outerBoundaryIs><_gml:Polygon><_Value><_Property><_Filter><_Update><_Transaction>')) new_geom = QgsGeometry.fromWkt('Polygon ((10 46, 11 46, 11 47, 10 47, 10 46))') + self.assertTrue(vl.dataProvider().changeGeometryValues({1: new_geom})) self.assertEqual(next(vl.dataProvider().getFeatures()).geometry().asWkt(), new_geom.asWkt()) From f29f86fab41531242ea3abc4a479b08df440241a Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 15 Oct 2020 15:54:13 +0200 Subject: [PATCH 4/9] Try special treatment for arcgis --- src/providers/wfs/qgswfsprovider.cpp | 9 ++++----- src/providers/wfs/qgswfsshareddata.cpp | 1 + src/providers/wfs/qgswfsshareddata.h | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index fa3354ccc0e..ecfa8048401 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -879,12 +879,11 @@ bool QgsWFSProvider::addFeatures( QgsFeatureList &flist, Flags flags ) } QDomElement gmlElem; // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 - /* - if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) + if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) && ! mShared->mServerPrefersCoordinatesForTransactions_1_1 ) { gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML3" ) ); } - else */ + else { gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML2" ) ); } @@ -1057,11 +1056,11 @@ bool QgsWFSProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) ); QDomElement gmlElem; // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 - /* if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) ) + if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) && ! mShared->mServerPrefersCoordinatesForTransactions_1_1 ) { gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML3" ) ); } - else */ + else { gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML2" ) ); } diff --git a/src/providers/wfs/qgswfsshareddata.cpp b/src/providers/wfs/qgswfsshareddata.cpp index 4e3aa27c329..e781a41bbb8 100644 --- a/src/providers/wfs/qgswfsshareddata.cpp +++ b/src/providers/wfs/qgswfsshareddata.cpp @@ -27,6 +27,7 @@ QgsWFSSharedData::QgsWFSSharedData( const QString &uri ) , mURI( uri ) { mHideProgressDialog = mURI.hideDownloadProgressDialog(); + mServerPrefersCoordinatesForTransactions_1_1 = uri.contains( QLatin1String( "/arcgis/" ), Qt::CaseSensitivity::CaseInsensitive ); } QgsWFSSharedData::~QgsWFSSharedData() diff --git a/src/providers/wfs/qgswfsshareddata.h b/src/providers/wfs/qgswfsshareddata.h index e01b37971ab..5ecc1293d0a 100644 --- a/src/providers/wfs/qgswfsshareddata.h +++ b/src/providers/wfs/qgswfsshareddata.h @@ -89,6 +89,11 @@ class QgsWFSSharedData : public QObject, public QgsBackgroundCachedSharedData */ bool mGetFeatureEPSGDotHonoursEPSGOrder = false; + /** + * Server (typically ESRI) does not like pos and posList, and wants "coordinates" for WFS 1.1 transactions + */ + bool mServerPrefersCoordinatesForTransactions_1_1 = false; + //! Geometry type of the features in this layer QgsWkbTypes::Type mWKBType = QgsWkbTypes::Unknown; From 9c0a620fa80f77e3d711ebfcbf5e64ec1977a45d Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 15 Oct 2020 19:36:17 +0200 Subject: [PATCH 5/9] Add option for ServerPrefersCoordinatesForTransactions_1_1 --- .../qgsnewhttpconnection.sip.in | 1 + src/gui/qgsnewhttpconnection.cpp | 9 ++ src/gui/qgsnewhttpconnection.h | 6 ++ src/providers/wfs/qgswfsconnection.cpp | 7 ++ src/providers/wfs/qgswfsconstants.cpp | 2 + src/providers/wfs/qgswfsconstants.h | 2 + src/providers/wfs/qgswfsdatasourceuri.cpp | 6 ++ src/providers/wfs/qgswfsdatasourceuri.h | 3 + src/providers/wfs/qgswfsshareddata.h | 2 +- src/ui/qgsnewhttpconnectionbase.ui | 84 +++++++++++-------- 10 files changed, 84 insertions(+), 38 deletions(-) diff --git a/python/gui/auto_generated/qgsnewhttpconnection.sip.in b/python/gui/auto_generated/qgsnewhttpconnection.sip.in index e478fd34dc7..0f27c1b3949 100644 --- a/python/gui/auto_generated/qgsnewhttpconnection.sip.in +++ b/python/gui/auto_generated/qgsnewhttpconnection.sip.in @@ -108,6 +108,7 @@ Returns the "test connection" button. + virtual QString wfsSettingsKey( const QString &base, const QString &connectionName ) const; %Docstring Returns the QSettings key for WFS related settings for the connection. diff --git a/src/gui/qgsnewhttpconnection.cpp b/src/gui/qgsnewhttpconnection.cpp index 3e1eb2d1c5c..fcdc8c9a51d 100644 --- a/src/gui/qgsnewhttpconnection.cpp +++ b/src/gui/qgsnewhttpconnection.cpp @@ -166,6 +166,7 @@ void QgsNewHttpConnection::wfsVersionCurrentIndexChanged( int index ) txtPageSize->setEnabled( cbxWfsFeaturePaging->isChecked() && ( index == WFS_VERSION_MAX || index >= WFS_VERSION_1_1 ) ); cbxWfsIgnoreAxisOrientation->setEnabled( index != WFS_VERSION_1_0 && index != WFS_VERSION_API_FEATURES_1_0 ); cbxWfsInvertAxisOrientation->setEnabled( index != WFS_VERSION_API_FEATURES_1_0 ); + wfsUseGml2EncodingForTransactions()->setEnabled( index == WFS_VERSION_1_1 ); } void QgsNewHttpConnection::wfsFeaturePagingStateChanged( int state ) @@ -256,6 +257,11 @@ QCheckBox *QgsNewHttpConnection::wfsPagingEnabledCheckBox() return cbxWfsFeaturePaging; } +QCheckBox *QgsNewHttpConnection::wfsUseGml2EncodingForTransactions() +{ + return cbxWfsUseGml2EncodingForTransactions; +} + QLineEdit *QgsNewHttpConnection::wfsPageSizeLineEdit() { return txtPageSize; @@ -281,6 +287,8 @@ void QgsNewHttpConnection::updateServiceSpecificSettings() cbxWmsIgnoreReportedLayerExtents->setChecked( settings.value( wmsKey + QStringLiteral( "/ignoreReportedLayerExtents" ), false ).toBool() ); cbxWfsIgnoreAxisOrientation->setChecked( settings.value( wfsKey + "/ignoreAxisOrientation", false ).toBool() ); cbxWfsInvertAxisOrientation->setChecked( settings.value( wfsKey + "/invertAxisOrientation", false ).toBool() ); + cbxWfsUseGml2EncodingForTransactions->setChecked( settings.value( wfsKey + "/preferCoordinatesForWfsT11", false ).toBool() ); + cbxWmsIgnoreAxisOrientation->setChecked( settings.value( wmsKey + "/ignoreAxisOrientation", false ).toBool() ); cbxWmsInvertAxisOrientation->setChecked( settings.value( wmsKey + "/invertAxisOrientation", false ).toBool() ); cbxIgnoreGetFeatureInfoURI->setChecked( settings.value( wmsKey + "/ignoreGetFeatureInfoURI", false ).toBool() ); @@ -463,6 +471,7 @@ void QgsNewHttpConnection::accept() { settings.setValue( wfsKey + "/ignoreAxisOrientation", cbxWfsIgnoreAxisOrientation->isChecked() ); settings.setValue( wfsKey + "/invertAxisOrientation", cbxWfsInvertAxisOrientation->isChecked() ); + settings.setValue( wfsKey + "/preferCoordinatesForWfsT11", cbxWfsUseGml2EncodingForTransactions->isChecked() ); } if ( mTypes & ConnectionWms || mTypes & ConnectionWcs ) { diff --git a/src/gui/qgsnewhttpconnection.h b/src/gui/qgsnewhttpconnection.h index 1978fb19fb5..5a5860d2f4f 100644 --- a/src/gui/qgsnewhttpconnection.h +++ b/src/gui/qgsnewhttpconnection.h @@ -150,6 +150,12 @@ class GUI_EXPORT QgsNewHttpConnection : public QDialog, private Ui::QgsNewHttpCo */ QCheckBox *wfsPagingEnabledCheckBox() SIP_SKIP; + /** + * Returns the "Use GML2 encoding for transactions" checkbox + * \since QGIS 3.16 + */ + QCheckBox *wfsUseGml2EncodingForTransactions() SIP_SKIP; + /** * Returns the "WFS page size" edit * \since QGIS 3.2 diff --git a/src/providers/wfs/qgswfsconnection.cpp b/src/providers/wfs/qgswfsconnection.cpp index 4e28fd4aa44..fb2c61f391a 100644 --- a/src/providers/wfs/qgswfsconnection.cpp +++ b/src/providers/wfs/qgswfsconnection.cpp @@ -53,6 +53,13 @@ QgsWfsConnection::QgsWfsConnection( const QString &connName ) settings.value( key + "/" + QgsWFSConstants::SETTINGS_PAGING_ENABLED, true ).toBool() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); } + if ( settings.contains( key + "/" + QgsWFSConstants::SETTINGS_WFST_1_1_PREFER_COORDINATES ) ) + { + mUri.removeParam( QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES ); // setParam allow for duplicates! + mUri.setParam( QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES, + settings.value( key + "/" + QgsWFSConstants::SETTINGS_WFST_1_1_PREFER_COORDINATES, true ).toBool() ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); + } + QgsDebugMsgLevel( QStringLiteral( "WFS full uri: '%1'." ).arg( QString( mUri.uri() ) ), 4 ); } diff --git a/src/providers/wfs/qgswfsconstants.cpp b/src/providers/wfs/qgswfsconstants.cpp index 2f2067acbfd..9bb7beb5b8b 100644 --- a/src/providers/wfs/qgswfsconstants.cpp +++ b/src/providers/wfs/qgswfsconstants.cpp @@ -40,6 +40,7 @@ const QString QgsWFSConstants::URI_PARAM_VALIDATESQLFUNCTIONS( QStringLiteral( " const QString QgsWFSConstants::URI_PARAM_HIDEDOWNLOADPROGRESSDIALOG( QStringLiteral( "hideDownloadProgressDialog" ) ); const QString QgsWFSConstants::URI_PARAM_PAGING_ENABLED( "pagingEnabled" ); const QString QgsWFSConstants::URI_PARAM_PAGE_SIZE( "pageSize" ); +const QString QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES( "preferCoordinatesForWfsT11" ); const QString QgsWFSConstants::VERSION_AUTO( QStringLiteral( "auto" ) ); @@ -48,3 +49,4 @@ const QString QgsWFSConstants::SETTINGS_VERSION( QStringLiteral( "version" ) ); const QString QgsWFSConstants::SETTINGS_MAXNUMFEATURES( QStringLiteral( "maxnumfeatures" ) ); const QString QgsWFSConstants::SETTINGS_PAGING_ENABLED( QStringLiteral( "pagingenabled" ) ); const QString QgsWFSConstants::SETTINGS_PAGE_SIZE( QStringLiteral( "pagesize" ) ); +const QString QgsWFSConstants::SETTINGS_WFST_1_1_PREFER_COORDINATES( QStringLiteral( "preferCoordinatesForWfsT11" ) ); diff --git a/src/providers/wfs/qgswfsconstants.h b/src/providers/wfs/qgswfsconstants.h index e1926f725f6..4a13d78c97d 100644 --- a/src/providers/wfs/qgswfsconstants.h +++ b/src/providers/wfs/qgswfsconstants.h @@ -48,6 +48,7 @@ struct QgsWFSConstants static const QString URI_PARAM_HIDEDOWNLOADPROGRESSDIALOG; static const QString URI_PARAM_PAGING_ENABLED; static const QString URI_PARAM_PAGE_SIZE; + static const QString URI_PARAM_WFST_1_1_PREFER_COORDINATES; // static const QString VERSION_AUTO; @@ -58,6 +59,7 @@ struct QgsWFSConstants static const QString SETTINGS_MAXNUMFEATURES; static const QString SETTINGS_PAGING_ENABLED; static const QString SETTINGS_PAGE_SIZE; + static const QString SETTINGS_WFST_1_1_PREFER_COORDINATES; }; #endif // QGSWFSCONSTANTS_H diff --git a/src/providers/wfs/qgswfsdatasourceuri.cpp b/src/providers/wfs/qgswfsdatasourceuri.cpp index dc37ace522c..4f56098988c 100644 --- a/src/providers/wfs/qgswfsdatasourceuri.cpp +++ b/src/providers/wfs/qgswfsdatasourceuri.cpp @@ -373,6 +373,12 @@ bool QgsWFSDataSourceURI::hideDownloadProgressDialog() const return mURI.hasParam( QgsWFSConstants::URI_PARAM_HIDEDOWNLOADPROGRESSDIALOG ); } + +bool QgsWFSDataSourceURI::preferCoordinatesForWfst11() const +{ + return mURI.hasParam( QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES ); +} + QString QgsWFSDataSourceURI::build( const QString &baseUri, const QString &typeName, const QString &crsString, diff --git a/src/providers/wfs/qgswfsdatasourceuri.h b/src/providers/wfs/qgswfsdatasourceuri.h index 2ccaefbd7ba..e3e09023ae3 100644 --- a/src/providers/wfs/qgswfsdatasourceuri.h +++ b/src/providers/wfs/qgswfsdatasourceuri.h @@ -113,6 +113,9 @@ class QgsWFSDataSourceURI //! Whether to hide download progress dialog in QGIS main app. Defaults to false bool hideDownloadProgressDialog() const; + //! Whether to use "ccordinates" instead of "pos" and "posList" for WFS-T 1.1 transactions (ESRI mapserver) + bool preferCoordinatesForWfst11() const; + //! Returns authorization parameters const QgsAuthorizationSettings &auth() const { return mAuth; } diff --git a/src/providers/wfs/qgswfsshareddata.h b/src/providers/wfs/qgswfsshareddata.h index 5ecc1293d0a..2a0f61e61ad 100644 --- a/src/providers/wfs/qgswfsshareddata.h +++ b/src/providers/wfs/qgswfsshareddata.h @@ -90,7 +90,7 @@ class QgsWFSSharedData : public QObject, public QgsBackgroundCachedSharedData bool mGetFeatureEPSGDotHonoursEPSGOrder = false; /** - * Server (typically ESRI) does not like pos and posList, and wants "coordinates" for WFS 1.1 transactions + * If the server (typically ESRI with WFS-T 1.1 in 2020) does not like "pos" and "posList", and requires "coordinates" for WFS 1.1 transactions */ bool mServerPrefersCoordinatesForTransactions_1_1 = false; diff --git a/src/ui/qgsnewhttpconnectionbase.ui b/src/ui/qgsnewhttpconnectionbase.ui index b5717dec65c..0b0f5e229d2 100644 --- a/src/ui/qgsnewhttpconnectionbase.ui +++ b/src/ui/qgsnewhttpconnectionbase.ui @@ -7,7 +7,7 @@ 0 0 448 - 761 + 815 @@ -173,9 +173,6 @@ - - - @@ -183,42 +180,45 @@ - - - - Max. number of features - - - - - - - <html><head/><body><p>Enter a number to limit the maximum number of features retrieved per feature request. If let to empty, no limit is set.</p></body></html> - - - - - - - Page size - - - - - - - <html><head/><body><p>Enter a number to limit the maximum number of features retrieved in a single GetFeature request when paging is enabled. If let to empty, server default will apply.</p></body></html> - - - - + Ignore axis orientation (WFS 1.1/WFS 2.0) - + + + + Max. number of features + + + + + + + + + + <html><head/><body><p>Enter a number to limit the maximum number of features retrieved in a single GetFeature request when paging is enabled. If let to empty, server default will apply.</p></body></html> + + + + + + + Invert axis orientation + + + + + + + Page size + + + + Enable feature paging @@ -228,10 +228,20 @@ - - + + + + <html><head/><body><p>Enter a number to limit the maximum number of features retrieved per feature request. If let to empty, no limit is set.</p></body></html> + + + + + + + <html><head/><body><p>This might be necessary on some <span style=" font-weight:600;">broken</span> ESRI map servers when using WFS-T 1.1.0.</p></body></html> + - Invert axis orientation + Use GML2 encoding for transactions From fd4319487f84c2f8ba287f7bcd426320c27513da Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 15 Oct 2020 21:34:12 +0200 Subject: [PATCH 6/9] Set prefer coordinates from uri --- src/providers/wfs/qgswfsshareddata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/wfs/qgswfsshareddata.cpp b/src/providers/wfs/qgswfsshareddata.cpp index e781a41bbb8..5fbbcc556c6 100644 --- a/src/providers/wfs/qgswfsshareddata.cpp +++ b/src/providers/wfs/qgswfsshareddata.cpp @@ -27,7 +27,7 @@ QgsWFSSharedData::QgsWFSSharedData( const QString &uri ) , mURI( uri ) { mHideProgressDialog = mURI.hideDownloadProgressDialog(); - mServerPrefersCoordinatesForTransactions_1_1 = uri.contains( QLatin1String( "/arcgis/" ), Qt::CaseSensitivity::CaseInsensitive ); + mServerPrefersCoordinatesForTransactions_1_1 = mURI.preferCoordinatesForWfst11(); } QgsWFSSharedData::~QgsWFSSharedData() From 785854c8db581546d356b90c46441f5d7826af72 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Fri, 16 Oct 2020 08:19:22 +0200 Subject: [PATCH 7/9] WFS-T Apply axis inversion logic --- src/core/qgsogcutils.cpp | 3 +- src/providers/wfs/qgswfsdatasourceuri.cpp | 3 +- src/providers/wfs/qgswfsdatasourceuri.h | 2 +- src/providers/wfs/qgswfsprovider.cpp | 70 ++++++++++++------ src/providers/wfs/qgswfsprovider.h | 5 ++ .../test_project_wms_grouped_layers.gpkg | Bin 692224 -> 692224 bytes 6 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/core/qgsogcutils.cpp b/src/core/qgsogcutils.cpp index d874e44191d..7a7625464d3 100644 --- a/src/core/qgsogcutils.cpp +++ b/src/core/qgsogcutils.cpp @@ -1173,7 +1173,8 @@ QDomElement QgsOgcUtils::geometryToGML( const QgsGeometry &geometry, QDomDocumen return geometryToGML( geometry, doc, ( format == QLatin1String( "GML2" ) ) ? GML_2_1_2 : GML_3_2_1, QString(), false, QString(), precision ); } -QDomElement QgsOgcUtils::geometryToGML( const QgsGeometry &geometry, QDomDocument &doc, +QDomElement QgsOgcUtils::geometryToGML( const QgsGeometry &geometry, + QDomDocument &doc, GMLVersion gmlVersion, const QString &srsName, bool invertAxisOrientation, diff --git a/src/providers/wfs/qgswfsdatasourceuri.cpp b/src/providers/wfs/qgswfsdatasourceuri.cpp index 4f56098988c..44f509c0dd6 100644 --- a/src/providers/wfs/qgswfsdatasourceuri.cpp +++ b/src/providers/wfs/qgswfsdatasourceuri.cpp @@ -376,7 +376,8 @@ bool QgsWFSDataSourceURI::hideDownloadProgressDialog() const bool QgsWFSDataSourceURI::preferCoordinatesForWfst11() const { - return mURI.hasParam( QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES ); + return mURI.hasParam( QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES ) && + mURI.param( QgsWFSConstants::URI_PARAM_WFST_1_1_PREFER_COORDINATES ).toUpper() == QLatin1String( "TRUE" ); } QString QgsWFSDataSourceURI::build( const QString &baseUri, diff --git a/src/providers/wfs/qgswfsdatasourceuri.h b/src/providers/wfs/qgswfsdatasourceuri.h index e3e09023ae3..7677b4c1756 100644 --- a/src/providers/wfs/qgswfsdatasourceuri.h +++ b/src/providers/wfs/qgswfsdatasourceuri.h @@ -113,7 +113,7 @@ class QgsWFSDataSourceURI //! Whether to hide download progress dialog in QGIS main app. Defaults to false bool hideDownloadProgressDialog() const; - //! Whether to use "ccordinates" instead of "pos" and "posList" for WFS-T 1.1 transactions (ESRI mapserver) + //! Whether to use "coordinates" instead of "pos" and "posList" for WFS-T 1.1 transactions (ESRI mapserver) bool preferCoordinatesForWfst11() const; //! Returns authorization parameters diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index ecfa8048401..137350bd7b7 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -787,6 +787,46 @@ void QgsWFSProvider::reloadProviderData() mShared->invalidateCache(); } +QDomElement QgsWFSProvider::geometryElement( const QgsGeometry &geometry, QDomDocument &transactionDoc ) +{ + QDomElement gmlElem; + + // Determine axis orientation and gml version + bool applyAxisInversion; + QgsOgcUtils::GMLVersion gmlVersion; + + if ( mShared->mWFSVersion.startsWith( QLatin1String( "1.1" ) ) ) + { + // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 + if ( ! mShared->mServerPrefersCoordinatesForTransactions_1_1 ) + { + gmlVersion = QgsOgcUtils::GML_3_1_0; + } + else + { + gmlVersion = QgsOgcUtils::GML_2_1_2; + } + applyAxisInversion = ( crs().hasAxisInverted() && ! mShared->mURI.ignoreAxisOrientation() ) + || mShared->mURI.invertAxisOrientation(); + } + else // 1.0 + { + gmlVersion = QgsOgcUtils::GML_2_1_2; + applyAxisInversion = mShared->mURI.invertAxisOrientation(); + } + + gmlElem = QgsOgcUtils::geometryToGML( + geometry, + transactionDoc, + gmlVersion, + crs().authid(), + applyAxisInversion, + QString() + ); + + return gmlElem; +} + QgsWkbTypes::Type QgsWFSProvider::wkbType() const { return mShared->mWKBType; @@ -877,19 +917,10 @@ bool QgsWFSProvider::addFeatures( QgsFeatureList &flist, Flags flags ) { the_geom.convertToMultiType(); } - QDomElement gmlElem; - // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 - if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) && ! mShared->mServerPrefersCoordinatesForTransactions_1_1 ) + + const QDomElement gmlElem { geometryElement( the_geom, transactionDoc ) }; + if ( ! gmlElem.isNull() ) { - gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML3" ) ); - } - else - { - gmlElem = QgsOgcUtils::geometryToGML( the_geom, transactionDoc, QLatin1String( "GML2" ) ); - } - if ( !gmlElem.isNull() ) - { - gmlElem.setAttribute( QStringLiteral( "srsName" ), crs().authid() ); geomElem.appendChild( gmlElem ); featureElem.appendChild( geomElem ); } @@ -1054,18 +1085,9 @@ bool QgsWFSProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) nameElem.appendChild( nameText ); propertyElem.appendChild( nameElem ); QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) ); - QDomElement gmlElem; - // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2 - if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) && ! mShared->mServerPrefersCoordinatesForTransactions_1_1 ) - { - gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML3" ) ); - } - else - { - gmlElem = QgsOgcUtils::geometryToGML( geomIt.value(), transactionDoc, QLatin1String( "GML2" ) ); - } - gmlElem.setAttribute( QStringLiteral( "srsName" ), crs().authid() ); - valueElem.appendChild( gmlElem ); + + valueElem.appendChild( geometryElement( geomIt.value(), transactionDoc ) ); + propertyElem.appendChild( valueElem ); updateElem.appendChild( propertyElem ); diff --git a/src/providers/wfs/qgswfsprovider.h b/src/providers/wfs/qgswfsprovider.h index add6ba1081b..d326be0758b 100644 --- a/src/providers/wfs/qgswfsprovider.h +++ b/src/providers/wfs/qgswfsprovider.h @@ -140,6 +140,11 @@ class QgsWFSProvider final: public QgsVectorDataProvider friend class QgsWFSFeatureSource; + /** + * Create the geometry element + */ + QDomElement geometryElement( const QgsGeometry &geometry, QDomDocument &transactionDoc ); + protected: //! String used to define a subset of the layer diff --git a/tests/testdata/qgis_server/test_project_wms_grouped_layers.gpkg b/tests/testdata/qgis_server/test_project_wms_grouped_layers.gpkg index 0dfc116091dd59e5d9862539791a39fb8c2b365b..f1aa289e04285fbfcd51dc13b584a747ba8d7dd5 100644 GIT binary patch delta 1200 zcmZXSeN0a#eIhHw)WG+AmO)(v1Qkcd=Tl1|2C?|PF$4-E+vOjtU|Aqg=ckmMa4d1}m z@zn^~izT9GrLF_BH}FGost;k0x}qLaefYR)jF456hzC(shtoQAuuLiPM>rpPRqORauyb zGkQOB+3Eu;R6iuY)YougZU3zkPJNm(jqp7F0#D;VX5(u5v5J2}YLxu$8k}y*;}Qnn zZA#@Ffqkdokn0O|ENo3M7>9x65Y1=>sMqb1p=6=ObtGqF2@Wb0uSpR{p zPuwPi1+%!w%3jnMB9>EL=nzHuII@L^Ra+F{OHn|GqvkYaDl(vQn%@8)7TBa^q;l+* z+SH#_k7?AnQ#mhB>M!Y*#OuNW=abVQgxoAn+u?$+Br@=c5E;`vZ$Sd5O;*63qEuZq zQutDYpHt^euNx(01-42J;$ESKFO7a$S|`|eZFv}EL6l2*B|`MP*3b>h9E;;VjgT;> z1k5i;NjoeM+(pfOVAWE3;ihPT&B%a8ki4K@*S#s`2zA`Y>J!E=q#GvCNQiWZ0iN7P z*G3_M;!#*K1ha`rO7dEv>o9FKKsxPWy*gYgyF9>_9IMf`Rr^P-x>YSTvSq;iH$Kg~)7_!mKe0 zo-+ik1aAz|_2R}L^k5I{q}#oqr{8viQM=Lu>8vZ8s4>n+1>=0MoN=UYQCH+dZ|@vZwa+F?Z&_>WlH&AO*{|K4KzFFocbcKf{` zj=!rMM_EMv<@b8NXV#P6nelIYHkZ9_?TvUA5d}%Czo+O-wwc4sbnfDJ!5>(2EF@-1 i8L?NLWntIu54N23+x{0GNY03IQ01Cz(JrjYCG=l7M|k4^ delta 812 zcmXw#Ye-Z<6vyxEoxS(myZ5pCMacDLRxV~HyKDAXindi#i@*1=W`zl|l$1$|lQ!-*C>EIrE?YY3k2x>d$O< zI&wIUyT=${$c%U_D~A=cQ^u?;1N{x#xtmleK{dK~Bg~SkCni4WTkV>{=>ct78`8=& zMN<;yih872uo;G-?&sOE1RJV?=R(1TQ=#Db3+F-&>$%tM*}KE-+2PJD^?H0>w=dV7 zz1y8zAyscHANVBIx0a9pKKW&$Ec*aLwK4Pbpx-|37wmE$r_bn*^-;ZV!d}v*lZRoX zB3H*~l^1P|nVaH%9L3cXSECm?a$TDWBZh^+Gcd!-5a?7`+}G~1bA_DcGWw_=*DOlZ z7LxA6b#X-KvbN%V{0Q&BU)hDqm}xvHghq^+>-3nJM!#ZshUMsPzvwJXgJuT_+h7Zs zk3kVhISeXkBw!^AHb^3scflZYop6X$DzKS6ibE$2*`Rx8I(th+Wt`rieb54GuaYlM zO2gu!Fl}AH@A+AFG{+re$~4apBO(*4To+3eGqKCP|HLxZB!>C`(cxmK*pR`jW6Z2 zpXv{lx2?#F(kyHcR;^cXHeZ@Bck#5e117O8>M=od7NkP6MA~k%8I}B<*7_Ke*X&U? zLBm1|A|Ua6#8OWO+QEWYR7Y5pM4oiP0!96>+DKPZr9CysMfbEwgil=epm-g|E+$hVJ$V From 27b64a0f1e0fb702e07dd4905e73ba324bbd934d Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Fri, 16 Oct 2020 11:09:56 +0200 Subject: [PATCH 8/9] Use srsName from shared data, fix tests --- src/providers/wfs/qgswfsprovider.cpp | 2 +- tests/src/python/test_provider_wfs.py | 10 +++------- .../test_project_wms_grouped_layers.gpkg | Bin 692224 -> 692224 bytes 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 137350bd7b7..69e2a7637ea 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -819,7 +819,7 @@ QDomElement QgsWFSProvider::geometryElement( const QgsGeometry &geometry, QDomDo geometry, transactionDoc, gmlVersion, - crs().authid(), + mShared->srsName(), applyAxisInversion, QString() ); diff --git a/tests/src/python/test_provider_wfs.py b/tests/src/python/test_provider_wfs.py index e06d856a0fc..505ba929ebb 100644 --- a/tests/src/python/test_provider_wfs.py +++ b/tests/src/python/test_provider_wfs.py @@ -4709,7 +4709,7 @@ Can't recognize service requested. self.assertTrue("InvalidFormat: Can't recognize service requested." in logger.messages()[0].decode('UTF-8'), logger.messages()) def testWFST11(self): - """Test WFS-T 1.1 (read-write)""" + """Test WFS-T 1.1 (read-write) taken from a geoserver session""" endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_T_1_1_transaction' @@ -4733,14 +4733,10 @@ Can't recognize service requested. (ret, _) = vl.dataProvider().addFeatures([QgsFeature()]) self.assertFalse(ret) self.assertEqual(vl.featureCount(), 0) - self.assertFalse(vl.dataProvider().deleteFeatures([0])) - self.assertEqual(vl.featureCount(), 0) - self.assertFalse(vl.dataProvider().changeGeometryValues({0: QgsGeometry.fromWkt('Polygon ((9 45, 10 45, 10 46, 9 46, 9 45))')})) - self.assertFalse(vl.dataProvider().changeAttributeValues({0: {0: 0}})) # Test add features for real # Transaction response with 1 feature added - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one<_name>1<_value>9,45 10,45 10,46 9,46 9,45<_gml:coordinates><_gml:LinearRing><_gml:outerBoundaryIs><_gml:Polygon><_geometry><_polygons><_Insert><_Transaction>')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=one<_name>1<_value>45 9 45 10 46 10 46 9 45 9<_gml:posList><_gml:LinearRing><_gml:exterior><_gml:Polygon><_geometry><_polygons><_Insert><_Transaction>')) feat = QgsFeature(vl.fields()) feat.setAttribute('name', 'one') @@ -4759,7 +4755,7 @@ Can't recognize service requested. # Test change geometry # Transaction response with 1 feature changed - shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry<_Name>10,46 11,46 11,47 10,47 10,46<_gml:coordinates><_gml:LinearRing><_gml:outerBoundaryIs><_gml:Polygon><_Value><_Property><_Filter><_Update><_Transaction>')) + shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=ws1:geometry<_Name>46 10 46 11 47 11 47 10 46 10<_gml:posList><_gml:LinearRing><_gml:exterior><_gml:Polygon><_Value><_Property><_Filter><_Update><_Transaction>')) new_geom = QgsGeometry.fromWkt('Polygon ((10 46, 11 46, 11 47, 10 47, 10 46))') diff --git a/tests/testdata/qgis_server/test_project_wms_grouped_layers.gpkg b/tests/testdata/qgis_server/test_project_wms_grouped_layers.gpkg index f1aa289e04285fbfcd51dc13b584a747ba8d7dd5..0dfc116091dd59e5d9862539791a39fb8c2b365b 100644 GIT binary patch delta 812 zcmXw#Ye-Z<6vyxEoxS(myZ5pCMacDLRxV~HyKDAXindi#i@*1=W`zl|l$1$|lQ!-*C>EIrE?YY3k2x>d$O< zI&wIUyT=${$c%U_D~A=cQ^u?;1N{x#xtmleK{dK~Bg~SkCni4WTkV>{=>ct78`8=& zMN<;yih872uo;G-?&sOE1RJV?=R(1TQ=#Db3+F-&>$%tM*}KE-+2PJD^?H0>w=dV7 zz1y8zAyscHANVBIx0a9pKKW&$Ec*aLwK4Pbpx-|37wmE$r_bn*^-;ZV!d}v*lZRoX zB3H*~l^1P|nVaH%9L3cXSECm?a$TDWBZh^+Gcd!-5a?7`+}G~1bA_DcGWw_=*DOlZ z7LxA6b#X-KvbN%V{0Q&BU)hDqm}xvHghq^+>-3nJM!#ZshUMsPzvwJXgJuT_+h7Zs zk3kVhISeXkBw!^AHb^3scflZYop6X$DzKS6ibE$2*`Rx8I(th+Wt`rieb54GuaYlM zO2gu!Fl}AH@A+AFG{+re$~4apBO(*4To+3eGqKCP|HLxZB!>C`(cxmK*pR`jW6Z2 zpXv{lx2?#F(kyHcR;^cXHeZ@Bck#5e117O8>M=od7NkP6MA~k%8I}B<*7_Ke*X&U? zLBm1|A|Ua6#8OWO+QEWYR7Y5pM4oiP0!96>+DKPZr9CysMfbEwgil=epm-g|E+$hVJ$V delta 1200 zcmZXSeN0a#eIhHw)WG+AmO)(v1Qkcd=Tl1|2C?|PF$4-E+vOjtU|Aqg=ckmMa4d1}m z@zn^~izT9GrLF_BH}FGost;k0x}qLaefYR)jF456hzC(shtoQAuuLiPM>rpPRqORauyb zGkQOB+3Eu;R6iuY)YougZU3zkPJNm(jqp7F0#D;VX5(u5v5J2}YLxu$8k}y*;}Qnn zZA#@Ffqkdokn0O|ENo3M7>9x65Y1=>sMqb1p=6=ObtGqF2@Wb0uSpR{p zPuwPi1+%!w%3jnMB9>EL=nzHuII@L^Ra+F{OHn|GqvkYaDl(vQn%@8)7TBa^q;l+* z+SH#_k7?AnQ#mhB>M!Y*#OuNW=abVQgxoAn+u?$+Br@=c5E;`vZ$Sd5O;*63qEuZq zQutDYpHt^euNx(01-42J;$ESKFO7a$S|`|eZFv}EL6l2*B|`MP*3b>h9E;;VjgT;> z1k5i;NjoeM+(pfOVAWE3;ihPT&B%a8ki4K@*S#s`2zA`Y>J!E=q#GvCNQiWZ0iN7P z*G3_M;!#*K1ha`rO7dEv>o9FKKsxPWy*gYgyF9>_9IMf`Rr^P-x>YSTvSq;iH$Kg~)7_!mKe0 zo-+ik1aAz|_2R}L^k5I{q}#oqr{8viQM=Lu>8vZ8s4>n+1>=0MoN=UYQCH+dZ|@vZwa+F?Z&_>WlH&AO*{|K4KzFFocbcKf{` zj=!rMM_EMv<@b8NXV#P6nelIYHkZ9_?TvUA5d}%Czo+O-wwc4sbnfDJ!5>(2EF@-1 i8L?NLWntIu54N23+x{0GNY03IQ01Cz(JrjYCG=l7M|k4^ From 9edf4825140425266fe6b2485f9592c93798543f Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Fri, 16 Oct 2020 14:38:04 +0200 Subject: [PATCH 9/9] Fix axis for QGIS server --- src/providers/wfs/qgswfsprovider.cpp | 5 +- tests/src/python/test_qgsserver_wfst.py | 70 ++++++++++++++++++++----- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 69e2a7637ea..9890f33849d 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -806,7 +806,10 @@ QDomElement QgsWFSProvider::geometryElement( const QgsGeometry &geometry, QDomDo { gmlVersion = QgsOgcUtils::GML_2_1_2; } - applyAxisInversion = ( crs().hasAxisInverted() && ! mShared->mURI.ignoreAxisOrientation() ) + // For servers like Geomedia and QGIS Server that advertise EPSG:XXXX in capabilities even in WFS 1.1 or 2.0 + // cpabilities useEPSGColumnFormat is set. + // We follow GeoServer convention here which is to treat EPSG:4326 as lon/lat + applyAxisInversion = ( crs().hasAxisInverted() && ! mShared->mURI.ignoreAxisOrientation() && ! mShared->mCaps.useEPSGColumnFormat ) || mShared->mURI.invertAxisOrientation(); } else // 1.0 diff --git a/tests/src/python/test_qgsserver_wfst.py b/tests/src/python/test_qgsserver_wfst.py index 46988e70af9..98ceb76c28a 100644 --- a/tests/src/python/test_qgsserver_wfst.py +++ b/tests/src/python/test_qgsserver_wfst.py @@ -60,9 +60,12 @@ qgis_app = start_app() class TestWFST(unittest.TestCase): + VERSION = '1.0.0' + @classmethod def setUpClass(cls): """Run before all tests""" + cls.port = QGIS_SERVER_PORT # Create tmp folder cls.temp_path = tempfile.mkdtemp() @@ -92,11 +95,13 @@ class TestWFST(unittest.TestCase): cls.port = int(re.findall(br':(\d+)', line)[0]) assert cls.port != 0 # Wait for the server process to start - assert waitServer('http://127.0.0.1:%s' % cls.port), "Server is not responding!" + assert waitServer('http://127.0.0.1:%s' % + cls.port), "Server is not responding!" @classmethod def tearDownClass(cls): """Run after all tests""" + cls.server.terminate() cls.server.wait() del cls.server @@ -107,10 +112,12 @@ class TestWFST(unittest.TestCase): def setUp(self): """Run before each test.""" + pass def tearDown(self): """Run after each test.""" + pass @classmethod @@ -118,6 +125,7 @@ class TestWFST(unittest.TestCase): """ Delete all features from a vector layer """ + layer = cls._getLayer(layer_name) layer.startEditing() layer.deleteFeatures([f.id() for f in layer.getFeatures()]) @@ -129,6 +137,7 @@ class TestWFST(unittest.TestCase): """ OGR Layer factory """ + path = cls.testdata_path + layer_name + '.shp' layer = QgsVectorLayer(path, layer_name, "ogr") assert layer.isValid() @@ -139,6 +148,7 @@ class TestWFST(unittest.TestCase): """ WFS layer factory """ + if layer_name is None: layer_name = 'wfs_' + type_name parms = { @@ -146,7 +156,7 @@ class TestWFST(unittest.TestCase): 'typename': type_name, 'url': 'http://127.0.0.1:%s/?map=%s' % (cls.port, cls.project_path), - 'version': 'auto', + 'version': cls.VERSION, 'table': '', # 'sql': '', } @@ -160,6 +170,7 @@ class TestWFST(unittest.TestCase): """ Find the feature and return it, raise exception if not found """ + request = QgsFeatureRequest(QgsExpression("%s=%s" % (attr_name, attr_value))) try: @@ -172,21 +183,36 @@ class TestWFST(unittest.TestCase): """ Check features were added """ + wfs_layer.dataProvider().addFeatures(features) layer = self._getLayer(layer.name()) self.assertTrue(layer.isValid()) self.assertEqual(layer.featureCount(), len(features)) - self.assertEqual(wfs_layer.dataProvider().featureCount(), len(features)) + self.assertEqual( + wfs_layer.dataProvider().featureCount(), len(features)) + + ogr_features = [f for f in layer.dataProvider().getFeatures()] + + # Verify features from the layers + for f in ogr_features: + geom = next(wfs_layer.dataProvider().getFeatures(QgsFeatureRequest( + QgsExpression('"id" = %s' % f.attribute('id'))))).geometry() + self.assertEqual(geom.boundingBox(), f.geometry().boundingBox()) def _checkUpdateFeatures(self, wfs_layer, old_features, new_features): """ Check features can be updated """ + for i in range(len(old_features)): - f = self._getFeatureByAttribute(wfs_layer, 'id', old_features[i]['id']) - self.assertTrue(wfs_layer.dataProvider().changeGeometryValues({f.id(): new_features[i].geometry()})) - self.assertTrue(wfs_layer.dataProvider().changeAttributeValues({f.id(): {0: new_features[i]['id']}})) - self.assertTrue(wfs_layer.dataProvider().changeAttributeValues({f.id(): {1: new_features[i]['name']}})) + f = self._getFeatureByAttribute( + wfs_layer, 'id', old_features[i]['id']) + self.assertTrue(wfs_layer.dataProvider().changeGeometryValues( + {f.id(): new_features[i].geometry()})) + self.assertTrue(wfs_layer.dataProvider().changeAttributeValues( + {f.id(): {0: new_features[i]['id']}})) + self.assertTrue(wfs_layer.dataProvider().changeAttributeValues( + {f.id(): {1: new_features[i]['name']}})) def _checkMatchFeatures(self, wfs_layer, features): """ @@ -202,6 +228,7 @@ class TestWFST(unittest.TestCase): """ Delete features """ + ids = [] for f in features: wf = self._getFeatureByAttribute(layer, 'id', f['id']) @@ -212,6 +239,7 @@ class TestWFST(unittest.TestCase): """ Perform all test steps on the layer. """ + self.assertEqual(wfs_layer.featureCount(), 0) self._checkAddFeatures(wfs_layer, layer, old_features) self._checkMatchFeatures(wfs_layer, old_features) @@ -226,6 +254,7 @@ class TestWFST(unittest.TestCase): """ Adds some points, then check and clear all """ + layer_name = 'test_point' layer = self._getLayer(layer_name) wfs_layer = self._getWFSLayer(layer_name) @@ -248,6 +277,7 @@ class TestWFST(unittest.TestCase): Adds some points, then check. Modify 2 points, then checks and clear all """ + layer_name = 'test_point' layer = self._getLayer(layer_name) wfs_layer = self._getWFSLayer(layer_name) @@ -276,15 +306,18 @@ class TestWFST(unittest.TestCase): """ Adds some polygons, then check and clear all """ + layer_name = 'test_polygon' layer = self._getLayer(layer_name) wfs_layer = self._getWFSLayer(layer_name) feat1 = QgsFeature(wfs_layer.fields()) feat1['id'] = 11 feat1['name'] = 'name 11' - feat1.setGeometry(QgsGeometry.fromRect(QgsRectangle(QgsPointXY(9, 45), QgsPointXY(10, 46)))) + feat1.setGeometry(QgsGeometry.fromRect( + QgsRectangle(QgsPointXY(9, 45), QgsPointXY(10, 46)))) feat2 = QgsFeature(wfs_layer.fields()) - feat2.setGeometry(QgsGeometry.fromRect(QgsRectangle(QgsPointXY(9.5, 45.5), QgsPointXY(10.5, 46.5)))) + feat2.setGeometry(QgsGeometry.fromRect(QgsRectangle( + QgsPointXY(9.5, 45.5), QgsPointXY(10.5, 46.5)))) feat2['id'] = 12 feat2['name'] = 'name 12' old_features = [feat1, feat2] @@ -292,7 +325,8 @@ class TestWFST(unittest.TestCase): new_feat1 = QgsFeature(wfs_layer.fields()) new_feat1['id'] = 121 new_feat1['name'] = 'name 121' - new_feat1.setGeometry(QgsGeometry.fromRect(QgsRectangle(QgsPointXY(10, 46), QgsPointXY(11.5, 47.5)))) + new_feat1.setGeometry(QgsGeometry.fromRect( + QgsRectangle(QgsPointXY(10, 46), QgsPointXY(11.5, 47.5)))) new_features = [new_feat1, feat2] self._testLayer(wfs_layer, layer, old_features, new_features) @@ -300,15 +334,18 @@ class TestWFST(unittest.TestCase): """ Adds some lines, then check and clear all """ + layer_name = 'test_linestring' layer = self._getLayer(layer_name) wfs_layer = self._getWFSLayer(layer_name) feat1 = QgsFeature(wfs_layer.fields()) feat1['id'] = 11 feat1['name'] = 'name 11' - feat1.setGeometry(QgsGeometry.fromPolylineXY([QgsPointXY(9, 45), QgsPointXY(10, 46)])) + feat1.setGeometry(QgsGeometry.fromPolylineXY( + [QgsPointXY(9, 45), QgsPointXY(10, 46)])) feat2 = QgsFeature(wfs_layer.fields()) - feat2.setGeometry(QgsGeometry.fromPolylineXY([QgsPointXY(9.5, 45.5), QgsPointXY(10.5, 46.5)])) + feat2.setGeometry(QgsGeometry.fromPolylineXY( + [QgsPointXY(9.5, 45.5), QgsPointXY(10.5, 46.5)])) feat2['id'] = 12 feat2['name'] = 'name 12' old_features = [feat1, feat2] @@ -316,10 +353,17 @@ class TestWFST(unittest.TestCase): new_feat1 = QgsFeature(wfs_layer.fields()) new_feat1['id'] = 121 new_feat1['name'] = 'name 121' - new_feat1.setGeometry(QgsGeometry.fromPolylineXY([QgsPointXY(9.8, 45.8), QgsPointXY(10.8, 46.8)])) + new_feat1.setGeometry(QgsGeometry.fromPolylineXY( + [QgsPointXY(9.8, 45.8), QgsPointXY(10.8, 46.8)])) new_features = [new_feat1, feat2] self._testLayer(wfs_layer, layer, old_features, new_features) +class TestWFST11(TestWFST): + """Same tests for WFS 1.1""" + + VERSION = '1.1.0' + + if __name__ == '__main__': unittest.main()