opengis: support namespaces in feature_info endpoint (#79982)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
4f136ee898
commit
5b5008eb2f
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.2.18 on 2023-07-25 09:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import passerelle.apps.opengis.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('opengis', '0015_computed_properties'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opengis',
|
||||
name='feature_info_namespaces',
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text='XML namespaces to consider for feature_info (one per line, format "{http://www.foo.bar/baz}")',
|
||||
null=True,
|
||||
validators=[passerelle.apps.opengis.models.validate_feature_info_namespaces],
|
||||
verbose_name='Feature info namespaces',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -20,6 +20,7 @@ import xml.etree.ElementTree as ET
|
|||
|
||||
import pyproj
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import JSONField, Q
|
||||
from django.http import HttpResponse
|
||||
|
@ -36,19 +37,41 @@ from passerelle.utils.jsonresponse import APIError
|
|||
from passerelle.utils.templates import validate_template
|
||||
|
||||
|
||||
def build_dict_from_xml(elem):
|
||||
def build_dict_from_xml(elem, namespaces=None):
|
||||
def match_namespaces(tag, namespaces):
|
||||
for ns in namespaces or []:
|
||||
if tag.startswith(ns):
|
||||
return True
|
||||
return False
|
||||
|
||||
d = {}
|
||||
for child in elem.find('.'):
|
||||
if child.tag.startswith('{'):
|
||||
continue
|
||||
attribute_name = slugify(child.tag).replace('-', '_')
|
||||
if not match_namespaces(child.tag, namespaces):
|
||||
d.update(build_dict_from_xml(child, namespaces))
|
||||
continue
|
||||
attribute_name = slugify(child.tag.split('}')[1]).replace('-', '_')
|
||||
else:
|
||||
attribute_name = slugify(child.tag).replace('-', '_')
|
||||
if child.text and child.text.strip():
|
||||
d[attribute_name] = child.text.strip()
|
||||
else:
|
||||
d[attribute_name] = build_dict_from_xml(child)
|
||||
d[attribute_name] = build_dict_from_xml(child, namespaces)
|
||||
return d
|
||||
|
||||
|
||||
def validate_feature_info_namespaces(value):
|
||||
if value:
|
||||
for ns in value.splitlines():
|
||||
ns = ns.strip()
|
||||
if ns:
|
||||
if ns.startswith('#'):
|
||||
continue
|
||||
if ns.startswith('{') and ns.endswith('}'):
|
||||
continue
|
||||
raise ValidationError(_('Accepetd format : {http://www.foo.bar/baz}'))
|
||||
|
||||
|
||||
PROJECTIONS = (
|
||||
('EPSG:2154', _('EPSG:2154 (Lambert-93)')),
|
||||
('EPSG:3857', _('EPSG:3857 (WGS 84 / Pseudo-Mercator)')),
|
||||
|
@ -71,6 +94,16 @@ class OpenGIS(BaseResource):
|
|||
_('GIS projection'), choices=PROJECTIONS, default='EPSG:3857', max_length=16
|
||||
)
|
||||
search_radius = models.IntegerField(_('Radius for point search'), default=5)
|
||||
feature_info_namespaces = models.TextField(
|
||||
_('Feature info namespaces'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_(
|
||||
'XML namespaces to consider for feature_info (one per line, format "{http://www.foo.bar/baz}")'
|
||||
),
|
||||
validators=[validate_feature_info_namespaces],
|
||||
)
|
||||
|
||||
attributes_mapping = (
|
||||
('road', ('road', 'road_name', 'street', 'street_name', 'voie', 'nom_voie', 'rue')),
|
||||
('city', ('city', 'city_name', 'town', 'town_name', 'commune', 'nom_commune', 'ville', 'nom_ville')),
|
||||
|
@ -82,6 +115,18 @@ class OpenGIS(BaseResource):
|
|||
class Meta:
|
||||
verbose_name = _('OpenGIS')
|
||||
|
||||
def get_feature_info_namespaces(self):
|
||||
if self.feature_info_namespaces is None:
|
||||
return []
|
||||
namespaces = []
|
||||
for ns in self.feature_info_namespaces.splitlines():
|
||||
ns = ns.strip()
|
||||
if not ns or ns.startswith('#'):
|
||||
continue
|
||||
if ns:
|
||||
namespaces.append(ns)
|
||||
return namespaces
|
||||
|
||||
def get_capabilities(self, service_type, service_url):
|
||||
if not service_url:
|
||||
raise APIError('no %s URL declared' % service_type)
|
||||
|
@ -309,7 +354,7 @@ class OpenGIS(BaseResource):
|
|||
}
|
||||
response = self.requests.get(self.wms_service_url, params=params)
|
||||
element = ET.fromstring(response.content)
|
||||
return {'err': 0, 'data': build_dict_from_xml(element)}
|
||||
return {'err': 0, 'data': build_dict_from_xml(element, self.get_feature_info_namespaces())}
|
||||
|
||||
# https://carton.entrouvert.org/hydda-tiles/16/33650/23378.pn
|
||||
@endpoint(
|
||||
|
|
|
@ -37,6 +37,84 @@ FAKE_FEATURE_INFO = '''<?xml version="1.0" encoding="UTF-8"?>
|
|||
</cad_cadastre.cadparcelle_layer>
|
||||
</msGMLOutput>'''
|
||||
|
||||
|
||||
FAKE_FEATURE_INFO_BIS = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<msGMLOutput
|
||||
xmlns:gml="http://www.opengis.net/gml"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<eco_ecologie.zfe_layer>
|
||||
<gml:name>Zone à faibles émissions de la Métropole de Lyon - ZFE</gml:name>
|
||||
<eco_ecologie.zfe_feature>
|
||||
<gml:boundedBy>
|
||||
<gml:Box srsName="EPSG:3857">
|
||||
<gml:coordinates>531238.257237,5735107.676599 546464.284851,5751147.206607</gml:coordinates>
|
||||
</gml:Box>
|
||||
</gml:boundedBy>
|
||||
<gid>200046977-ZFE-001</gid>
|
||||
<date_debut>2020-01-01</date_debut>
|
||||
<date_fin></date_fin>
|
||||
<vp_critair>V5</vp_critair>
|
||||
<vp_horaires>24/7</vp_horaires>
|
||||
<vul_critair>V3</vul_critair>
|
||||
<vul_horaires>24/7</vul_horaires>
|
||||
<pl_critair>V3</pl_critair>
|
||||
<pl_horaires>24/7</pl_horaires>
|
||||
<autobus_autocars_critair>V5</autobus_autocars_critair>
|
||||
<autobus_autocars_horaires>24/7</autobus_autocars_horaires>
|
||||
<deux_rm_critair>V5</deux_rm_critair>
|
||||
<deux_rm_horaires>24/7</deux_rm_horaires>
|
||||
<url_arrete>https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf</url_arrete>
|
||||
<url_site_information>https://www.grandlyon.com/actions/zfe.html#c20726</url_site_information>
|
||||
<id>200046977-ZFE-001</id>
|
||||
</eco_ecologie.zfe_feature>
|
||||
</eco_ecologie.zfe_layer>
|
||||
</msGMLOutput>'''
|
||||
|
||||
|
||||
FAKE_FEATURE_INFO_WITH_NAMESPACE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs" xmlns="http://www.opengis.net/wfs" xmlns:gml="http://www.opengis.net/gml" xmlns:metropole-de-lyon="http://metropole-de-lyon" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs https://download.data.grandlyon.com/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd http://metropole-de-lyon https://download.data.grandlyon.com/geoserver/wfs?service=WFS&version=1.0.0&request=DescribeFeatureType&typeName=metropole-de-lyon%3Aeco_ecologie.zfe">
|
||||
<gml:boundedBy>
|
||||
<gml:Box srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
|
||||
<gml:coordinates decimal="." cs="," ts=" ">531238.20604672,5735107.61605308 546464.26335844,5751147.23178471</gml:coordinates>
|
||||
</gml:Box>
|
||||
</gml:boundedBy>
|
||||
<gml:featureMember>
|
||||
<metropole-de-lyon:eco_ecologie.zfe fid="eco_ecologie.zfe.200046977-ZFE-001">
|
||||
<gml:boundedBy>
|
||||
<gml:Box srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
|
||||
<gml:coordinates decimal="." cs="," ts=" ">531238.20604672,5735107.61605308 546464.26335844,5751147.23178471</gml:coordinates>
|
||||
</gml:Box>
|
||||
</gml:boundedBy>
|
||||
<metropole-de-lyon:gid>200046977-ZFE-001</metropole-de-lyon:gid>
|
||||
<metropole-de-lyon:date_debut>2020-01-01</metropole-de-lyon:date_debut>
|
||||
<metropole-de-lyon:vp_critair>V5</metropole-de-lyon:vp_critair>
|
||||
<metropole-de-lyon:vp_horaires>24/7</metropole-de-lyon:vp_horaires>
|
||||
<metropole-de-lyon:vul_critair>V3</metropole-de-lyon:vul_critair>
|
||||
<metropole-de-lyon:vul_horaires>24/7</metropole-de-lyon:vul_horaires>
|
||||
<metropole-de-lyon:pl_critair>V3</metropole-de-lyon:pl_critair>
|
||||
<metropole-de-lyon:pl_horaires>24/7</metropole-de-lyon:pl_horaires>
|
||||
<metropole-de-lyon:autobus_autocars_critair>V5</metropole-de-lyon:autobus_autocars_critair>
|
||||
<metropole-de-lyon:autobus_autocars_horaires>24/7</metropole-de-lyon:autobus_autocars_horaires>
|
||||
<metropole-de-lyon:deux_rm_critair>V5</metropole-de-lyon:deux_rm_critair>
|
||||
<metropole-de-lyon:deux_rm_horaires>24/7</metropole-de-lyon:deux_rm_horaires>
|
||||
<metropole-de-lyon:url_arrete>https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf</metropole-de-lyon:url_arrete>
|
||||
<metropole-de-lyon:url_site_information>https://www.grandlyon.com/actions/zfe.html#c20726</metropole-de-lyon:url_site_information>
|
||||
<metropole-de-lyon:the_geom>
|
||||
<gml:Polygon srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
|
||||
<gml:outerBoundaryIs>
|
||||
<gml:LinearRing>
|
||||
<gml:coordinates decimal="." cs="," ts=" ">pleinsde coordonnées</gml:coordinates>
|
||||
</gml:LinearRing>
|
||||
</gml:outerBoundaryIs>
|
||||
</gml:Polygon>
|
||||
</metropole-de-lyon:the_geom>
|
||||
<metropole-de-lyon:id>200046977-ZFE-001</metropole-de-lyon:id>
|
||||
</metropole-de-lyon:eco_ecologie.zfe>
|
||||
</gml:featureMember>
|
||||
</wfs:FeatureCollection>
|
||||
'''
|
||||
|
||||
FAKE_SERVICE_CAPABILITIES = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<wfs:WFS_Capabilities version="2.0.0"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0"
|
||||
|
@ -440,6 +518,82 @@ def test_feature_info(mocked_get, app, connector):
|
|||
assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:4326'
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_feature_info_bis(mocked_get, app, connector):
|
||||
endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug)
|
||||
assert endpoint == '/opengis/test/feature_info'
|
||||
mocked_get.return_value = tests.utils.FakedResponse(content=FAKE_FEATURE_INFO_BIS, status_code=200)
|
||||
resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'})
|
||||
assert (
|
||||
mocked_get.call_args[1]['params']['bbox']
|
||||
== '532556.896735,5747844.261214,532579.160633,5747876.194333'
|
||||
)
|
||||
assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857'
|
||||
assert resp.json['data'] == {
|
||||
'eco_ecologiezfe_layer': {
|
||||
'eco_ecologiezfe_feature': {
|
||||
'autobus_autocars_critair': 'V5',
|
||||
'autobus_autocars_horaires': '24/7',
|
||||
'date_debut': '2020-01-01',
|
||||
'date_fin': {},
|
||||
'deux_rm_critair': 'V5',
|
||||
'deux_rm_horaires': '24/7',
|
||||
'gid': '200046977-ZFE-001',
|
||||
'id': '200046977-ZFE-001',
|
||||
'pl_critair': 'V3',
|
||||
'pl_horaires': '24/7',
|
||||
'url_arrete': 'https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf',
|
||||
'url_site_information': 'https://www.grandlyon.com/actions/zfe.html#c20726',
|
||||
'vp_critair': 'V5',
|
||||
'vp_horaires': '24/7',
|
||||
'vul_critair': 'V3',
|
||||
'vul_horaires': '24/7',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_feature_info_with_namespace(mocked_get, app, connector):
|
||||
connector.feature_info_namespaces = '''
|
||||
{http://metropole-de-lyon}
|
||||
# a comment
|
||||
|
||||
'''
|
||||
connector.save()
|
||||
endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug)
|
||||
assert endpoint == '/opengis/test/feature_info'
|
||||
mocked_get.return_value = tests.utils.FakedResponse(
|
||||
content=FAKE_FEATURE_INFO_WITH_NAMESPACE, status_code=200
|
||||
)
|
||||
resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'})
|
||||
assert (
|
||||
mocked_get.call_args[1]['params']['bbox']
|
||||
== '532556.896735,5747844.261214,532579.160633,5747876.194333'
|
||||
)
|
||||
assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857'
|
||||
assert resp.json['data'] == {
|
||||
'eco_ecologiezfe': {
|
||||
'autobus_autocars_critair': 'V5',
|
||||
'autobus_autocars_horaires': '24/7',
|
||||
'date_debut': '2020-01-01',
|
||||
'deux_rm_critair': 'V5',
|
||||
'deux_rm_horaires': '24/7',
|
||||
'gid': '200046977-ZFE-001',
|
||||
'id': '200046977-ZFE-001',
|
||||
'pl_critair': 'V3',
|
||||
'pl_horaires': '24/7',
|
||||
'the_geom': {},
|
||||
'url_arrete': 'https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf',
|
||||
'url_site_information': 'https://www.grandlyon.com/actions/zfe.html#c20726',
|
||||
'vp_critair': 'V5',
|
||||
'vp_horaires': '24/7',
|
||||
'vul_critair': 'V3',
|
||||
'vul_horaires': '24/7',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
@pytest.mark.parametrize(
|
||||
'lat,lon',
|
||||
|
|
Loading…
Reference in New Issue