Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Server WFS NULL values support
- expose nillable in describefeaturetype
- serve xsi:nil="true" in getfeature
- check for NULL in transactions and report an error

Fixes #20961  - plus some other unreported
  • Loading branch information
elpaso committed Jan 23, 2019
1 parent 847e7ef commit 71e0116
Show file tree
Hide file tree
Showing 41 changed files with 322 additions and 272 deletions.
7 changes: 6 additions & 1 deletion src/server/services/wfs/qgswfsdescribefeaturetype.cpp
Expand Up @@ -79,7 +79,7 @@ namespace QgsWfs
// test oFormat
if ( oFormat == QgsWfsParameters::Format::NONE )
throw QgsBadRequestException( QStringLiteral( "Invalid WFS Parameter" ),
"OUTPUTFORMAT " + wfsParameters.outputFormatAsString() + "is not supported" );
QStringLiteral( "OUTPUTFORMAT %1 is not supported" ).arg( wfsParameters.outputFormatAsString() ) );

QgsAccessControl *accessControl = serverIface->accessControls();

Expand Down Expand Up @@ -346,6 +346,11 @@ namespace QgsWfs
}
}

if ( !( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull ) )
{
attElem.setAttribute( QStringLiteral( "nillable" ), QStringLiteral( "true" ) );
}

sequenceElem.appendChild( attElem );

QString alias = field.alias();
Expand Down
18 changes: 10 additions & 8 deletions src/server/services/wfs/qgswfsgetfeature.cpp
Expand Up @@ -1334,16 +1334,16 @@ namespace QgsWfs
{
continue;
}
if ( featureAttributes[idx].isNull() )
{
continue;
}
const QgsField field = fields.at( idx );
const QgsEditorWidgetSetup setup = field.editorWidgetSetup();
QString attributeName = field.name();

QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) );
if ( featureAttributes[idx].isNull() )
{
fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) );
}
fieldElem.appendChild( fieldText );
typeNameElement.appendChild( fieldElem );
}
Expand Down Expand Up @@ -1435,16 +1435,18 @@ namespace QgsWfs
{
continue;
}
if ( featureAttributes[idx].isNull() )
{
continue;
}

const QgsField field = fields.at( idx );
const QgsEditorWidgetSetup setup = field.editorWidgetSetup();

QString attributeName = field.name();

QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) );
if ( featureAttributes[idx].isNull() )
{
fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) );
}
fieldElem.appendChild( fieldText );
typeNameElement.appendChild( fieldElem );
}
Expand Down
1 change: 1 addition & 0 deletions src/server/services/wfs/qgswfstransaction.cpp
Expand Up @@ -411,6 +411,7 @@ namespace QgsWfs
{
if ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull )
{
action.error = true;
action.errorMsg = QStringLiteral( "NOT NULL constraint error error on layer '%1', field '%2'" ).arg( typeName, field.name() );
vlayer->rollBack();
break;
Expand Down
46 changes: 35 additions & 11 deletions src/server/services/wfs/qgswfstransaction_1_0_0.cpp
Expand Up @@ -388,26 +388,50 @@ namespace QgsWfs
}
QgsField field = fields.at( fieldMapIt.value() );
QVariant value = it.value();
if ( field.type() == 2 )
if ( value.isNull() )
{
value = it.value().toInt( &conversionSuccess );
if ( !conversionSuccess )
if ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
action.errorMsg = QStringLiteral( "NOT NULL constraint error error on layer '%1', field '%2'" ).arg( typeName, field.name() );
vlayer->rollBack();
break;
}
}
else if ( field.type() == 6 )
else // Not NULL
{
value = it.value().toDouble( &conversionSuccess );
if ( !conversionSuccess )
if ( field.type() == QVariant::Type::Int )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
value = it.value().toInt( &conversionSuccess );
if ( !conversionSuccess )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
}
}
else if ( field.type() == QVariant::Type::Double )
{
value = it.value().toDouble( &conversionSuccess );
if ( !conversionSuccess )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
}
}
else if ( field.type() == QVariant::Type::LongLong )
{
value = it.value().toLongLong( &conversionSuccess );
if ( !conversionSuccess )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
}
}
}
vlayer->changeAttributeValue( feature.id(), fieldMapIt.value(), value );
Expand Down
76 changes: 47 additions & 29 deletions tests/src/python/test_qgsserver_wfs.py
Expand Up @@ -446,19 +446,19 @@ def test_describeFeatureType(self):
'wfs_describeFeatureType_1_1_0_typename_wrong', project_file=project_file)

