QGIS/tests/src/python/test_provider_wfs.py
Matthias Kuhn 6d683230de
Disable unstable tests
Parts of the following tests behave unstable because of enironment changes
(distro upgrade, running in docker, ...) or dependency updates.

- qgis_rastersublayertest (Failed)
- PyQgsShapefileProvider (Failed)
- PyQgsWFSProvider (Failed)

They are unlikely to be introduced by the new continuous integration
environment.

Therefore, the affected parts have been disabled. Help by knowledgeable
developers to make the tests more stable or fix real underlying issues.
2017-10-03 17:36:39 +02:00

2570 lines
123 KiB
Python

# -*- coding: utf-8 -*-
"""QGIS Unit tests for the WFS provider.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = 'Even Rouault'
__date__ = '2016-03-25'
__copyright__ = 'Copyright 2016, Even Rouault'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import hashlib
import os
import tempfile
import shutil
# Needed on Qt 5 so that the serialization of XML is consistent among all executions
os.environ['QT_HASH_SEED'] = '1'
from qgis.PyQt.QtCore import QCoreApplication, Qt, QObject, QDateTime
from qgis.core import (
QgsWkbTypes,
QgsVectorLayer,
QgsFeature,
QgsGeometry,
QgsRectangle,
QgsPointXY,
QgsVectorDataProvider,
QgsFeatureRequest,
QgsApplication,
QgsSettings
)
from qgis.testing import (start_app,
unittest
)
from providertestbase import ProviderTestCase
def sanitize(endpoint, x):
if len(endpoint + x) > 256:
ret = endpoint + hashlib.md5(x.encode()).hexdigest()
# print('Before: ' + endpoint + x)
# print('After: ' + ret)
return ret
return endpoint + x.replace('?', '_').replace('&', '_').replace('<', '_').replace('>', '_').replace('"', '_').replace("'", '_').replace(' ', '_').replace(':', '_').replace('/', '_').replace('\n', '_')
class MessageLogger(QObject):
def __init__(self, tag=None):
QObject.__init__(self)
self.log = []
self.tag = tag
def __enter__(self):
QgsApplication.messageLog().messageReceived.connect(self.logMessage)
return self
def __exit__(self, type, value, traceback):
QgsApplication.messageLog().messageReceived.disconnect(self.logMessage)
def logMessage(self, msg, tag, level):
if tag == self.tag or not self.tag:
self.log.append(msg.encode('UTF-8'))
def messages(self):
return self.log
class TestPyQgsWFSProvider(unittest.TestCase, ProviderTestCase):
@classmethod
def setUpClass(cls):
"""Run before all tests"""
QCoreApplication.setOrganizationName("QGIS_Test")
QCoreApplication.setOrganizationDomain("TestPyQgsWFSProvider.com")
QCoreApplication.setApplicationName("TestPyQgsWFSProvider")
QgsSettings().clear()
start_app()
# On Windows we must make sure that any backslash in the path is
# replaced by a forward slash so that QUrl can process it
cls.basetestpath = tempfile.mkdtemp().replace('\\', '/')
endpoint = cls.basetestpath + '/fake_qgis_http_endpoint'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml/3.2"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<!-- add a trailing space to the name to test https://issues.qgis.org/issues/3426 -->
<xsd:element maxOccurs="1" minOccurs="0" name="pk " nillable="true" type="xsd:long"/>
<xsd:element maxOccurs="1" minOccurs="0" name="cnt" nillable="true" type="xsd:long"/>
<xsd:element maxOccurs="1" minOccurs="0" name="name" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="name2" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="num_char" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PolygonPropertyType"/>
<!-- check that an element with ref without name doesn't confuse the DescribeFeatureType analyzer -->
<xsd:element maxOccurs="0" minOccurs="0" ref="my:somethingElseType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
<xsd:complexType name="somethingElseType"/>
</xsd:schema>
""".encode('UTF-8'))
# Create test layer
cls.vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename'", 'test', 'WFS')
assert cls.vl.isValid()
cls.source = cls.vl.dataProvider()
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="5" numberReturned="5" timeStamp="2016-03-25T14:51:48.998Z">
<wfs:member>
<my:typename gml:id="typename.0">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>66.33 -70.332</gml:lowerCorner><gml:upperCorner>66.33 -70.332</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.0"><gml:pos>66.33 -70.332</gml:pos></gml:Point></my:geometryProperty>
<my:pk>1</my:pk>
<my:cnt>100</my:cnt>
<my:name>Orange</my:name>
<my:name2>oranGe</my:name2>
<my:num_char>1</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.1">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>70.8 -68.2</gml:lowerCorner><gml:upperCorner>70.8 -68.2</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.1"><gml:pos>70.8 -68.2</gml:pos></gml:Point></my:geometryProperty>
<my:pk>2</my:pk>
<my:cnt>200</my:cnt>
<my:name>Apple</my:name>
<my:name2>Apple</my:name2>
<my:num_char>2</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.2">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>78.3 -65.32</gml:lowerCorner><gml:upperCorner>78.3 -65.32</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.2"><gml:pos>78.3 -65.32</gml:pos></gml:Point></my:geometryProperty>
<my:pk>4</my:pk>
<my:cnt>400</my:cnt>
<my:name>Honey</my:name>
<my:name2>Honey</my:name2>
<my:num_char>4</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.3">
<my:pk>3</my:pk>
<my:cnt>300</my:cnt>
<my:name>Pear</my:name>
<my:name2>PEaR</my:name2>
<my:num_char>3</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.4">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>78.23 -71.123</gml:lowerCorner><gml:upperCorner>78.23 -71.123</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.4"><gml:pos>78.23 -71.123</gml:pos></gml:Point></my:geometryProperty>
<my:pk>5</my:pk>
<my:cnt>-200</my:cnt>
<my:name2>NuLl</my:name2>
<my:num_char>5</my:num_char>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&RESULTTYPE=hits'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="5" numberReturned="0" timeStamp="2016-03-25T14:51:48.998Z">
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0">
<fes:And>
<fes:PropertyIsGreaterThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>100</fes:Literal>
</fes:PropertyIsGreaterThan>
<fes:PropertyIsLessThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>410</fes:Literal>
</fes:PropertyIsLessThan>
</fes:And>
</fes:Filter>
&RESULTTYPE=hits"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
numberMatched="3" numberReturned="0" timeStamp="2016-03-25T14:51:48.998Z">
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0">
<fes:And>
<fes:PropertyIsGreaterThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>100</fes:Literal>
</fes:PropertyIsGreaterThan>
<fes:PropertyIsLessThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>410</fes:Literal>
</fes:PropertyIsLessThan>
</fes:And>
</fes:Filter>
"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="3" numberReturned="3" timeStamp="2016-03-25T14:51:48.998Z">
<wfs:member>
<my:typename gml:id="typename.1">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>70.8 -68.2</gml:lowerCorner><gml:upperCorner>70.8 -68.2</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.1"><gml:pos>70.8 -68.2</gml:pos></gml:Point></my:geometryProperty>
<my:pk>2</my:pk>
<my:cnt>200</my:cnt>
<my:name>Apple</my:name>
<my:name2>Apple</my:name2>
<my:num_char>2</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.2">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>78.3 -65.32</gml:lowerCorner><gml:upperCorner>78.3 -65.32</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.2"><gml:pos>78.3 -65.32</gml:pos></gml:Point></my:geometryProperty>
<my:pk>4</my:pk>
<my:cnt>400</my:cnt>
<my:name>Honey</my:name>
<my:name2>Honey</my:name2>
<my:num_char>4</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.3">
<my:pk>3</my:pk>
<my:cnt>300</my:cnt>
<my:name>Pear</my:name>
<my:name2>PEaR</my:name2>
<my:num_char>3</my:num_char>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0">
<fes:And>
<fes:PropertyIsGreaterThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>100</fes:Literal>
</fes:PropertyIsGreaterThan>
<fes:PropertyIsLessThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>400</fes:Literal>
</fes:PropertyIsLessThan>
</fes:And>
</fes:Filter>
&RESULTTYPE=hits"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
numberMatched="2" numberReturned="0" timeStamp="2016-03-25T14:51:48.998Z">
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0">
<fes:And>
<fes:PropertyIsGreaterThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>100</fes:Literal>
</fes:PropertyIsGreaterThan>
<fes:PropertyIsLessThan>
<fes:ValueReference>cnt</fes:ValueReference>
<fes:Literal>400</fes:Literal>
</fes:PropertyIsLessThan>
</fes:And>
</fes:Filter>
"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="2" numberReturned="2" timeStamp="2016-03-25T14:51:48.998Z">
<wfs:member>
<my:typename gml:id="typename.1">
<gml:boundedBy><gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326"><gml:lowerCorner>70.8 -68.2</gml:lowerCorner><gml:upperCorner>70.8 -68.2</gml:upperCorner></gml:Envelope></gml:boundedBy>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.1"><gml:pos>70.8 -68.2</gml:pos></gml:Point></my:geometryProperty>
<my:pk>2</my:pk>
<my:cnt>200</my:cnt>
<my:name>Apple</my:name>
<my:name2>Apple</my:name2>
<my:num_char>2</my:num_char>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.3">
<my:pk>3</my:pk>
<my:cnt>300</my:cnt>
<my:name>Pear</my:name>
<my:name2>PEaR</my:name2>
<my:num_char>3</my:num_char>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
@classmethod
def tearDownClass(cls):
"""Run after all tests"""
QgsSettings().clear()
shutil.rmtree(cls.basetestpath, True)
cls.vl = None # so as to properly close the provider and remove any temporary file
def testWkbType(self):
"""N/A for WFS provider"""
pass
def testInconsistentUri(self):
"""Test a URI with a typename that doesn't match a type of the capabilities"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_testInconsistentUri'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<FeatureTypeList>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
# Could not find typename my:typename in capabilities
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename'", 'test', 'WFS')
self.assertFalse(vl.isValid())
def testWFS10(self):
"""Test WFS 1.0 read-only"""
# We also test attribute fields in upper-case, and a field named GEOMETRY
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS1.0'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:32631</SRS>
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="INTFIELD" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="GEOMETRY" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="longfield" nillable="true" type="xsd:long"/>
<xsd:element maxOccurs="1" minOccurs="0" name="stringfield" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="datetimefield" nillable="true" type="xsd:dateTime"/>
<!-- use geometry that is the default SpatiaLite geometry name -->
<xsd:element maxOccurs="1" minOccurs="0" name="geometry" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
self.assertEqual(len(vl.fields()), 5)
self.assertEqual(vl.featureCount(), 0)
reference = QgsGeometry.fromRect(QgsRectangle(400000.0, 5400000.0, 450000.0, 5500000.0))
vl_extent = QgsGeometry.fromRect(vl.extent())
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point>
</my:geometry>
<my:INTFIELD>1</my:INTFIELD>
<my:GEOMETRY>2</my:GEOMETRY>
<my:longfield>1234567890123</my:longfield>
<my:stringfield>foo</my:stringfield>
<my:datetimefield>2016-04-10T12:34:56.789Z</my:datetimefield>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
# Also test that on file iterator works
os.environ['QGIS_WFS_ITERATOR_TRANSFER_THRESHOLD'] = '0'
values = [f['INTFIELD'] for f in vl.getFeatures()]
self.assertEqual(values, [1])
del os.environ['QGIS_WFS_ITERATOR_TRANSFER_THRESHOLD']
values = [f['GEOMETRY'] for f in vl.getFeatures()]
self.assertEqual(values, [2])
values = [f['longfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1234567890123])
values = [f['stringfield'] for f in vl.getFeatures()]
self.assertEqual(values, ['foo'])
values = [f['datetimefield'] for f in vl.getFeatures()]
self.assertEqual(values, [QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))])
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (426858.0, 5427937.0))
self.assertEqual(vl.featureCount(), 1)
self.assertEqual(vl.dataProvider().capabilities(), QgsVectorDataProvider.SelectAtId)
(ret, _) = vl.dataProvider().addFeatures([QgsFeature()])
self.assertFalse(ret)
self.assertFalse(vl.dataProvider().deleteFeatures([0]))
# Test with restrictToRequestBBOX=1
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&BBOX=400000,5400000,450000,5500000'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point>
</my:geometry>
<my:INTFIELD>100</my:INTFIELD>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' restrictToRequestBBOX=1", 'test', 'WFS')
extent = QgsRectangle(400000.0, 5400000.0, 450000.0, 5500000.0)
request = QgsFeatureRequest().setFilterRect(extent)
values = [f['INTFIELD'] for f in vl.getFeatures(request)]
self.assertEqual(values, [100])
def testWFS10_outputformat_GML3(self):
"""Test WFS 1.0 with OUTPUTFORMAT=GML3"""
# We also test attribute fields in upper-case, and a field named GEOMETRY
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS1.0_gml3'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<Capability>
<Request>
<GetFeature>
<ResultFormat>
<GML2/>
<GML3/>
</ResultFormat>
</GetFeature>
</Request>
</Capability>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:32631</SRS>
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometry" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML3'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point>
</my:geometry>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (426858.0, 5427937.0))
# Test with explicit OUTPUTFORMAT as parameter
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' outputformat='GML2'", 'test', 'WFS')
self.assertTrue(vl.isValid())
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML2'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">1,2</gml:coordinates></gml:Point>
</my:geometry>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (1.0, 2.0))
# Test with explicit OUTPUTFORMAT in URL
vl = QgsVectorLayer("url='http://" + endpoint + "?OUTPUTFORMAT=GML2' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631&OUTPUTFORMAT=GML2'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometry>
<gml:Point srsName="urn:ogc:def:crs:EPSG::32631"><gml:coordinates decimal="." cs="," ts=" ">3,4</gml:coordinates></gml:Point>
</my:geometry>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (3.0, 4.0))
def testWFS10_latlongboundingbox_in_WGS84(self):
"""Test WFS 1.0 with non conformatn LatLongBoundingBox"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS1.0_latlongboundingbox_in_WGS84'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:32631</SRS>
<!-- in WFS 1.0, LatLongBoundingBox are supposed to be in SRS units, not necessarily lat/long...
But some servers do not honour this, so let's try to be robust -->
<LatLongBoundingBox minx="1.63972075372399" miny="48.7449841112119" maxx="2.30733562794991" maxy="49.6504711179582"/>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
reference = QgsGeometry.fromRect(QgsRectangle(399999.9999999680439942, 5399338.9090830031782389, 449999.9999999987776391, 5500658.0448500607162714))
vl_extent = QgsGeometry.fromRect(vl.extent())
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
@unittest.skipIf(os.environ.get('TRAVIS', '') == 'true', 'Test unstable')
def testWFST10(self):
"""Test WFS-T 1.0 (read-write)"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_T_1.0'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<Operations>
<Query/>
<Insert/>
<Update/>
<Delete/>
</Operations>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:4326</SRS>
<LatLongBoundingBox minx="-71.123" miny="66.33" maxx="-65.32" maxy="78.3"/>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="intfield" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="longfield" nillable="true" type="xsd:long"/>
<xsd:element maxOccurs="1" minOccurs="0" name="stringfield" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="datetimefield" nillable="true" type="xsd:dateTime"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.dataProvider().capabilities(),
QgsVectorDataProvider.AddFeatures |
QgsVectorDataProvider.ChangeAttributeValues |
QgsVectorDataProvider.ChangeGeometries |
QgsVectorDataProvider.DeleteFeatures |
QgsVectorDataProvider.SelectAtId)
(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('Point (3 50)')}))
self.assertFalse(vl.dataProvider().changeAttributeValues({0: {0: 0}}))
# Test addFeatures
response = """
<wfs:WFS_TransactionResponse version="1.0.0" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<wfs:InsertResult>
<ogc:FeatureId fid="typename.1" />
</wfs:InsertResult>
<wfs:TransactionResult>
<wfs:Status>
<wfs:SUCCESS/>
</wfs:Status>
</wfs:TransactionResult>
</wfs:WFS_TransactionResponse>
"""
# Qt 4 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0.0" service="WFS" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml"><Insert xmlns="http://www.opengis.net/wfs"><typename xmlns="http://my"><intfield xmlns="http://my">1</intfield><longfield xmlns="http://my">1234567890123</longfield><stringfield xmlns="http://my">foo</stringfield><datetimefield xmlns="http://my">2016-04-10T12:34:56.789Z</datetimefield><geometryProperty xmlns="http://my"><gml:Point srsName="EPSG:4326"><gml:coordinates cs="," ts=" ">2,49</gml:coordinates></gml:Point></geometryProperty></typename></Insert></Transaction>'), 'wb') as f:
f.write(response.encode('UTF-8'))
# Qt 5 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" service="WFS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" version="1.0.0"><Insert xmlns="http://www.opengis.net/wfs"><typename xmlns="http://my"><intfield xmlns="http://my">1</intfield><longfield xmlns="http://my">1234567890123</longfield><stringfield xmlns="http://my">foo</stringfield><datetimefield xmlns="http://my">2016-04-10T12:34:56.789Z</datetimefield><geometryProperty xmlns="http://my"><gml:Point srsName="EPSG:4326"><gml:coordinates cs="," ts=" ">2,49</gml:coordinates></gml:Point></geometryProperty></typename></Insert></Transaction>'), 'wb') as f:
f.write(response.encode('UTF-8'))
f = QgsFeature()
f.setAttributes([1, 1234567890123, 'foo', QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))])
f.setGeometry(QgsGeometry.fromWkt('Point (2 49)'))
# def logMessage(msg, tag, level):
# print('--------################----------------')
# print(msg)
# print('--------################----------------')
# QgsApplication.messageLog().messageReceived.connect(logMessage)
(ret, fl) = vl.dataProvider().addFeatures([f])
self.assertTrue(ret)
# QgsApplication.messageLog().messageReceived.disconnect(logMessage)
self.assertEqual(fl[0].id(), 1)
self.assertEqual(vl.featureCount(), 1)
values = [f['intfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1])
values = [f['longfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1234567890123])
values = [f['stringfield'] for f in vl.getFeatures()]
self.assertEqual(values, ['foo'])
values = [f['datetimefield'] for f in vl.getFeatures()]
self.assertEqual(values, [QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))])
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (2.0, 49.0))
# Test changeGeometryValues
content = """
<wfs:WFS_TransactionResponse version="1.0.0" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<wfs:TransactionResult>
<wfs:Status>
<wfs:SUCCESS/>
</wfs:Status>
</wfs:TransactionResult>
</wfs:WFS_TransactionResponse>
"""
# Qt 4 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0.0" service="WFS" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml"><Update xmlns="http://www.opengis.net/wfs" typeName="my:typename"><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">geometryProperty</Name><Value xmlns="http://www.opengis.net/wfs"><gml:Point srsName="EPSG:4326"><gml:coordinates cs="," ts=" ">3,50</gml:coordinates></gml:Point></Value></Property><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="typename.1"/></Filter></Update></Transaction>'), 'wb') as f:
f.write(content.encode('UTF-8'))
# Qt 5 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" service="WFS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" version="1.0.0"><Update xmlns="http://www.opengis.net/wfs" typeName="my:typename"><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">geometryProperty</Name><Value xmlns="http://www.opengis.net/wfs"><gml:Point srsName="EPSG:4326"><gml:coordinates cs="," ts=" ">3,50</gml:coordinates></gml:Point></Value></Property><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="typename.1"/></Filter></Update></Transaction>'), 'wb') as f:
f.write(content.encode('UTF-8'))
self.assertTrue(vl.dataProvider().changeGeometryValues({1: QgsGeometry.fromWkt('Point (3 50)')}))
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (3.0, 50.0))
values = [f['intfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1])
values = [f['longfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1234567890123])
values = [f['stringfield'] for f in vl.getFeatures()]
self.assertEqual(values, ['foo'])
values = [f['datetimefield'] for f in vl.getFeatures()]
self.assertEqual(values, [QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))])
# Test changeAttributeValues
content = """
<wfs:WFS_TransactionResponse version="1.0.0" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<wfs:TransactionResult>
<wfs:Status>
<wfs:SUCCESS/>
</wfs:Status>
</wfs:TransactionResult>
</wfs:WFS_TransactionResponse>
"""
# Qt 4 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0.0" service="WFS" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml"><Update xmlns="http://www.opengis.net/wfs" typeName="my:typename"><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">intfield</Name><Value xmlns="http://www.opengis.net/wfs">2</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">longfield</Name><Value xmlns="http://www.opengis.net/wfs">3</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">stringfield</Name><Value xmlns="http://www.opengis.net/wfs">bar</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">datetimefield</Name><Value xmlns="http://www.opengis.net/wfs">2015-04-10T12:34:56.789Z</Value></Property><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="typename.1"/></Filter></Update></Transaction>'), 'wb') as f:
f.write(content.encode('UTF-8'))
# Qt 5 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" service="WFS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" version="1.0.0"><Update xmlns="http://www.opengis.net/wfs" typeName="my:typename"><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">intfield</Name><Value xmlns="http://www.opengis.net/wfs">2</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">longfield</Name><Value xmlns="http://www.opengis.net/wfs">3</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">stringfield</Name><Value xmlns="http://www.opengis.net/wfs">bar</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">datetimefield</Name><Value xmlns="http://www.opengis.net/wfs">2015-04-10T12:34:56.789Z</Value></Property><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="typename.1"/></Filter></Update></Transaction>'), 'wb') as f:
f.write(content.encode('UTF-8'))
self.assertTrue(vl.dataProvider().changeAttributeValues({1: {0: 2, 1: 3, 2: "bar", 3: QDateTime(2015, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))}}))
values = [f['intfield'] for f in vl.getFeatures()]
self.assertEqual(values, [2])
values = [f['longfield'] for f in vl.getFeatures()]
self.assertEqual(values, [3])
values = [f['stringfield'] for f in vl.getFeatures()]
self.assertEqual(values, ['bar'])
values = [f['datetimefield'] for f in vl.getFeatures()]
self.assertEqual(values, [QDateTime(2015, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))])
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (3.0, 50.0))
# Test deleteFeatures
content = """
<wfs:WFS_TransactionResponse version="1.0.0" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<wfs:TransactionResult>
<wfs:Status>
<wfs:SUCCESS/>
</wfs:Status>
</wfs:TransactionResult>
</wfs:WFS_TransactionResponse>
"""
# Qt 4 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0.0" service="WFS" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml"><Delete xmlns="http://www.opengis.net/wfs" typeName="my:typename"><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="typename.1"/></Filter></Delete></Transaction>'), 'wb') as f:
f.write(content.encode('UTF-8'))
# Qt 5 order
with open(sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" service="WFS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://my http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=my:typename" xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" version="1.0.0"><Delete xmlns="http://www.opengis.net/wfs" typeName="my:typename"><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="typename.1"/></Filter></Delete></Transaction>'), 'wb') as f:
f.write(content.encode('UTF-8'))
self.assertTrue(vl.dataProvider().deleteFeatures([1]))
self.assertEqual(vl.featureCount(), 0)
def testWFS20Paging(self):
"""Test WFS 2.0 paging"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_2.0_paging'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<ows:OperationsMetadata>
<ows:Operation name="GetFeature">
<ows:Constraint name="CountDefault">
<ows:NoValues/>
<ows:DefaultValue>1</ows:DefaultValue>
</ows:Constraint>
</ows:Operation>
<ows:Constraint name="ImplementsResultPaging">
<ows:NoValues/>
<ows:DefaultValue>TRUE</ows:DefaultValue>
</ows:Constraint>
</ows:OperationsMetadata>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml/3.2"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="id" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:GeometryPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=0&COUNT=1&SRSNAME=urn:ogc:def:crs:EPSG::4326'), 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="2" numberReturned="1" timeStamp="2016-03-25T14:51:48.998Z">
<wfs:member>
<my:typename gml:id="typename.100">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.0"><gml:pos>66.33 -70.332</gml:pos></gml:Point></my:geometryProperty>
<my:id>1</my:id>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
# Create test layer
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=1&COUNT=1&SRSNAME=urn:ogc:def:crs:EPSG::4326'), 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="2" numberReturned="1" timeStamp="2016-03-25T14:51:48.998Z">
<wfs:member>
<my:typename gml:id="typename.200">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.0"><gml:pos>66.33 -70.332</gml:pos></gml:Point></my:geometryProperty>
<my:id>2</my:id>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=2&COUNT=1&SRSNAME=urn:ogc:def:crs:EPSG::4326'), 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="2" numberReturned="0" timeStamp="2016-03-25T14:51:48.998Z">
</wfs:FeatureCollection>""".encode('UTF-8'))
values = [f['id'] for f in vl.getFeatures()]
self.assertEqual(values, [1, 2])
# Suppress GetFeature responses to demonstrate that the cache is used
os.unlink(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=0&COUNT=1&SRSNAME=urn:ogc:def:crs:EPSG::4326'))
os.unlink(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=1&COUNT=1&SRSNAME=urn:ogc:def:crs:EPSG::4326'))
os.unlink(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=2&COUNT=1&SRSNAME=urn:ogc:def:crs:EPSG::4326'))
values = [f['id'] for f in vl.getFeatures()]
self.assertEqual(values, [1, 2])
# No need for hits since the download went to its end
self.assertEqual(vl.featureCount(), 2)
vl.dataProvider().reloadData()
# Hits not working
self.assertEqual(vl.featureCount(), 0)
vl.dataProvider().reloadData()
# Hits working
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&RESULTTYPE=hits'), 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="2" numberReturned="0" timeStamp="2016-03-25T14:51:48.998Z">
</wfs:FeatureCollection>""".encode('UTF-8'))
self.assertEqual(vl.featureCount(), 2)
def testWFSGetOnlyFeaturesInViewExtent(self):
"""Test 'get only features in view extent' """
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_only_features_in_view_extent'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="1.1.0" xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:ows="http://www.opengis.net/ows" xmlns:gml="http://schemas.opengis.net/gml">
<ows:OperationsMetadata>
<ows:Operation name="GetFeature">
<ows:Parameter name="resultType">
<ows:Value>results</ows:Value>
<ows:Value>hits</ows:Value>
</ows:Parameter>
</ows:Operation>
<ows:Constraint name="DefaultMaxFeatures">
<ows:Value>2</ows:Value>
</ows:Constraint>
</ows:OperationsMetadata>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-80 60</ows:LowerCorner>
<ows:UpperCorner>-50 80</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<!-- use ogc_fid that is the default SpatiaLite FID name -->
<xsd:element maxOccurs="1" minOccurs="0" name="ogc_fid" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
# Create test layer
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' restrictToRequestBBOX=1", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
last_url = sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&MAXFEATURES=2&SRSNAME=urn:ogc:def:crs:EPSG::4326&BBOX=60,-70,80,-60,urn:ogc:def:crs:EPSG::4326')
with open(last_url, 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my"
numberOfFeatures="1" timeStamp="2016-03-25T14:51:48.998Z">
<gml:featureMembers>
<my:typename gml:id="typename.200">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>70 -65</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>2</my:ogc_fid>
</my:typename>
</gml:featureMembers>
</wfs:FeatureCollection>""".encode('UTF-8'))
extent = QgsRectangle(-70, 60, -60, 80)
request = QgsFeatureRequest().setFilterRect(extent)
values = [f['ogc_fid'] for f in vl.getFeatures(request)]
self.assertEqual(values, [2])
# To show that if we zoom-in, we won't issue a new request
with open(last_url, 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my"
numberOfFeatures="1" timeStamp="2016-03-25T14:51:48.998Z">
<gml:featureMembers>
<my:typename gml:id="typename.20000">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>70 -65</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>200</my:ogc_fid>
</my:typename>
</gml:featureMembers>
</wfs:FeatureCollection>""".encode('UTF-8'))
extent = QgsRectangle(-66, 62, -62, 78)
request = QgsFeatureRequest().setFilterRect(extent)
values = [f['ogc_fid'] for f in vl.getFeatures(request)]
self.assertEqual(values, [2])
# Move to a neighbouring area, and reach the download limit
last_url = sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&MAXFEATURES=2&SRSNAME=urn:ogc:def:crs:EPSG::4326&BBOX=65,-70,90,-60,urn:ogc:def:crs:EPSG::4326')
with open(last_url, 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my"
numberOfFeatures="1" timeStamp="2016-03-25T14:51:48.998Z">
<gml:featureMembers>
<my:typename gml:id="typename.200">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>70 -65</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>2</my:ogc_fid>
</my:typename>
<my:typename gml:id="typename.300">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>85 -65</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>3</my:ogc_fid>
</my:typename>
</gml:featureMembers>
</wfs:FeatureCollection>""".encode('UTF-8'))
extent = QgsRectangle(-70, 65, -60, 90)
request = QgsFeatureRequest().setFilterRect(extent)
values = [f['ogc_fid'] for f in vl.getFeatures(request)]
self.assertEqual(values, [2, 3])
# Zoom-in again, and bring more features
last_url = sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&MAXFEATURES=2&SRSNAME=urn:ogc:def:crs:EPSG::4326&BBOX=66,-69,89,-61,urn:ogc:def:crs:EPSG::4326')
with open(last_url, 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my"
numberOfFeatures="1" timeStamp="2016-03-25T14:51:48.998Z">
<gml:featureMembers>
<my:typename gml:id="typename.200">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>70 -65</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>2</my:ogc_fid>
</my:typename>
<my:typename gml:id="typename.400">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>84 -64</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>4</my:ogc_fid>
</my:typename>
</gml:featureMembers>
</wfs:FeatureCollection>""".encode('UTF-8'))
extent = QgsRectangle(-69, 66, -61, 89)
request = QgsFeatureRequest().setFilterRect(extent)
values = [f['ogc_fid'] for f in vl.getFeatures(request)]
self.assertEqual(values, [2, 3, 4])
# Test RESULTTYPE=hits
last_url = sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&RESULTTYPE=hits')
with open(last_url, 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs"
numberOfFeatures="10" timeStamp="2016-03-25T14:51:48.998Z"/>""".encode('UTF-8'))
self.assertEqual(vl.featureCount(), 10)
# Combine BBOX and FILTER
last_url = sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&MAXFEATURES=2&SRSNAME=urn:ogc:def:crs:EPSG::4326&FILTER=<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml">
<ogc:And>
<ogc:BBOX>
<ogc:PropertyName>geometryProperty</ogc:PropertyName>
<gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326">
<gml:lowerCorner>66 -69</gml:lowerCorner>
<gml:upperCorner>89 -61</gml:upperCorner>
</gml:Envelope>
</ogc:BBOX>
<ogc:PropertyIsEqualTo xmlns:ogc="http://www.opengis.net/ogc">
<ogc:PropertyName xmlns:ogc="http://www.opengis.net/ogc">ogc_fid</ogc:PropertyName>
<ogc:Literal xmlns:ogc="http://www.opengis.net/ogc">101</ogc:Literal>
</ogc:PropertyIsEqualTo>
</ogc:And>
</ogc:Filter>
""")
with open(last_url, 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my"
numberOfFeatures="1" timeStamp="2016-03-25T14:51:48.998Z">
<gml:featureMembers>
<my:typename gml:id="typename.101">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326"><gml:pos>70 -65</gml:pos></gml:Point></my:geometryProperty>
<my:ogc_fid>101</my:ogc_fid>
</my:typename>
</gml:featureMembers>
</wfs:FeatureCollection>""".encode('UTF-8'))
vl.dataProvider().setSubsetString('ogc_fid = 101')
extent = QgsRectangle(-69, 66, -61, 89)
request = QgsFeatureRequest().setFilterRect(extent)
values = [f['ogc_fid'] for f in vl.getFeatures(request)]
self.assertEqual(values, [101])
def testWFS20TruncatedResponse(self):
"""Test WFS 2.0 truncatedResponse"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_2.0_truncated_response'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml/3.2"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326'), 'wb') as f:
f.write("""
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my"
numberMatched="1" numberReturned="1" timeStamp="2016-03-25T14:51:48.998Z">
<wfs:member>
<my:typename gml:id="typename.1"/>
</wfs:member>
<wfs:truncatedResponse/>
</wfs:FeatureCollection>""".encode('UTF-8'))
# Create test layer
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename'", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Check that we get a log message
with MessageLogger('WFS') as logger:
[f for f in vl.getFeatures()]
self.assertEqual(len(logger.messages()), 1, logger.messages())
self.assertTrue(logger.messages()[0].decode('UTF-8').find('The download limit has been reached') >= 0, logger.messages())
def testRetryLogic(self):
"""Test retry logic """
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_retry'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:4326</SRS>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="INTFIELD" nillable="true" type="xsd:int"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.NoGeometry)
self.assertEqual(len(vl.fields()), 1)
# Failed download: test that error is propagated to the data provider, so as to get application notification
[f['INTFIELD'] for f in vl.getFeatures()]
errors = vl.dataProvider().errors()
self.assertEqual(len(errors), 1, errors)
# Reload
vl.reload()
# First retry: Empty response
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:4326&RETRY=1'), 'wb') as f:
f.write(''.encode('UTF-8'))
# Second retry: Incomplete response
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:4326&RETRY=2'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:featureMember>
<my:typename fid="typename.0">
<my:INTFIELD>1</my:INTFIELD>
</my:typename>
</gml:featureMember>
<gml:featureMember>
<my:typename fid="typename.1">
<my:INTFIELD>2</my:INTFIELD>""".encode('UTF-8'))
# Third retry: Valid response
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:4326&RETRY=3'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:featureMember>
<my:typename fid="typename.0">
<my:INTFIELD>1</my:INTFIELD>
</my:typename>
</gml:featureMember>
<gml:featureMember>
<my:typename fid="typename.1">
<my:INTFIELD>2</my:INTFIELD>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
values = [f['INTFIELD'] for f in vl.getFeatures()]
self.assertEqual(values, [1, 2])
def testDetachedFeatureSource(self):
"""Test using a feature source after the provider has been destroyed """
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_detached_source'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:4326</SRS>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="INTFIELD" nillable="true" type="xsd:int"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.NoGeometry)
self.assertEqual(len(vl.fields()), 1)
source = vl.dataProvider().featureSource()
# Close the vector layer
vl = None
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:4326'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:featureMember>
<my:typename fid="typename.0">
<my:INTFIELD>1</my:INTFIELD>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
values = [f['INTFIELD'] for f in source.getFeatures(QgsFeatureRequest())]
self.assertEqual(values, [1])
def testJoins(self):
"""Test SELECT with joins """
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_detached_source'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=2.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<ows:OperationsMetadata>
<ows:Constraint name="ImplementsStandardJoins">
<ows:NoValues/>
<ows:DefaultValue>TRUE</ows:DefaultValue>
</ows:Constraint>
<ows:Constraint name="ImplementsSpatialJoins">
<ows:NoValues/>
<ows:DefaultValue>TRUE</ows:DefaultValue>
</ows:Constraint>
</ows:OperationsMetadata>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
<FeatureType>
<Name>my:othertypename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
<FeatureType>
<Name>first_ns:ambiguous</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
<FeatureType>
<Name>second_ns:ambiguous</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
schema = """
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="id" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
<xsd:complexType name="othertypenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="main_id" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="second_id" nillable="true" type="xsd:int"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="othertypename" substitutionGroup="gml:_Feature" type="my:othertypenameType"/>
</xsd:schema>
"""
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename,my:othertypename'), 'wb') as f:
f.write(schema.encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename,my:othertypename&SRSNAME=urn:ogc:def:crs:EPSG::4326&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0">
<fes:And>
<fes:PropertyIsEqualTo>
<fes:ValueReference>my:typename/id</fes:ValueReference>
<fes:ValueReference>my:othertypename/main_id</fes:ValueReference>
</fes:PropertyIsEqualTo>
<fes:PropertyIsGreaterThan>
<fes:ValueReference>my:typename/id</fes:ValueReference>
<fes:Literal>0</fes:Literal>
</fes:PropertyIsGreaterThan>
</fes:And>
</fes:Filter>
&SORTBY=id DESC"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<wfs:member>
<wfs:Tuple>
<wfs:member>
<my:typename fid="typename.0">
<my:id>1</my:id>
</my:typename>
</wfs:member>
<wfs:member>
<my:othertypename fid="othertypename.0">
<my:main_id>1</my:main_id>
<my:second_id>2</my:second_id>
</my:othertypename>
</wfs:member>
</wfs:Tuple>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
# * syntax
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM \"my:typename\" JOIN \"my:othertypename\" o ON \"my:typename\".id = o.main_id WHERE \"my:typename\".id > 0 ORDER BY \"my:typename\".id DESC", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
fields = vl.fields()
self.assertEqual(len(fields), 3, fields)
self.assertEqual(fields[0].name(), 'typename.id')
self.assertEqual(fields[1].name(), 'o.main_id')
self.assertEqual(fields[2].name(), 'o.second_id')
values = [(f['typename.id'], f['o.main_id'], f['o.second_id']) for f in vl.getFeatures()]
self.assertEqual(values, [(1, 1, 2)])
# * syntax with unprefixed typenames
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM typename JOIN othertypename o ON typename.id = o.main_id WHERE typename.id > 0 ORDER BY typename.id DESC", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
fields = vl.fields()
self.assertEqual(len(fields), 3, fields)
self.assertEqual(fields[0].name(), 'typename.id')
self.assertEqual(fields[1].name(), 'o.main_id')
self.assertEqual(fields[2].name(), 'o.second_id')
values = [(f['typename.id'], f['o.main_id'], f['o.second_id']) for f in vl.getFeatures()]
self.assertEqual(values, [(1, 1, 2)])
# main table not appearing in first
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:othertypename,my:typename'), 'wb') as f:
f.write(schema.encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM othertypename o, typename WHERE typename.id = o.main_id AND typename.id > 0 ORDER BY typename.id DESC", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
fields = vl.fields()
self.assertEqual(len(fields), 3, fields)
self.assertEqual(fields[0].name(), 'o.main_id')
self.assertEqual(fields[1].name(), 'o.second_id')
self.assertEqual(fields[2].name(), 'typename.id')
# main table not appearing in first, not in FROM but in JOIN
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM othertypename o JOIN typename ON typename.id = o.main_id WHERE typename.id > 0 ORDER BY typename.id DESC", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
fields = vl.fields()
self.assertEqual(len(fields), 3, fields)
self.assertEqual(fields[0].name(), 'o.main_id')
self.assertEqual(fields[1].name(), 'o.second_id')
self.assertEqual(fields[2].name(), 'typename.id')
# table_alias.*, field alias
vl.setSubsetString("SELECT o.*, m.id AS m_id FROM \"my:typename\" m JOIN \"my:othertypename\" o ON m.id = o.main_id WHERE m.id > 0 ORDER BY m.id DESC")
fields = vl.fields()
self.assertEqual(len(fields), 3, fields)
self.assertEqual(fields[0].name(), 'o.main_id')
self.assertEqual(fields[1].name(), 'o.second_id')
self.assertEqual(fields[2].name(), 'm_id')
values = [(f['o.main_id'], f['o.second_id'], f['m_id']) for f in vl.getFeatures()]
self.assertEqual(values, [(1, 2, 1)])
# table_alias.*, field alias, with unprefixed typenames
vl.setSubsetString("SELECT o.*, m.id AS m_id FROM typename m JOIN othertypename o ON m.id = o.main_id WHERE m.id > 0 ORDER BY m.id DESC")
fields = vl.fields()
self.assertEqual(len(fields), 3, fields)
self.assertEqual(fields[0].name(), 'o.main_id')
self.assertEqual(fields[1].name(), 'o.second_id')
self.assertEqual(fields[2].name(), 'm_id')
values = [(f['o.main_id'], f['o.second_id'], f['m_id']) for f in vl.getFeatures()]
self.assertEqual(values, [(1, 2, 1)])
# Test going back to single layer
vl.setSubsetString(None)
fields = vl.fields()
self.assertEqual(len(fields), 1, fields)
self.assertEqual(fields[0].name(), 'id')
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write(schema.encode('UTF-8'))
# Duplicate fields
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT id, id FROM \"my:typename\"", 'test', 'WFS')
self.assertFalse(vl.isValid())
# * syntax with single layer
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM \"my:typename\"", 'test', 'WFS')
self.assertTrue(vl.isValid())
fields = vl.fields()
self.assertEqual(len(fields), 1, fields)
self.assertEqual(fields[0].name(), 'id')
# * syntax with single layer, unprefixed
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM typename", 'test', 'WFS')
self.assertTrue(vl.isValid())
fields = vl.fields()
self.assertEqual(len(fields), 1, fields)
self.assertEqual(fields[0].name(), 'id')
# test with unqualified field name, and geometry name specified
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT id, geometryProperty FROM typename", 'test', 'WFS')
self.assertTrue(vl.isValid())
fields = vl.fields()
self.assertEqual(len(fields), 1, fields)
self.assertEqual(fields[0].name(), 'id')
# Ambiguous typename
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='first_ns:ambiguous' version='2.0.0' sql=SELECT id FROM ambiguous", 'test', 'WFS')
self.assertFalse(vl.isValid())
# main table missing from SQL
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:othertypename' version='2.0.0' sql=SELECT * FROM typename", 'test', 'WFS')
self.assertFalse(vl.isValid())
def testFunctionValidation(self):
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_function_validation'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:4326</SRS>
<LatLongBoundingBox minx="-71.123" miny="66.33" maxx="-65.32" maxy="78.3"/>
</FeatureType>
</FeatureTypeList>
<ogc:Filter_Capabilities>
<ogc:Spatial_Capabilities>
<ogc:Spatial_Operators>
<ogc:Disjoint/>
<ogc:Equals/>
<ogc:DWithin/>
<ogc:Beyond/>
<ogc:Intersect/>
<ogc:Touches/>
<ogc:Crosses/>
<ogc:Within/>
<ogc:Contains/>
<ogc:Overlaps/>
<ogc:BBOX/>
</ogc:Spatial_Operators>
</ogc:Spatial_Capabilities>
<ogc:Scalar_Capabilities>
<ogc:Arithmetic_Operators>
<ogc:Functions>
<ogc:Function_Names>
<ogc:Function_Name nArgs="1">abs</ogc:Function_Name>
</ogc:Function_Names>
</ogc:Functions>
</ogc:Arithmetic_Operators>
</ogc:Scalar_Capabilities>
</ogc:Filter_Capabilities>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="1.1.0" xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:ows="http://www.opengis.net/ows" xmlns:gml="http://schemas.opengis.net/gml">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-80 60</ows:LowerCorner>
<ows:UpperCorner>-50 80</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
<ogc:Filter_Capabilities>
<ogc:Spatial_Capabilities>
<ogc:GeometryOperands>
<ogc:GeometryOperand name="gml:Envelope"/>
<ogc:GeometryOperand name="gml:Point"/>
<ogc:GeometryOperand name="gml:LineString"/>
<ogc:GeometryOperand name="gml:Polygon"/>
</ogc:GeometryOperands>
<ogc:SpatialOperators>
<ogc:SpatialOperator name="Disjoint"/>
<ogc:SpatialOperator name="Equals"/>
<ogc:SpatialOperator name="DWithin"/>
<ogc:SpatialOperator name="Beyond"/>
<ogc:SpatialOperator name="Intersects"/>
<ogc:SpatialOperator name="Touches"/>
<ogc:SpatialOperator name="Crosses"/>
<ogc:SpatialOperator name="Within"/>
<ogc:SpatialOperator name="Contains"/>
<ogc:SpatialOperator name="Overlaps"/>
<ogc:SpatialOperator name="BBOX"/>
</ogc:SpatialOperators>
</ogc:Spatial_Capabilities>
<ogc:Scalar_Capabilities>
<ogc:ArithmeticOperators>
<ogc:Functions>
<ogc:FunctionNames>
<ogc:FunctionName nArgs="1">abs</ogc:FunctionName>
<ogc:FunctionName nArgs="-2">atleasttwoargs</ogc:FunctionName> <!-- GeoTools way -->
</ogc:FunctionNames>
</ogc:Functions>
</ogc:ArithmeticOperators>
</ogc:Scalar_Capabilities>
</ogc:Filter_Capabilities>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=2.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
<fes:Filter_Capabilities>
<fes:Spatial_Capabilities>
<fes:GeometryOperands>
<fes:GeometryOperand name="gml:Envelope"/>
<fes:GeometryOperand name="gml:Point"/>
<fes:GeometryOperand name="gml:MultiPoint"/>
<fes:GeometryOperand name="gml:LineString"/>
<fes:GeometryOperand name="gml:MultiLineString"/>
<fes:GeometryOperand name="gml:Polygon"/>
<fes:GeometryOperand name="gml:MultiPolygon"/>
<fes:GeometryOperand name="gml:MultiGeometry"/>
</fes:GeometryOperands>
<fes:SpatialOperators>
<fes:SpatialOperator name="Disjoint"/>
<fes:SpatialOperator name="Equals"/>
<fes:SpatialOperator name="DWithin"/>
<fes:SpatialOperator name="Beyond"/>
<fes:SpatialOperator name="Intersects"/>
<fes:SpatialOperator name="Touches"/>
<fes:SpatialOperator name="Crosses"/>
<fes:SpatialOperator name="Within"/>
<fes:SpatialOperator name="Contains"/>
<fes:SpatialOperator name="Overlaps"/>
<fes:SpatialOperator name="BBOX"/>
</fes:SpatialOperators>
</fes:Spatial_Capabilities>
<fes:Functions>
<fes:Function name="abs">
<fes:Returns>xs:int</fes:Returns>
<fes:Arguments>
<fes:Argument name="int">
<fes:Type>xs:int</fes:Type>
</fes:Argument>
</fes:Arguments>
</fes:Function>
</fes:Functions>
</fes:Filter_Capabilities>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
schema = """
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="id" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geom" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>"""
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write(schema.encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=my:typename'), 'wb') as f:
f.write(schema.encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write(schema.encode('UTF-8'))
# Existing function and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE abs(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Existing spatial predicated and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE ST_Intersects(geom, geom)", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Non existing function and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE non_existing(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertFalse(vl.isValid())
# Non existing function, but validation disabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0' sql=SELECT * FROM \"my:typename\" WHERE non_existing(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Existing function and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.1.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE abs(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Existing spatial predicated and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.1.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE ST_Intersects(geom, geom)", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Non existing function and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.1.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE non_existing(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertFalse(vl.isValid())
# Existing function and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE abs(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Existing spatial predicated and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE ST_Intersects(geom, geom)", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Non existing function and validation enabled
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' validateSQLFunctions=1 sql=SELECT * FROM \"my:typename\" WHERE non_existing(\"my:typename\".id) > 1", 'test', 'WFS')
self.assertFalse(vl.isValid())
def testSelectDistinct(self):
"""Test SELECT DISTINCT """
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_select_distinct'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=2.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="intfield" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="longfield" nillable="true" type="xsd:long"/>
<xsd:element maxOccurs="1" minOccurs="0" name="stringfield" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="datetimefield" nillable="true" type="xsd:dateTime"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<wfs:member>
<my:typename gml:id="typename.0">
<my:intfield>1</my:intfield>
<my:longfield>1234567890</my:longfield>
<my:stringfield>foo</my:stringfield>
<my:datetimefield>2016-04-10T12:34:56.789Z</my:datetimefield>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.1"> <!-- duplicate -->
<my:intfield>1</my:intfield>
<my:longfield>1234567890</my:longfield>
<my:stringfield>foo</my:stringfield>
<my:datetimefield>2016-04-10T12:34:56.789Z</my:datetimefield>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.2">
<my:intfield>2</my:intfield> <!-- difference -->
<my:longfield>1234567890</my:longfield>
<my:stringfield>foo</my:stringfield>
<my:datetimefield>2016-04-10T12:34:56.789Z</my:datetimefield>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.3">
<my:intfield>1</my:intfield>
<my:longfield>1234567891</my:longfield> <!-- difference -->
<my:stringfield>foo</my:stringfield>
<my:datetimefield>2016-04-10T12:34:56.789Z</my:datetimefield>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.4">
<my:intfield>1</my:intfield>
<my:longfield>1234567890</my:longfield>
<my:stringfield>fop</my:stringfield> <!-- difference -->
<my:datetimefield>2016-04-10T12:34:56.789Z</my:datetimefield>
</my:typename>
</wfs:member>
<wfs:member>
<my:typename gml:id="typename.5">
<my:intfield>1</my:intfield>
<my:longfield>1234567890</my:longfield>
<my:stringfield>foo</my:stringfield>
<my:datetimefield>2016-04-10T12:34:56.788Z</my:datetimefield> <!-- difference -->
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT DISTINCT * FROM \"my:typename\"", 'test', 'WFS')
self.assertTrue(vl.isValid())
values = [(f['intfield'], f['longfield'], f['stringfield'], f['datetimefield']) for f in vl.getFeatures()]
self.assertEqual(values, [(1, 1234567890, 'foo', QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))),
(2, 1234567890, 'foo', QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))),
(1, 1234567891, 'foo', QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))),
(1, 1234567890, 'fop', QDateTime(2016, 4, 10, 12, 34, 56, 789, Qt.TimeSpec(Qt.UTC))),
(1, 1234567890, 'foo', QDateTime(2016, 4, 10, 12, 34, 56, 788, Qt.TimeSpec(Qt.UTC)))])
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT DISTINCT intfield FROM \"my:typename\"", 'test', 'WFS')
self.assertTrue(vl.isValid())
values = [(f['intfield']) for f in vl.getFeatures()]
self.assertEqual(values, [(1), (2)])
def testWrongCapabilityExtent(self):
"""Test behavior when capability extent is wrong."""
# Note the logic that is tested is purely heuristic, trying to recover from wrong server behavior,
# so it might be legitimate to change that at a later point.
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_wrong_capability_extent'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=2.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>0 0</ows:LowerCorner>
<ows:UpperCorner>1 1</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="intfield" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PolygonPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<wfs:member>
<my:typename gml:id="typename.0">
<my:intfield>1</my:intfield>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.0"><gml:pos>49 2</gml:pos></gml:Point></my:geometryProperty>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
# Download all features
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 1)
reference = QgsGeometry.fromRect(QgsRectangle(2, 49, 2, 49))
vl_extent = QgsGeometry.fromRect(vl.extent())
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
# Same with restrictToRequestBBOX=1
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' restrictToRequestBBOX=1", 'test', 'WFS')
self.assertTrue(vl.isValid())
# First request that will be attempted
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326&BBOX=-0.125,-0.125,1.125,1.125,urn:ogc:def:crs:EPSG::4326"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
</wfs:FeatureCollection>""".encode('UTF-8'))
# And fallback
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&COUNT=1"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<wfs:member>
<my:typename gml:id="typename.0">
<my:intfield>1</my:intfield>
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::4326" gml:id="typename.geom.0"><gml:pos>49 2</gml:pos></gml:Point></my:geometryProperty>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
# Download all features in a BBOX that encloses the extent reported by capabilities
extent = QgsRectangle(-0.125, -0.125, 1.125, 1.125)
request = QgsFeatureRequest().setFilterRect(extent)
features = [f for f in vl.getFeatures(request)]
self.assertEqual(len(features), 0)
# Check that the approx extent contains the geometry
self.assertTrue(vl.extent().contains(QgsPointXY(2, 49)))
def testGeomedia(self):
"""Test various interoperability specifities that occur with Geomedia Web Server."""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_geomedia'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=2.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<ows:OperationsMetadata>
<ows:Constraint name="ImplementsResultPaging">
<ows:NoValues/>
<ows:DefaultValue>TRUE</ows:DefaultValue>
</ows:Constraint>
</ows:OperationsMetadata>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>EPSG:32631</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>0 40</ows:LowerCorner>
<ows:UpperCorner>15 50</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="intfield" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gmgml:Polygon_Surface_MultiSurface_CompositeSurfacePropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=0&COUNT=1&SRSNAME=EPSG:32631"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<wfs:member>
<my:typename>
<my:intfield>1</my:intfield>
<my:geometryProperty><gml:Polygon srsName="EPSG:32631" gml:id="typename.geom.0"><gml:exterior><gml:LinearRing><gml:posList>500000 4500000 500000 4510000 510000 4510000 510000 4500000 500000 4500000</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></my:geometryProperty>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
# Simulate improper paging support by returning same result set whatever the STARTINDEX is
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&STARTINDEX=1&COUNT=1&SRSNAME=EPSG:32631"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<wfs:member>
<my:typename>
<my:intfield>1</my:intfield>
<my:geometryProperty><gml:Polygon srsName="EPSG:32631" gml:id="typename.geom.0"><gml:exterior><gml:LinearRing><gml:posList>500000 4500000 500000 4510000 510000 4510000 510000 4500000 500000 4500000</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></my:geometryProperty>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=EPSG:32631"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<wfs:member>
<my:typename>
<my:intfield>1</my:intfield>
<my:geometryProperty><gml:Polygon srsName="EPSG:32631" gml:id="typename.geom.0"><gml:exterior><gml:LinearRing><gml:posList>500000 4500000 500000 4510000 510000 4510000 510000 4500000 500000 4500000</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></my:geometryProperty>
</my:typename>
<my:typename>
<my:intfield>2</my:intfield>
<my:geometryProperty><gml:Polygon srsName="EPSG:32631" gml:id="typename.geom.0"><gml:exterior><gml:LinearRing><gml:posList>500000 4500000 500000 4510000 510000 4510000 510000 4500000 500000 4500000</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></my:geometryProperty>
</my:typename>
</wfs:member>
</wfs:FeatureCollection>""".encode('UTF-8'))
QgsSettings().setValue('wfs/max_feature_count_if_not_provided', '1')
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.MultiPolygon)
# Extent before downloading features
reference = QgsGeometry.fromRect(QgsRectangle(243900.3520259926444851, 4427769.1559739429503679, 1525592.3040170343592763, 5607994.6020106188952923))
vl_extent = QgsGeometry.fromRect(vl.extent())
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
# Download all features
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 2)
reference = QgsGeometry.fromRect(QgsRectangle(500000, 4500000, 510000, 4510000))
vl_extent = QgsGeometry.fromRect(vl.extent())
assert QgsGeometry.compare(vl_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001), 'Expected {}, got {}'.format(reference.exportToWkt(), vl_extent.exportToWkt())
self.assertEqual(features[0]['intfield'], 1)
self.assertEqual(features[1]['intfield'], 2)
def testMapServerWFS1_1_EPSG_4326(self):
"""Test interoperability with MapServer WFS 1.1."""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_mapserver_wfs_1_1'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="1.1.0" xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:ows="http://www.opengis.net/ows" xmlns:gml="http://schemas.opengis.net/gml">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>2 49</ows:LowerCorner>
<ows:UpperCorner>2 49</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<schema
targetNamespace="http://my"
xmlns:my="http://my"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:gml="http://www.opengis.net/gml"
elementFormDefault="qualified" version="0.1" >
<import namespace="http://www.opengis.net/gml"
schemaLocation="http://schemas.opengis.net/gml/3.1.1/base/gml.xsd" />
<element name="typename"
type="my:typenameType"
substitutionGroup="gml:_Feature" />
<complexType name="typenameType">
<complexContent>
<extension base="gml:AbstractFeatureType">
<sequence>
<element name="geometryProperty" type="gml:GeometryPropertyType" minOccurs="0" maxOccurs="1"/>
</sequence>
</extension>
</complexContent>
</complexType>
</schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:my="http://my"
xmlns:gml="http://www.opengis.net/gml"
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:ogc="http://www.opengis.net/ogc">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<gml:featureMember>
<my:typename gml:id="typename.1">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<my:geometryProperty>
<gml:Point srsName="EPSG:4326">
<gml:pos>49.000000 2.000000</gml:pos>
</gml:Point>
</my:geometryProperty>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.1.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (2.0, 49.0))
def testDescribeFeatureTypeWithInlineType(self):
"""Test a DescribeFeatureType response with a inline ComplexType (#15395)."""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_testDescribeFeatureTypeWithInlineType'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="1.1.0" xmlns="http://www.opengis.net/wfs" xmlns:wfs="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc" xmlns:ows="http://www.opengis.net/ows" xmlns:gml="http://schemas.opengis.net/gml">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>2 49</ows:LowerCorner>
<ows:UpperCorner>2 49</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<schema
targetNamespace="http://my"
xmlns:my="http://my"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:gml="http://www.opengis.net/gml"
elementFormDefault="qualified" version="0.1" >
<import namespace="http://www.opengis.net/gml"
schemaLocation="http://schemas.opengis.net/gml/3.1.1/base/gml.xsd" />
<element name="typename"
substitutionGroup="gml:_Feature">
<complexType>
<complexContent>
<extension base="gml:AbstractFeatureType">
<sequence>
<element name="geometryProperty" type="gml:GeometryPropertyType" minOccurs="0" maxOccurs="1"/>
</sequence>
</extension>
</complexContent>
</complexType>
</element>
</schema>
""".encode('UTF-8'))
with open(sanitize(endpoint, """?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::4326"""), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:my="http://my"
xmlns:gml="http://www.opengis.net/gml"
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:ogc="http://www.opengis.net/ogc">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<gml:featureMember>
<my:typename gml:id="typename.1">
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>49.000000 2.000000</gml:lowerCorner>
<gml:upperCorner>49.000000 2.000000</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<my:geometryProperty>
<gml:Point srsName="EPSG:4326">
<gml:pos>49.000000 2.000000</gml:pos>
</gml:Point>
</my:geometryProperty>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.1.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (2.0, 49.0))
def testWFS20TransactionsDisabled(self):
"""Test WFS 2.0 Transaction disabled"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_2.0_transaction'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<ows:OperationsMetadata>
<ows:Operation name="GetFeature">
<ows:Constraint name="CountDefault">
<ows:NoValues/>
<ows:DefaultValue>1</ows:DefaultValue>
</ows:Constraint>
</ows:Operation>
<ows:Constraint name="ImplementsTransactionalWFS">
<ows:NoValues/>
<ows:DefaultValue>TRUE</ows:DefaultValue>
</ows:Constraint>
</ows:OperationsMetadata>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml/3.2"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
# Create test layer
vl = QgsVectorLayer(u"url='http://" + endpoint + u"' typename='my:typename'", u'test', u'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.dataProvider().capabilities() & vl.dataProvider().EditingCapabilities, vl.dataProvider().NoCapabilities)
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
def testWFS20TransactionsEnabled(self):
"""Test WFS 2.0 Transaction enabled"""
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_WFS_2.0_transaction'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?ACCEPTVERSIONS=2.0.0,1.1.0,1.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns="http://www.opengis.net/wfs/2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://schemas.opengis.net/gml/3.2" xmlns:fes="http://www.opengis.net/fes/2.0">
<ows:OperationsMetadata>
<ows:Operation name="GetFeature">
<ows:Constraint name="CountDefault">
<ows:NoValues/>
<ows:DefaultValue>1</ows:DefaultValue>
</ows:Constraint>
</ows:Operation>
<ows:Constraint name="ImplementsTransactionalWFS">
<ows:NoValues/>
<ows:DefaultValue>TRUE</ows:DefaultValue>
</ows:Constraint>
<ows:Operation name="Transaction">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="http://{endpoint}"/>
<ows:Post xlink:href="{endpoint}"/>
</ows:HTTP>
</ows:DCP>
<ows:Parameter name="inputFormat">
<ows:AllowedValues>
<ows:Value>text/xml; subtype=gml/3.2</ows:Value>
</ows:AllowedValues>
</ows:Parameter>
<ows:Parameter name="releaseAction">
<ows:AllowedValues>
<ows:Value>ALL</ows:Value>
<ows:Value>SOME</ows:Value>
</ows:AllowedValues>
</ows:Parameter>
</ows:Operation>
</ows:OperationsMetadata>
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
<ows:WGS84BoundingBox>
<ows:LowerCorner>-71.123 66.33</ows:LowerCorner>
<ows:UpperCorner>-65.32 78.3</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
</wfs:WFS_Capabilities>""".format(endpoint=endpoint).encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml/3.2"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="intfield" nillable="true" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="0" name="longfield" nillable="true" type="xsd:long"/>
<xsd:element maxOccurs="1" minOccurs="0" name="stringfield" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="datetimefield" nillable="true" type="xsd:dateTime"/>
<xsd:element maxOccurs="1" minOccurs="0" name="geomfield" nillable="true" type="gml:PointPropertyType"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
# Create test layer
vl = QgsVectorLayer(u"url='http://" + endpoint + u"' typename='my:typename'", u'test', u'WFS')
self.assertTrue(vl.isValid())
self.assertNotEqual(vl.dataProvider().capabilities() & vl.dataProvider().EditingCapabilities, vl.dataProvider().NoCapabilities)
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
def testDeprecatedGML2GeometryDeclaration(self):
"""Test ref="gml:pointProperty" """
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_deprecated_gml2'
with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.0.0'), 'wb') as f:
f.write("""
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
<FeatureTypeList>
<FeatureType>
<Name>my:typename</Name>
<Title>Title</Title>
<Abstract>Abstract</Abstract>
<SRS>EPSG:32631</SRS>
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
</FeatureType>
</FeatureTypeList>
</WFS_Capabilities>""".encode('UTF-8'))
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" ref="gml:pointProperty"/>
<xsd:element maxOccurs="1" minOccurs="0" name="INTFIELD" nillable="true" type="xsd:int"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.wkbType(), QgsWkbTypes.Point)
self.assertEqual(len(vl.fields()), 1)
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631'), 'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml"
xmlns:my="http://my">
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
<gml:featureMember>
<my:typename fid="typename.0">
<gml:pointProperty>
<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326"><gml:coordinates decimal="." cs="," ts=" ">426858,5427937</gml:coordinates></gml:Point>
</gml:pointProperty>
<my:INTFIELD>1</my:INTFIELD>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))
values = [f['INTFIELD'] for f in vl.getFeatures()]
self.assertEqual(values, [1])
got_f = [f for f in vl.getFeatures()]
got = got_f[0].geometry().geometry()
self.assertEqual((got.x(), got.y()), (426858.0, 5427937.0))
if __name__ == '__main__':
unittest.main()