Skip to content

Commit

Permalink
Server OAPIF properties parameter
Browse files Browse the repository at this point in the history
Makes it possible to specify a comma separate list
of attributes to be returned by items call

This is apparently not in core specifications
but most of sample implementations supports it
and well, it's just useful.
  • Loading branch information
elpaso committed Nov 5, 2019
1 parent 459c105 commit 5f9405b
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 36 deletions.
94 changes: 72 additions & 22 deletions src/server/services/wfs3/qgswfs3handlers.cpp
Expand Up @@ -207,7 +207,7 @@ void QgsWfs3AbstractItemsHandler::checkLayerIsAccessible( const QgsVectorLayer *
}
}

QgsFeatureRequest QgsWfs3AbstractItemsHandler::filteredRequest( const QgsVectorLayer *vLayer, const QgsServerApiContext &context ) const
QgsFeatureRequest QgsWfs3AbstractItemsHandler::filteredRequest( const QgsVectorLayer *vLayer, const QgsServerApiContext &context, const QStringList &subsetAttributes ) const
{
QgsFeatureRequest featureRequest;
QgsExpressionContext expressionContext;
Expand All @@ -230,7 +230,8 @@ QgsFeatureRequest QgsWfs3AbstractItemsHandler::filteredRequest( const QgsVectorL
const QgsFields constFields { publishedFields( vLayer, context ) };
for ( const QgsField &f : constFields )
{
publishedAttrs.insert( f.name() );
if ( subsetAttributes.isEmpty() || subsetAttributes.contains( f.name( ) ) )
publishedAttrs.insert( f.name() );
}
featureRequest.setSubsetOfAttributes( publishedAttrs, vLayer->fields() );
return featureRequest;
Expand Down Expand Up @@ -799,6 +800,38 @@ QList<QgsServerQueryStringParameter> QgsWfs3CollectionsItemsHandler::parameters(
{
params.push_back( p );
}

const QgsFields published { publishedFields( mapLayer, context ) };
QStringList publishedFieldNames;
for ( const auto &f : published )
{
publishedFieldNames.push_back( f.name() );
}

// Properties (CSV list of properties to return)
QgsServerQueryStringParameter properties { QStringLiteral( "properties" ), false,
QgsServerQueryStringParameter::Type::List,
QStringLiteral( "Comma separated list of feature property names to be added to the result. Valid values: %1" )
.arg( publishedFieldNames.join( QStringLiteral( "', '" ) )
.append( '\'' )
.prepend( '\'' ) ) };

auto propertiesValidator = [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
const QStringList properties { value.toStringList() };
for ( const auto &p : properties )
{
if ( ! publishedFieldNames.contains( p ) )
{
return false;
}
}
return true;
};

properties.setCustomValidator( propertiesValidator );
params.push_back( properties );

}

// Check if is there any suitable datetime fields
Expand Down Expand Up @@ -904,34 +937,45 @@ json QgsWfs3CollectionsItemsHandler::schema( const QgsServerApiContext &context
const std::string title { mapLayer->title().isEmpty() ? mapLayer->name().toStdString() : mapLayer->title().toStdString() };
// Use layer id for operationId
const QString layerId { mapLayer->id() };
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/collections/%1/items" ).arg( shortName ), context.request()->url() ).toStdString() };
const QString path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/collections/%1/items" ).arg( shortName ), context.request()->url() ) };

json parameters =
static const QStringList componentNames
{
{{ "$ref", "#/components/parameters/limit" }},
{{ "$ref", "#/components/parameters/offset" }},
{{ "$ref", "#/components/parameters/resultType" }},
{{ "$ref", "#/components/parameters/bbox" }},
{{ "$ref", "#/components/parameters/bbox-crs" }},
{{ "$ref", "#/components/parameters/datetime" }}
QStringLiteral( "limit" ),
QStringLiteral( "offset" ),
QStringLiteral( "resultType" ),
QStringLiteral( "bbox" ),
QStringLiteral( "bbox-crs" ),
QStringLiteral( "crs" ),
QStringLiteral( "datetime" )
};

const QList<QgsServerQueryStringParameter> constFieldParameters { fieldParameters( mapLayer, context ) };
for ( const auto &p : constFieldParameters )
json componentParameters = json::array();
for ( const QString &name : componentNames )
{
const std::string name { p.name().toStdString() };
parameters.push_back( p.data() );
componentParameters.push_back( {{ "$ref", "#/components/parameters/" + name.toStdString() }} );
}

data[ path ] =
// Add layer specific filters
QgsServerApiContext layerContext( context );
QgsBufferServerRequest layerRequest( path );
layerContext.setRequest( &layerRequest );
const auto requestParameters { parameters( layerContext ) };
for ( const auto &p : requestParameters )
{
if ( ! componentNames.contains( p.name() ) )
componentParameters.push_back( p.data() );
}

data[ path.toStdString() ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", "Retrieve features of '" + title + "' feature collection" },
{ "description", description() },
{ "operationId", operationId() + '_' + layerId.toStdString() },
{ "parameters", parameters },
{ "parameters", componentParameters },
{
"responses", {
{
Expand Down Expand Up @@ -1060,8 +1104,8 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c

// Attribute filters
QgsStringMap attrFilters;
const QgsFields constFields { publishedFields( mapLayer, context ) };
for ( const QgsField &f : constFields )
const QgsFields constPublishedFields { publishedFields( mapLayer, context ) };
for ( const QgsField &f : constPublishedFields )
{
const QString fName { f.alias().isEmpty() ? f.name() : f.alias() };
const QString val = params.value( fName ).toString() ;
Expand All @@ -1087,7 +1131,7 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
QStringList expressions;

// datetime
const QString datetime { context.request()->queryParameter( QStringLiteral( "datetime" ) ) };
const QString datetime { params.value( QStringLiteral( "datetime" ) ).toString() };
if ( ! datetime.isEmpty() )
{
const QgsExpression timeExpression { QgsServerApiUtils::temporalFilterExpression( mapLayer, datetime ) };
Expand All @@ -1101,8 +1145,15 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
}
}

// Inputs are valid, process request
QgsFeatureRequest featureRequest = filteredRequest( mapLayer, context );
// Properties (subset attributes)
const QStringList requestedProperties { params.value( QStringLiteral( "properties" ) ).toStringList( ) };


// ////////////////////////////////////////////////////////////////////////////////////////////////////
// End of input control: inputs are valid, process the request

QgsFeatureRequest featureRequest = filteredRequest( mapLayer, context, requestedProperties );

if ( ! filterRect.isNull() )
{
QgsCoordinateTransform ct( bboxCrs, mapLayer->crs(), context.project()->transformContext() );
Expand Down Expand Up @@ -1141,7 +1192,6 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
QgsDebugMsgLevel( QStringLiteral( "Filter expression: %1" ).arg( featureRequest.filterExpression()->expression() ), 4 );
}


// WFS3 core specs only serves 4326
featureRequest.setDestinationCrs( crs, context.project()->transformContext() );
// Add offset to limit because paging is not supported by QgsFeatureRequest
Expand Down
3 changes: 2 additions & 1 deletion src/server/services/wfs3/qgswfs3handlers.h
Expand Up @@ -47,9 +47,10 @@ class QgsWfs3AbstractItemsHandler: public QgsServerOgcApiHandler
* Creates a filtered QgsFeatureRequest containing only fields published for WMS and plugin filters applied.
* \param layer the vector layer
* \param context the server api context
* \param subsetAttributes optional list of field names requested by the client, if empty all published attributes will be added to the request
* \return QgsFeatureRequest with filters applied
*/
QgsFeatureRequest filteredRequest( const QgsVectorLayer *layer, const QgsServerApiContext &context ) const;
QgsFeatureRequest filteredRequest( const QgsVectorLayer *layer, const QgsServerApiContext &context, const QStringList &subsetAttributes = QStringList() ) const;

/**
* Returns a filtered list of fields containing only fields published for WMS and plugin filters applied.
Expand Down
33 changes: 33 additions & 0 deletions tests/src/python/test_qgsserver_api.py
Expand Up @@ -532,6 +532,39 @@ def test_wfs3_field_filters(self):
self.assertEqual(response.statusCode(), 200)
self.compareApi(request, project, 'test_wfs3_collections_items_layer1_with_short_name_eq_two.json')

def test_wfs3_collection_items_properties(self):
"""Test WFS3 API items"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project_api.qgs')

# Invalid request
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, project)
self.assertEqual(bytes(response.body()).decode('utf8'), '[{"code":"Bad request error","description":"Argument \'properties\' is not valid. Comma separated list of feature property names to be added to the result. Valid values: \'id\', \'name\', \'utf8nameè\'"}]')

# Valid request
response = QgsBufferServerResponse()
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties=name')
self.server.handleRequest(request, response, project)
j = json.loads(bytes(response.body()).decode('utf8'))
self.assertTrue('name' in j['features'][0]['properties'])
self.assertFalse('id' in j['features'][0]['properties'])

response = QgsBufferServerResponse()
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties=name,id')
self.server.handleRequest(request, response, project)
j = json.loads(bytes(response.body()).decode('utf8'))
self.assertTrue('name' in j['features'][0]['properties'])
self.assertTrue('id' in j['features'][0]['properties'])

response = QgsBufferServerResponse()
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?properties=id')
self.server.handleRequest(request, response, project)
j = json.loads(bytes(response.body()).decode('utf8'))
self.assertFalse('name' in j['features'][0]['properties'])
self.assertTrue('id' in j['features'][0]['properties'])

def test_wfs3_field_filters_star(self):
"""Test field filters"""
project = QgsProject()
Expand Down

0 comments on commit 5f9405b

Please sign in to comment.