def test_getFeatureFeature_0_nulls(self):
"""Test that 0 and null in integer columns are reported correctly: note that WFS does not support NULL but QGIS Server does"""
"""Test that 0 and null in integer columns are reported correctly"""

# Test transactions with 0 and nulls

post_data = b"""<?xml version="1.0" ?>
<wfs:Transaction service="WFS" version="1.1.0"
post_data = """<?xml version="1.0" ?>
<wfs:Transaction service="WFS" version="{version}"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml">
<wfs:Update typeName="cdb_lines">
<wfs:Property>
<wfs:Name>id_long</wfs:Name>
<wfs:Value>%s</wfs:Value>
<wfs:Name>{field}</wfs:Name>
<wfs:Value>{value}</wfs:Value>
</wfs:Property>
<fes:Filter>
<fes:FeatureId fid="cdb_lines.22"/>
Expand All @@ -467,37 +467,55 @@ def test_getFeatureFeature_0_nulls(self):
</wfs:Transaction>
"""

def _round_trip(value):
"""Set a value on fid 22 and long_id field and check it back"""
def _round_trip(value, field, version='1.1.0'):
"""Set a value on fid 22 and field and check it back"""

header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'), QgsServerRequest.PostMethod, post_data % value)
self.assertTrue(b'<TotalUpdated>1</TotalUpdated>' in body)
encoded_data = post_data.format(field=field, value=value, version=version).encode('utf8')
# Strip the field if NULL
if value is None:
encoded_data = encoded_data.replace(b'<wfs:Value>None</wfs:Value>', b'')

header, body = self._execute_request("?MAP=%s&SERVICE=WFS&VERSION=%s" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs', version), QgsServerRequest.PostMethod, encoded_data)
if version == '1.0.0':
self.assertTrue(b'<SUCCESS/>' in body, body)
else:
self.assertTrue(b'<TotalUpdated>1</TotalUpdated>' in body, body)
header, body = self._execute_request("?MAP=%s&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=cdb_lines&FEATUREID=cdb_lines.22" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'))
self.assertTrue(b'<qgs:id_long>%s</qgs:id_long>' % value in body)
if value is not None:
xml_value = '<qgs:{0}>{1}</qgs:{0}>'.format(field, value).encode('utf8')
self.assertTrue(xml_value in body, "%s not found in body" % xml_value)
else:
xml_value = '<qgs:{0}>'.format(field).encode('utf8')
self.assertFalse(xml_value in body)
# Check the backend
vl = QgsVectorLayer(
self.testdata_path + 'test_project_wms_grouped_layers.gpkg|layername=cdb_lines', 'vl', 'ogr')
self.assertTrue(vl.isValid())
self.assertEqual(str(vl.getFeature(22)['id_long']).encode(
'utf8'), value if value != b'' else b'NULL')

_round_trip(b'0')
_round_trip(b'12345')

# Now check NULL
header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % (self.testdata_path + 'test_project_wms_grouped_layers.qgs'),
QgsServerRequest.PostMethod, (post_data % b'').replace(b'<wfs:Value></wfs:Value>', b''))
self.assertTrue(b'<TotalUpdated>1</TotalUpdated>' in body)
vl = QgsVectorLayer(
self.testdata_path + 'test_project_wms_grouped_layers.gpkg|layername=cdb_lines', 'vl', 'ogr')
self.assertTrue(vl.isValid())
self.assertTrue(vl.getFeature(22)['id_long'].isNull())
del(vl)
header, body = self._execute_request("?MAP=%s&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=cdb_lines&FEATUREID=cdb_lines.22" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'))
self.assertFalse(b'<qgs:id_long></qgs:id_long>' in body)
self.assertEqual(
str(vl.getFeature(22)[field]), value if value is not None else 'NULL')

for version in ('1.0.0', '1.1.0'):
_round_trip('0', 'id_long', version)
_round_trip('12345', 'id_long', version)
_round_trip('0', 'id', version)
_round_trip('12345', 'id', version)
_round_trip(None, 'id', version)
_round_trip(None, 'id_long', version)

# "name" is NOT NULL: try to set it to empty string
_round_trip('', 'name', version)
# Then NULL
data = post_data.format(field='name', value='', version=version).encode('utf8')
encoded_data = data.replace(b'<wfs:Value></wfs:Value>', b'')
header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'), QgsServerRequest.PostMethod, encoded_data)
if version == '1.0.0':
self.assertTrue(b'<ERROR/>' in body, body)
else:
self.assertTrue(b'<TotalUpdated>0</TotalUpdated>' in body)
self.assertTrue(b'<Message>NOT NULL constraint error error on layer \'cdb_lines\', field \'name\'</Message>' in body)


if __name__ == '__main__':
Expand Down
Binary file not shown.
@@ -1,4 +1,4 @@
Content-Length: 1840
Content-Length: 2176
Content-Type: text/xml; charset=utf-8

<schema xmlns:gml="http://www.opengis.net/gml" targetNamespace="http://www.qgis.org/gml" xmlns:qgs="http://www.qgis.org/gml" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ogc="http://www.opengis.net/ogc" version="1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
Expand All @@ -10,27 +10,27 @@ Content-Type: text/xml; charset=utf-8
<sequence>
<element maxOccurs="1" type="gml:MultiPolygonPropertyType" minOccurs="0" name="geometry"/>
<element type="long" name="fid"/>
<element type="int" name="gid"/>
<element type="string" name="datum"/>
<element type="string" name="bearbeiter"/>
<element type="string" name="veranstaltung"/>
<element type="string" name="beschriftung"/>
<element type="string" name="name"/>
<element type="string" name="flaechentyp"/>
<element type="string" name="farbe"/>
<element type="string" name="schraff_width"/>
<element type="string" name="schraff_width_prt"/>
<element type="string" name="schraff_size"/>
<element type="string" name="schraff_size_prt"/>
<element type="string" name="schraff_winkel"/>
<element type="string" name="umrissfarbe"/>
<element type="string" name="umrisstyp"/>
<element type="string" name="umrissstaerke"/>
<element type="string" name="umrissstaerke_prt"/>
<element type="decimal" name="umfang"/>
<element type="decimal" name="flaeche"/>
<element type="string" name="bemerkung"/>
<element type="string" name="last_change"/>
<element type="int" nillable="true" name="gid"/>
<element type="string" nillable="true" name="datum"/>
<element type="string" nillable="true" name="bearbeiter"/>
<element type="string" nillable="true" name="veranstaltung"/>
<element type="string" nillable="true" name="beschriftung"/>
<element type="string" nillable="true" name="name"/>
<element type="string" nillable="true" name="flaechentyp"/>
<element type="string" nillable="true" name="farbe"/>
<element type="string" nillable="true" name="schraff_width"/>
<element type="string" nillable="true" name="schraff_width_prt"/>
<element type="string" nillable="true" name="schraff_size"/>
<element type="string" nillable="true" name="schraff_size_prt"/>
<element type="string" nillable="true" name="schraff_winkel"/>
<element type="string" nillable="true" name="umrissfarbe"/>
<element type="string" nillable="true" name="umrisstyp"/>
<element type="string" nillable="true" name="umrissstaerke"/>
<element type="string" nillable="true" name="umrissstaerke_prt"/>
<element type="decimal" nillable="true" name="umfang"/>
<element type="decimal" nillable="true" name="flaeche"/>
<element type="string" nillable="true" name="bemerkung"/>
<element type="string" nillable="true" name="last_change"/>
</sequence>
</extension>
</complexContent>
Expand Down

0 comments on commit 71e0116

Please sign in to comment.