Skip to content

Commit e40e163

Browse files
committedJan 22, 2021
Fix Server WMS unstable feature IDs
Fixes #41124 Fix for JSON and GML only: the other formats were already ok.
1 parent 4dbb40e commit e40e163

File tree

4 files changed

+141
-3
lines changed

4 files changed

+141
-3
lines changed
 

‎src/server/services/wms/qgswmsrenderer.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,6 +2319,8 @@ namespace QgsWms
23192319
if ( featuresNode.isEmpty() )
23202320
continue;
23212321

2322+
QMap<QgsFeatureId, QString> fidMap;
2323+
23222324
for ( int j = 0; j < featuresNode.size(); ++j )
23232325
{
23242326
const QDomElement featureNode = featuresNode.at( j ).toElement();
@@ -2336,6 +2338,8 @@ namespace QgsWms
23362338
vl->getFeatures( request ).nextFeature( feature );
23372339
}
23382340

2341+
fidMap.insert( feature.id(), fid );
2342+
23392343
QString wkt;
23402344
if ( withGeometry )
23412345
{
@@ -2381,7 +2385,7 @@ namespace QgsWms
23812385

23822386
for ( const auto &feature : qgis::as_const( features ) )
23832387
{
2384-
const QString id = QStringLiteral( "%1.%2" ).arg( layerName ).arg( feature.id() );
2388+
const QString id = QStringLiteral( "%1.%2" ).arg( layerName ).arg( fidMap.value( feature.id() ) );
23852389
json["features"].push_back( exporter.exportFeatureToJsonObject( feature, QVariantMap(), id ) );
23862390
}
23872391
}
@@ -2432,7 +2436,13 @@ namespace QgsWms
24322436
{
24332437
//qgs:%TYPENAME%
24342438
QDomElement typeNameElement = doc.createElement( "qgs:" + typeName /*qgs:%TYPENAME%*/ );
2435-
typeNameElement.setAttribute( QStringLiteral( "fid" ), typeName + "." + QString::number( feat->id() ) );
2439+
QString fid;
2440+
if ( layer && layer->dataProvider() )
2441+
fid = QgsServerFeatureId::getServerFid( *feat, layer->dataProvider()->pkAttributeIndexes() );
2442+
else
2443+
fid = feat->id();
2444+
2445+
typeNameElement.setAttribute( QStringLiteral( "fid" ), QStringLiteral( "%1.%2" ).arg( typeName, fid ) );
24362446

24372447
QgsCoordinateTransform transform;
24382448
if ( layer && layer->crs() != crs )
@@ -2460,7 +2470,7 @@ namespace QgsWms
24602470
{
24612471
try
24622472
{
2463-
QgsRectangle transformedBox = transform.transformBoundingBox( box );
2473+
const QgsRectangle transformedBox = transform.transformBoundingBox( box );
24642474
box = transformedBox;
24652475
}
24662476
catch ( QgsCsException &e )

‎tests/src/python/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ if (ENABLE_PGTEST)
380380
ADD_PYTHON_TEST(PyQgsDatabaseSchemaComboBox test_qgsdatabaseschemacombobox.py)
381381
ADD_PYTHON_TEST(PyQgsDatabaseTableComboBox test_qgsdatabasetablecombobox.py)
382382
ADD_PYTHON_TEST(PyQgsProviderConnectionPostgres test_qgsproviderconnection_postgres.py)
383+
if (WITH_SERVER)
384+
ADD_PYTHON_TEST(PyQgsServerWMSGetFeatureInfoPG test_qgsserver_wms_getfeatureinfo_postgres.py)
385+
endif()
383386
endif()
384387

385388
if (ENABLE_MSSQLTEST)

‎tests/src/python/test_qgsserver.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def assertXMLEqual(self, response, expected, msg='', raw=False):
108108

109109
@classmethod
110110
def setUpClass(cls):
111+
111112
cls.app = QgsApplication([], False)
113+
cls.app.initQgis()
112114

113115
@classmethod
114116
def tearDownClass(cls):
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsServer GetFeatureInfo WMS with PG.
3+
4+
From build dir, run: ctest -R PyQgsServerWMSGetFeatureInfoPG -V
5+
6+
7+
.. note:: This program is free software; you can redistribute it and/or modify
8+
it under the terms of the GNU General Public License as published by
9+
the Free Software Foundation; either version 2 of the License, or
10+
(at your option) any later version.
11+
12+
"""
13+
__author__ = 'Alessandro Pasotti'
14+
__date__ = '22/01/2021'
15+
__copyright__ = 'Copyright 2021, The QGIS Project'
16+
# This will get replaced with a git SHA1 when you do a git archive
17+
__revision__ = '$Format:%H$'
18+
19+
import os
20+
import tempfile
21+
22+
# Needed on Qt 5 so that the serialization of XML is consistent among all
23+
# executions
24+
os.environ['QT_HASH_SEED'] = '1'
25+
26+
os.environ['QGIS_CUSTOM_CONFIG_PATH'] = tempfile.mkdtemp('', 'QGIS-PythonTestConfigPath')
27+
28+
import re
29+
import urllib.request
30+
import urllib.parse
31+
import urllib.error
32+
33+
import xml.etree.ElementTree as ET
34+
import json
35+
36+
from qgis.testing import unittest, start_app
37+
from qgis.PyQt.QtCore import QSize
38+
39+
import osgeo.gdal # NOQA
40+
41+
from test_qgsserver_wms import TestQgsServerWMSTestBase
42+
from qgis.core import QgsProject, QgsVectorLayer, QgsFeatureRequest, QgsExpression, QgsProviderRegistry
43+
from qgis.server import QgsBufferServerRequest, QgsBufferServerResponse
44+
45+
46+
class TestQgsServerWMSGetFeatureInfoPG(TestQgsServerWMSTestBase):
47+
"""QGIS Server WMS Tests for GetFeatureInfo request"""
48+
49+
@classmethod
50+
def setUpClass(cls):
51+
52+
super().setUpClass()
53+
54+
cls.dbconn = 'service=qgis_test'
55+
if 'QGIS_PGTEST_DB' in os.environ:
56+
cls.dbconn = os.environ['QGIS_PGTEST_DB']
57+
58+
# Test layer
59+
md = QgsProviderRegistry.instance().providerMetadata('postgres')
60+
uri = cls.dbconn + 'dbname=qgis_test sslmode=disable '
61+
conn = md.createConnection(uri, {})
62+
conn.executeSql('DROP TABLE IF EXISTS "qgis_test"."someDataLong" CASCADE')
63+
conn.executeSql('SELECT * INTO "qgis_test"."someDataLong" FROM "qgis_test"."someData"')
64+
conn.executeSql('ALTER TABLE "qgis_test"."someDataLong" ALTER COLUMN "pk" TYPE bigint')
65+
conn.executeSql('ALTER TABLE "qgis_test"."someDataLong" ALTER COLUMN "pk" TYPE bigint')
66+
conn.executeSql('ALTER TABLE "qgis_test"."someDataLong" ALTER COLUMN "pk" SET NOT NULL')
67+
conn.executeSql('CREATE UNIQUE INDEX someDataLongIdx ON "qgis_test"."someDataLong" ("pk")')
68+
69+
cls.vlconn = cls.dbconn + ' sslmode=disable key=\'pk\' checkPrimaryKeyUnicity=0 srid=4326 type=POINT table="qgis_test"."someDataLong" (geom) sql='
70+
71+
def _baseFilterTest(self, info_format):
72+
73+
vl = QgsVectorLayer(self.vlconn, 'someData', 'postgres')
74+
self.assertTrue(vl.isValid())
75+
76+
# Pre-filtered
77+
vl2 = QgsVectorLayer(self.vlconn, 'someData', 'postgres')
78+
self.assertTrue(vl2.isValid())
79+
[f for f in vl2.getFeatures(QgsFeatureRequest(QgsExpression('pk > 2')))]
80+
81+
base_features_url = ('http://qgis/?SERVICE=WMS&REQUEST=GetFeatureInfo&' +
82+
'LAYERS=someData&STYLES=&' +
83+
r'INFO_FORMAT={}&' +
84+
'SRS=EPSG%3A4326&' +
85+
'QUERY_LAYERS=someData&X=-1&Y=-1&' +
86+
'FEATURE_COUNT=100&'
87+
'FILTER=someData')
88+
89+
two_feature_url = base_features_url + urllib.parse.quote(':"pk" = 2')
90+
91+
p = QgsProject()
92+
p.addMapLayers([vl])
93+
94+
url = two_feature_url.format(urllib.parse.quote(info_format))
95+
96+
req = QgsBufferServerRequest(url)
97+
res = QgsBufferServerResponse()
98+
self.server.handleRequest(req, res, p)
99+
reference_body = bytes(res.body()).decode('utf8')
100+
101+
# Pre-filter
102+
p = QgsProject()
103+
p.addMapLayers([vl2])
104+
105+
req = QgsBufferServerRequest(url)
106+
res = QgsBufferServerResponse()
107+
self.server.handleRequest(req, res, p)
108+
two_feature_body = bytes(res.body()).decode('utf8')
109+
110+
self.assertEqual(reference_body, two_feature_body, info_format)
111+
112+
def testGetFeatureInfoFilterPg(self):
113+
"""Test issue GH #41124"""
114+
115+
self._baseFilterTest('text/plain')
116+
self._baseFilterTest('text/html')
117+
self._baseFilterTest('text/xml')
118+
self._baseFilterTest('application/json')
119+
self._baseFilterTest('application/vnd.ogc.gml')
120+
121+
122+
if __name__ == '__main__':
123+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.