Server OAPIF datetime support

This commit is contained in:
Alessandro Pasotti 2019-10-18 08:01:19 +02:00
parent b2e7121493
commit 6112da45f6
24 changed files with 519 additions and 94 deletions

View File

@ -1904,6 +1904,18 @@ A set of attributes that are not advertised in WFS requests with QGIS server.
void setExcludeAttributesWfs( const QSet<QString> &att );
%Docstring
A set of attributes that are not advertised in WFS requests with QGIS server.
%End
QSet<QString> includeAttributesOapifTemporalFilters() const;
%Docstring
Returns the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
\since: QGIS 3.12
%End
void setIncludeAttributesOapifTemporalFilters( const QSet<QString> &att );
%Docstring
Sets the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
\since: QGIS 3.12
%End
virtual bool deleteAttribute( int attr );

View File

@ -35,6 +35,41 @@ Parses a comma separated ``bbox`` into a (possibly empty) :py:class:`QgsRectangl
Z values (i.e. a 6 elements bbox) are silently discarded
%End
struct TemporalDateInterval
{
QDate begin;
QDate end;
};
struct TemporalDateTimeInterval
{
QDateTime begin;
QDateTime end;
};
static TemporalDateInterval parseTemporalDateInterval( const QString &interval ) throw( QgsServerApiBadRequestException );
%Docstring
Parse a date time ``interval`` and returns a TemporalInterval
:raises QgsServerApiBadRequestException: if interval cannot be parsed
%End
static TemporalDateTimeInterval parseTemporalDateTimeInterval( const QString &interval ) throw( QgsServerApiBadRequestException );
static QgsExpression temporalFilterExpression( const QgsVectorLayer *layer, const QString &interval );
%Docstring
Parse the ``interval`` and constructs a (possibily invalid) temporal filter expression for the given ``layer``
Syntax:
interval-closed = date-time "/" date-time
interval-open-start = [".."] "/" date-time
interval-open-end = date-time "/" [".."]
interval = interval-closed / interval-open-start / interval-open-end
datetime = date-time / interval
%End
static QgsCoordinateReferenceSystem parseCrs( const QString &bboxCrs );
%Docstring

View File

@ -60,8 +60,16 @@ QgsSourceFieldsProperties::QgsSourceFieldsProperties( QgsVectorLayer *layer, QWi
mFieldsList->setHorizontalHeaderItem( AttrLengthCol, new QTableWidgetItem( tr( "Length" ) ) );
mFieldsList->setHorizontalHeaderItem( AttrPrecCol, new QTableWidgetItem( tr( "Precision" ) ) );
mFieldsList->setHorizontalHeaderItem( AttrCommentCol, new QTableWidgetItem( tr( "Comment" ) ) );
mFieldsList->setHorizontalHeaderItem( AttrWMSCol, new QTableWidgetItem( QStringLiteral( "WMS" ) ) );
mFieldsList->setHorizontalHeaderItem( AttrWFSCol, new QTableWidgetItem( QStringLiteral( "WFS" ) ) );
const auto wmsWi = new QTableWidgetItem( QStringLiteral( "WMS" ) );
wmsWi->setToolTip( tr( "Defines if this field is available in QGIS Server WMS service" ) );
mFieldsList->setHorizontalHeaderItem( AttrWMSCol, wmsWi );
const auto wfsWi = new QTableWidgetItem( QStringLiteral( "WFS" ) );
wfsWi->setToolTip( tr( "Defines if this field is available in QGIS Server WFS (and OAPIF) service" ) );
mFieldsList->setHorizontalHeaderItem( AttrWFSCol, wfsWi );
mFieldsList->setHorizontalHeaderItem( AttrAliasCol, new QTableWidgetItem( tr( "Alias" ) ) );
const auto oapifWi = new QTableWidgetItem( QStringLiteral( "Temporal OAPIF" ) );
oapifWi->setToolTip( tr( "Defines if this field will be used for temporal filtering in QGIS Server OAPIF service" ) );
mFieldsList->setHorizontalHeaderItem( AttrOapifDateTimeCol, oapifWi );
mFieldsList->setHorizontalHeaderItem( AttrAliasCol, new QTableWidgetItem( tr( "Alias" ) ) );
mFieldsList->setSortingEnabled( true );
@ -265,6 +273,23 @@ void QgsSourceFieldsProperties::setRow( int row, int idx, const QgsField &field
wfsAttrItem->setCheckState( mLayer->excludeAttributesWfs().contains( field.name() ) ? Qt::Unchecked : Qt::Checked );
wfsAttrItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable );
mFieldsList->setItem( row, AttrWFSCol, wfsAttrItem );
// OAPIF temporal
QTableWidgetItem *oapifAttrItem = new QTableWidgetItem();
// ok, in theory, we could support any field type that
// can contain something convertible to a date/datetime, but
// let's keep it simple for now
if ( field.isDateOrTime() )
{
oapifAttrItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable );
oapifAttrItem->setCheckState( mLayer->includeAttributesOapifTemporalFilters().contains( field.name() ) ? Qt::Checked : Qt::Unchecked );
}
else
{
oapifAttrItem->setFlags( oapifAttrItem->flags() & ~Qt::ItemIsSelectable & ~Qt::ItemIsEnabled & ~Qt::ItemIsUserCheckable );
}
mFieldsList->setItem( row, AttrOapifDateTimeCol, oapifAttrItem );
}
bool QgsSourceFieldsProperties::addAttribute( const QgsField &field )
@ -286,7 +311,7 @@ bool QgsSourceFieldsProperties::addAttribute( const QgsField &field )
void QgsSourceFieldsProperties::apply()
{
QSet<QString> excludeAttributesWMS, excludeAttributesWFS;
QSet<QString> excludeAttributesWMS, excludeAttributesWFS, includeTemporalFilters;
for ( int i = 0; i < mFieldsList->rowCount(); i++ )
{
@ -298,11 +323,16 @@ void QgsSourceFieldsProperties::apply()
{
excludeAttributesWFS.insert( mFieldsList->item( i, AttrNameCol )->text() );
}
if ( mFieldsList->item( i, AttrOapifDateTimeCol )->checkState() == Qt::Checked )
{
includeTemporalFilters.insert( mFieldsList->item( i, AttrNameCol )->text() );
}
}
mLayer->setExcludeAttributesWms( excludeAttributesWMS );
mLayer->setExcludeAttributesWfs( excludeAttributesWFS );
// Note: this is a whitelist unlike the previous two!
mLayer->setIncludeAttributesOapifTemporalFilters( includeTemporalFilters );
}
//SLOTS

View File

@ -79,6 +79,7 @@ class APP_EXPORT QgsSourceFieldsProperties : public QWidget, private Ui_QgsSourc
AttrCommentCol,
AttrWMSCol,
AttrWFSCol,
AttrOapifDateTimeCol, //! OAPIF (WFS3) datetime filter column
AttrColCount,
};

View File

@ -2192,6 +2192,17 @@ bool QgsVectorLayer::readSymbology( const QDomNode &layerNode, QString &errorMes
}
}
mIncludeAttributesOapifTemporalFilters.clear();
QDomNode includeOapifTemporalFiltersNode = layerNode.namedItem( QStringLiteral( "includeAttributesOapifTemporalFilters" ) );
if ( !excludeWFSNode.isNull() )
{
QDomNodeList attributeNodeList = includeOapifTemporalFiltersNode.toElement().elementsByTagName( QStringLiteral( "attribute" ) );
for ( int i = 0; i < attributeNodeList.size(); ++i )
{
mIncludeAttributesOapifTemporalFilters.insert( attributeNodeList.at( i ).toElement().text() );
}
}
// Load editor widget configuration
QDomElement widgetsElem = layerNode.namedItem( QStringLiteral( "fieldConfiguration" ) ).toElement();
QDomNodeList fieldConfigurationElementList = widgetsElem.elementsByTagName( QStringLiteral( "field" ) );
@ -2505,6 +2516,18 @@ bool QgsVectorLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString
}
node.appendChild( excludeWFSElem );
//include attributes OAPIF
QDomElement includeOapifElem = doc.createElement( QStringLiteral( "includeAttributesOapifTemporalFilters" ) );
QSet<QString>::const_iterator attOapifTemporalIt = mIncludeAttributesOapifTemporalFilters.constBegin();
for ( ; attOapifTemporalIt != mIncludeAttributesOapifTemporalFilters.constEnd(); ++attOapifTemporalIt )
{
QDomElement attrElem = doc.createElement( QStringLiteral( "attribute" ) );
QDomText attrText = doc.createTextNode( *attOapifTemporalIt );
attrElem.appendChild( attrText );
includeOapifElem.appendChild( attrElem );
}
node.appendChild( includeOapifElem );
//default expressions
QDomElement defaultsElem = doc.createElement( QStringLiteral( "defaults" ) );
for ( const QgsField &field : mFields )

View File

@ -1775,6 +1775,18 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
*/
void setExcludeAttributesWfs( const QSet<QString> &att ) { mExcludeAttributesWFS = att; }
/**
* Returns the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
* \since: QGIS 3.12
*/
QSet<QString> includeAttributesOapifTemporalFilters() const { return mIncludeAttributesOapifTemporalFilters; }
/**
* Sets the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
* \since: QGIS 3.12
*/
void setIncludeAttributesOapifTemporalFilters( const QSet<QString> &att ) { mIncludeAttributesOapifTemporalFilters = att; }
/**
* Deletes an attribute field (but does not commit it).
*
@ -2728,6 +2740,9 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
//! Attributes which are not published in WFS
QSet<QString> mExcludeAttributesWFS;
//! Attributes which are used for OAPIF temporal filtering
QSet<QString> mIncludeAttributesOapifTemporalFilters;
//! Geometry type as defined in enum WkbType (qgis.h)
QgsWkbTypes::Type mWkbType = QgsWkbTypes::Unknown;

View File

@ -24,6 +24,7 @@
#include "qgsserverprojectutils.h"
#include "qgsmessagelog.h"
#include "nlohmann/json.hpp"
#include <QUrl>
@ -61,6 +62,199 @@ QgsRectangle QgsServerApiUtils::parseBbox( const QString &bbox )
return QgsRectangle();
}
template<typename T, class T2> T QgsServerApiUtils::parseTemporalInterval( const QString &interval )
{
auto parseDate = [ ]( const QString & date ) -> T2
{
T2 result;
if ( date == QStringLiteral( ".." ) || date.isEmpty() )
{
return result;
}
else
{
T2 result { T2::fromString( date, Qt::DateFormat::ISODate ) };
if ( !result.isValid() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "%1 is not a valid date/datetime." ).arg( date ) );
}
return result;
}
};
const QStringList parts { interval.split( '/' ) };
return { parseDate( parts[0] ), parseDate( parts[1] ) };
}
QgsServerApiUtils::TemporalDateInterval QgsServerApiUtils::parseTemporalDateInterval( const QString &interval )
{
return QgsServerApiUtils::parseTemporalInterval<QgsServerApiUtils::TemporalDateInterval, QDate>( interval );
}
QgsServerApiUtils::TemporalDateTimeInterval QgsServerApiUtils::parseTemporalDateTimeInterval( const QString &interval )
{
return QgsServerApiUtils::parseTemporalInterval<QgsServerApiUtils::TemporalDateTimeInterval, QDateTime>( interval );
}
QgsExpression QgsServerApiUtils::temporalFilterExpression( const QgsVectorLayer *layer, const QString &interval )
{
QgsExpression expression;
QStringList conditions;
// Is it an interval?
if ( interval.contains( '/' ) )
{
// Try date first
try
{
TemporalDateInterval dateInterval { QgsServerApiUtils::parseTemporalDateInterval( interval ) };
for ( const auto &fieldName : layer->includeAttributesOapifTemporalFilters() )
{
int fieldIdx { layer->fields().lookupField( fieldName ) };
if ( fieldIdx < 0 )
{
continue;
}
const QgsField field { layer->fields().at( fieldIdx ) };
QString fieldValue;
if ( field.type() == QVariant::Date )
{
fieldValue = QStringLiteral( R"raw("%1")raw" ).arg( fieldName );
}
else
{
fieldValue = QStringLiteral( R"raw(to_date( "%1" ))raw" ).arg( fieldName );
}
if ( ! dateInterval.begin.isValid( ) && ! dateInterval.end.isValid( ) )
{
// Nothing to do here: log?
}
else
{
if ( dateInterval.begin.isValid( ) )
{
conditions.push_back( QStringLiteral( R"raw(%1 >= to_date('%2'))raw" )
.arg( fieldValue )
.arg( dateInterval.begin.toString( Qt::DateFormat::ISODate ) ) );
}
if ( dateInterval.end.isValid( ) )
{
conditions.push_back( QStringLiteral( R"raw(%1 <= to_date('%2'))raw" )
.arg( fieldValue )
.arg( dateInterval.end.toString( Qt::DateFormat::ISODate ) ) );
}
}
}
}
catch ( QgsServerApiBadRequestException & ) // try datetime
{
TemporalDateTimeInterval dateTimeInterval { QgsServerApiUtils::parseTemporalDateTimeInterval( interval ) };
for ( const auto &fieldName : layer->includeAttributesOapifTemporalFilters() )
{
int fieldIdx { layer->fields().lookupField( fieldName ) };
if ( fieldIdx < 0 )
{
continue;
}
const QgsField field { layer->fields().at( fieldIdx ) };
QString fieldValue;
if ( field.type() == QVariant::Date )
{
fieldValue = QStringLiteral( R"raw("%1")raw" ).arg( fieldName );
}
else
{
fieldValue = QStringLiteral( R"raw(to_datetime( "%1" ))raw" ).arg( fieldName );
}
if ( ! dateTimeInterval.begin.isValid( ) && ! dateTimeInterval.end.isValid( ) )
{
// Nothing to do here: log?
}
else
{
if ( dateTimeInterval.begin.isValid( ) )
{
conditions.push_back( QStringLiteral( R"raw(%1 >= to_datetime('%2'))raw" )
.arg( fieldValue )
.arg( dateTimeInterval.begin.toString( Qt::DateFormat::ISODate ) ) );
}
if ( dateTimeInterval.end.isValid( ) )
{
conditions.push_back( QStringLiteral( R"raw(%1 <= to_datetime('%2'))raw" )
.arg( fieldValue )
.arg( dateTimeInterval.end.toString( Qt::DateFormat::ISODate ) ) );
}
}
}
}
}
else // plain value
{
for ( const auto &fieldName : layer->includeAttributesOapifTemporalFilters() )
{
int fieldIdx { layer->fields().lookupField( fieldName ) };
if ( fieldIdx < 0 )
{
continue;
}
const QgsField field { layer->fields().at( fieldIdx ) };
if ( field.type() == QVariant::Date )
{
conditions.push_back( QStringLiteral( R"raw("%1" = to_date('%2'))raw" )
.arg( fieldName )
.arg( interval ) );
}
else if ( field.type() == QVariant::DateTime )
{
conditions.push_back( QStringLiteral( R"raw("%1" = to_datetime('%2'))raw" )
.arg( fieldName )
.arg( interval ) );
}
else
{
// Guess the type from input
QDateTime dateTime { QDateTime::fromString( interval, Qt::DateFormat::ISODate )};
if ( dateTime.isValid() )
{
conditions.push_back( QStringLiteral( R"raw(to_datetime("%1") = to_datetime('%2'))raw" )
.arg( fieldName )
.arg( interval ) );
}
else
{
QDate date { QDate::fromString( interval, Qt::DateFormat::ISODate )};
if ( date.isValid() )
{
conditions.push_back( QStringLiteral( R"raw(to_date("%1") = to_date('%2'))raw" )
.arg( fieldName )
.arg( interval ) );
}
else
{
// Nothing done here, log?
}
}
}
}
}
if ( ! conditions.isEmpty() )
{
expression.setExpression( conditions.join( QStringLiteral( " AND " ) ) );
}
return expression;
}
json QgsServerApiUtils::layerExtent( const QgsVectorLayer *layer )
{
auto extent { layer->extent() };

View File

@ -26,6 +26,7 @@
#include "qgsproject.h"
#include "qgsserverprojectutils.h"
#include "qgsserverapicontext.h"
#include "qgsserverexception.h"
#ifdef HAVE_SERVER_PYTHON_PLUGINS
#include "qgsaccesscontrol.h"
@ -58,6 +59,44 @@ class SERVER_EXPORT QgsServerApiUtils
*/
static QgsRectangle parseBbox( const QString &bbox );
struct TemporalDateInterval
{
QDate begin;
QDate end;
};
struct TemporalDateTimeInterval
{
QDateTime begin;
QDateTime end;
};
/**
* Parse a date time \a interval and returns a TemporalInterval
*
* \throws QgsServerApiBadRequestException if interval cannot be parsed
*/
static TemporalDateInterval parseTemporalDateInterval( const QString &interval ) SIP_THROW( QgsServerApiBadRequestException );
static TemporalDateTimeInterval parseTemporalDateTimeInterval( const QString &interval ) SIP_THROW( QgsServerApiBadRequestException );
///@cond PRIVATE
template<typename T, class T2> static T parseTemporalInterval( const QString &interval ) SIP_SKIP;
/// @endcond
/**
* Parse the \a interval and constructs a (possibily invalid) temporal filter expression for the given \a layer
*
* Syntax:
*
* interval-closed = date-time "/" date-time
* interval-open-start = [".."] "/" date-time
* interval-open-end = date-time "/" [".."]
* interval = interval-closed / interval-open-start / interval-open-end
* datetime = date-time / interval
*/
static QgsExpression temporalFilterExpression( const QgsVectorLayer *layer, const QString &interval );
/**
* layerExtent returns json array with [xMin,yMin,xMax,yMax] CRS84 extent for the given \a layer
* FIXME: the OpenAPI swagger docs say that it is inverted axis order: West, north, east, south edges of the spatial extent.

View File

@ -756,6 +756,20 @@ QList<QgsServerQueryStringParameter> QgsWfs3CollectionsItemsHandler::parameters(
} );
params.push_back( limit );
// datetime
QgsServerQueryStringParameter datetime { QStringLiteral( "datetime" ), false,
QgsServerQueryStringParameter::Type::Integer,
QStringLiteral( "Date time filter" ),
};
datetime.setCustomValidator( [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
// TODO
return true;
} );
params.push_back( datetime );
// Offset
QgsServerQueryStringParameter offset { QStringLiteral( "offset" ), false,
QgsServerQueryStringParameter::Type::Integer,
@ -856,7 +870,7 @@ json QgsWfs3CollectionsItemsHandler::schema( const QgsServerApiContext &context
{ "$ref", "#/components/parameters/resultType" },
{ "$ref", "#/components/parameters/bbox" },
{ "$ref", "#/components/parameters/bbox-crs" },
// TODO: {{ "$ref", "#/components/parameters/time" }},
{ "$ref", "#/components/parameters/time" },
}
};
@ -1025,15 +1039,25 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
// so we do our own paging with "offset")
const qlonglong offset { params.value( QStringLiteral( "offset" ) ).toLongLong( &ok ) };
// TODO: make the max limit configurable
const qlonglong limit { params.value( QStringLiteral( "limit" ) ).toLongLong( &ok ) };
// TODO: implement time
const QString time { context.request()->queryParameter( QStringLiteral( "time" ) ) };
if ( ! time.isEmpty() )
QString filterExpression;
QStringList expressions;
/*/ datetime
const QString datetime { context.request()->queryParameter( QStringLiteral( "datetime" ) ) };
if ( ! datetime.isEmpty() )
{
throw QgsServerApiNotImplementedException( QStringLiteral( "Time filter is not implemented" ) ) ;
}
const QgsExpression timeExpression { QgsServerApiUtils::temporalFilterExpression( mapLayer, datetime ) };
if ( ! timeExpression.isValid() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Invalid datetime filter expression: %1 " ).arg( datetime ) );
}
else
{
expressions.push_back( timeExpression.expression() );
}
}*/
// Inputs are valid, process request
QgsFeatureRequest featureRequest = filteredRequest( mapLayer, context );
@ -1044,10 +1068,9 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
featureRequest.setFilterRect( ct.transform( filterRect ) );
}
QString filterExpression;
if ( ! attrFilters.isEmpty() )
{
QStringList expressions;
if ( featureRequest.filterExpression() && ! featureRequest.filterExpression()->expression().isEmpty() )
{
expressions.push_back( featureRequest.filterExpression()->expression() );
@ -1066,10 +1089,12 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
expressions.push_back( QStringLiteral( "\"%1\" = '%2'" ).arg( it.key() ).arg( it.value() ) );
}
}
filterExpression = expressions.join( QStringLiteral( " AND " ) );
featureRequest.setFilterExpression( filterExpression );
}
// Join all expression filters
filterExpression = expressions.join( QStringLiteral( " AND " ) );
featureRequest.setFilterExpression( filterExpression );
// WFS3 core specs only serves 4326
featureRequest.setDestinationCrs( crs, context.project()->transformContext() );
// Add offset to limit because paging is not supported by QgsFeatureRequest

View File

@ -92,7 +92,7 @@ class QgsServerAPIUtilsTest(QgsServerTestBase):
def test_append_path(self):
path = QgsServerApiUtils.appendMapParameter('/wfs3', QtCore.QUrl('https://www.qgis.org/wfs3?MAP=/some/path'))
self.assertEquals(path, '/wfs3?MAP=/some/path')
self.assertEqual(path, '/wfs3?MAP=/some/path')
class API(QgsServerApi):
@ -141,6 +141,15 @@ class QgsServerAPITestBase(QgsServerTestBase):
result = bytes(response.body()).decode('utf8') if reference_file.endswith('html') else self.dump(response)
path = unitTestDataPath('qgis_server') + '/api/' + reference_file
if self.regeregenerate_api_reference:
# Try to change timestamp
try:
content = result.split('\n')
j = ''.join(content[content.index('') + 1:])
j = json.loads(j)
j['timeStamp'] = '2019-07-05T12:27:07Z'
result = '\n'.join(content[:2]) + '\n' + json.dumps(j, ensure_ascii=False, indent=2)
except:
pass
f = open(path.encode('utf8'), 'w+', encoding='utf8')
f.write(result)
f.close()
@ -462,6 +471,55 @@ class QgsServerAPITest(QgsServerAPITestBase):
response = self.compareApi(request, project, 'test_wfs3_collections_items_layer1_with_short_name_eq_tw_star.json')
self.assertEqual(response.statusCode(), 200)
def test_wfs3_time_filters(self):
"""Test datetime filters"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project_api_timefilters.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/points/items?name=lus*')
response = self.compareApi(request, project, 'test_wfs3_collections_items_points_eq_lus.json')
self.assertEqual(response.statusCode(), 200)
# Prepare 3 projects with all three options: "created", "updated", both.
tmpDir = QtCore.QTemporaryDir()
layer = list(project.mapLayers().values())[0]
layer.setIncludeAttributesOapifTemporalFilters(['created'])
created_path = os.path.join(tmpDir.path(), 'test_project_api_timefilters_created.qgs')
project.write(created_path)
layer.setIncludeAttributesOapifTemporalFilters(['updated'])
updated_path = os.path.join(tmpDir.path(), 'test_project_api_timefilters_updated.qgs')
project.write(updated_path)
layer.setIncludeAttributesOapifTemporalFilters(['updated', 'created'])
both_path = os.path.join(tmpDir.path(), 'test_project_api_timefilters_both.qgs')
project.write(both_path)
# Test data:
#wkt_geom fid name created updated
#Point (7.30355493642693343 44.82162158126364915) 2 bricherasio 2019-05-05 2020-05-05T05:05:05.000
#Point (7.2500747591236081 44.81342128741047048) 1 luserna 2019-01-01 2020-01-01T10:10:10.000
# What to test:
#interval-closed = date-time "/" date-time
#interval-open-start = [".."] "/" date-time
#interval-open-end = date-time "/" [".."]
#interval = interval-closed / interval-open-start / interval-open-end
#datetime = date-time / interval
def _date_tester(project_path, expected, unexpected):
# Test "created" date field exact
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/points/items?datetime=2019-05-05')
response = QgsBufferServerResponse()
project.read(project_path)
self.server.handleRequest(request, response, project)
body = bytes(response.body()).decode('utf8')
for exp in expected:
self.assertTrue(exp in body)
for unexp in unexpected:
self.assertFalse(unexp in body)
# Test "created" date field exact
_date_tester(created_path, ['bricherasio'], ['luserna'])
def test_wfs3_excluded_attributes(self):
"""Test excluded attributes"""
project = QgsProject()
@ -706,10 +764,10 @@ class QgsServerOgcAPITest(QgsServerAPITestBase):
self.assertEqual(str(ex.exception), 'Template not found: handlerThree.html')
# Define a template path
dir = QtCore.QTemporaryDir()
with open(dir.path() + '/handlerThree.html', 'w+') as f:
tmpDir = QtCore.QTemporaryDir()
with open(tmpDir.path() + '/handlerThree.html', 'w+') as f:
f.write("Hello world")
h3.templatePathOverride = dir.path() + '/handlerThree.html'
h3.templatePathOverride = tmpDir.path() + '/handlerThree.html'
ctx.response().clear()
api.executeRequest(ctx)
self.assertEqual(bytes(ctx.response().data()), b"Hello world")

View File

@ -1328,5 +1328,6 @@ Content-Type: application/openapi+json;version=3.0
"description": "Access to data (features).",
"name": "Features"
}
]
],
"timeStamp": "2019-07-05T12:27:07Z"
}

View File

@ -19,5 +19,5 @@ Content-Type: application/json
"type": "text/html"
}
],
"timeStamp": "2019-10-16T13:53:56Z"
}
"timeStamp": "2019-07-05T12:27:07Z"
}

View File

@ -27,6 +27,6 @@ Content-Type: application/geo+json
"id": 1,
"utf8nameè": "one èé"
},
"timeStamp": "2019-10-16T13:53:57Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "Feature"
}
}

View File

@ -35,6 +35,6 @@ Content-Type: application/geo+json
],
"numberMatched": 1,
"numberReturned": 1,
"timeStamp": "2019-10-16T13:53:57Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -35,6 +35,6 @@ Content-Type: application/geo+json
],
"numberMatched": 1,
"numberReturned": 1,
"timeStamp": "2019-10-16T13:53:57Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -67,6 +67,6 @@ Content-Type: application/geo+json
],
"numberMatched": 3,
"numberReturned": 3,
"timeStamp": "2019-10-16T13:53:55Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -35,6 +35,6 @@ Content-Type: application/geo+json
],
"numberMatched": 1,
"numberReturned": 1,
"timeStamp": "2019-10-16T13:53:55Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -51,6 +51,6 @@ Content-Type: application/geo+json
],
"numberMatched": 2,
"numberReturned": 2,
"timeStamp": "2019-10-16T13:53:55Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -67,6 +67,6 @@ Content-Type: application/geo+json
],
"numberMatched": 3,
"numberReturned": 3,
"timeStamp": "2019-10-16T13:53:55Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -42,6 +42,6 @@ Content-Type: application/geo+json
],
"numberMatched": 3,
"numberReturned": 1,
"timeStamp": "2019-10-16T13:53:55Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -49,6 +49,6 @@ Content-Type: application/geo+json
],
"numberMatched": 3,
"numberReturned": 1,
"timeStamp": "2019-10-16T13:53:55Z",
"timeStamp": "2019-07-05T12:27:07Z",
"type": "FeatureCollection"
}
}

View File

@ -10,19 +10,16 @@ Content-Type: application/json
],
"description": "A test vector layer with unicode òà",
"extent": {
"spatial": {
"bbox": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
],
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"spatial": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
]
},
"id": "testlayer èé",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json",
@ -37,6 +34,7 @@ Content-Type: application/json
"type": "text/html"
}
],
"name": "testlayer èé",
"title": "A test vector layer èé"
},
{
@ -47,19 +45,16 @@ Content-Type: application/json
],
"description": "A Layer1 with an abstract",
"extent": {
"spatial": {
"bbox": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
],
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"spatial": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
]
},
"id": "layer1_with_short_name",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/items.json",
@ -74,6 +69,7 @@ Content-Type: application/json
"type": "text/html"
}
],
"name": "layer1_with_short_name",
"title": "A Layer1 with a short name"
},
{
@ -84,19 +80,16 @@ Content-Type: application/json
],
"description": "A test vector layer with unicode òà",
"extent": {
"spatial": {
"bbox": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
],
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"spatial": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
]
},
"id": "exclude_attribute",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/exclude_attribute/items.json",
@ -111,6 +104,7 @@ Content-Type: application/json
"type": "text/html"
}
],
"name": "exclude_attribute",
"title": "A test vector layer exclude attrs"
},
{
@ -121,19 +115,16 @@ Content-Type: application/json
],
"description": "A test vector layer with aliases",
"extent": {
"spatial": {
"bbox": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
],
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"spatial": [
[
8.203459307036344,
44.90139483904469,
8.203546993993488,
44.901482526001836
]
]
},
"id": "fields_alias",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/fields_alias/items.json",
@ -148,6 +139,7 @@ Content-Type: application/json
"type": "text/html"
}
],
"name": "fields_alias",
"title": "A test vector layer with aliases"
}
],
@ -170,5 +162,5 @@ Content-Type: application/json
"type": "text/html"
}
],
"timeStamp": "2019-10-16T13:53:56Z"
}
"timeStamp": "2019-07-05T12:27:07Z"
}

View File

@ -22,5 +22,5 @@ Content-Type: application/json
"type": "text/html"
}
],
"timeStamp": "2019-10-16T13:53:57Z"
}
"timeStamp": "2019-07-05T12:27:07Z"
}

View File

@ -33,5 +33,5 @@ Content-Type: application/json
"type": "application/openapi+json;version=3.0"
}
],
"timeStamp": "2019-10-16T13:53:57Z"
}
"timeStamp": "2019-07-05T12:27:07Z"
}