diff --git a/passerelle/apps/opengis/models.py b/passerelle/apps/opengis/models.py index 9beb0e3e..f5ceb8f3 100644 --- a/passerelle/apps/opengis/models.py +++ b/passerelle/apps/opengis/models.py @@ -73,18 +73,21 @@ class OpenGIS(BaseResource): class Meta: verbose_name = _('OpenGIS') - def get_service_version(self, service_type, service_url, renew=False): + def get_capabilities(self, service_type, service_url): if not service_url: raise APIError('no %s URL declared' % service_type) + return self.requests.get( + service_url, + params={'request': 'GetCapabilities', 'service': service_type.upper()}, + ) + + def get_service_version(self, service_type, service_url, renew=False): cache_key = 'opengis-%s-%s-version' % (service_type, self.id) if not renew: service_version = cache.get(cache_key) if service_version: return service_version - response = self.requests.get( - service_url, - params={'request': 'GetCapabilities', 'service': service_type.upper()}, - ) + response = self.get_capabilities(service_type, service_url) element = ET.fromstring(response.content) service_version = element.attrib.get('version') # cache version number for an hour @@ -97,6 +100,23 @@ class OpenGIS(BaseResource): def get_wfs_service_version(self, renew=False): return self.get_service_version('wfs', self.wfs_service_url, renew=renew) + def get_output_format(self): + cache_key = 'opengis-%s-output-format' % self.id + output_format = cache.get(cache_key) + if output_format: + return output_format + response = self.get_capabilities('wfs', self.wfs_service_url) + element = ET.fromstring(response.content) + ns = {'ows': 'http://www.opengis.net/ows/1.1'} + formats = element.findall('.//ows:Operation[@name="GetFeature"]/' + 'ows:Parameter[@name="outputFormat"]/' + 'ows:AllowedValues/ows:Value', ns) + for output_format in formats: + if 'json' in output_format.text.lower(): + cache.set(cache_key, output_format.text, 3600) + return output_format.text + raise APIError('WFS server doesn\'t support json output format for GetFeature request') + def get_typename_label(self): version_str = self.get_wfs_service_version() version_tuple = tuple(int(x) for x in version_str.split('.')) @@ -123,6 +143,19 @@ class OpenGIS(BaseResource): }) response.raise_for_status() + def build_get_features_params(self, typename=None, property_name=None, cql_filter=None): + params = { + 'version': self.get_wfs_service_version(), + 'service': 'WFS', + 'request': 'GetFeature', + self.get_typename_label(): typename, + 'propertyName': property_name, + 'outputFormat': self.get_output_format(), + } + if cql_filter: + params['cql_filter'] = cql_filter + return params + @endpoint(perm='can_access', description='Get features', parameters={ 'type_names': { @@ -152,22 +185,14 @@ class OpenGIS(BaseResource): }) def features(self, request, type_names, property_name, cql_filter=None, filter_property_name=None, q=None, **kwargs): - params = { - 'VERSION': self.get_wfs_service_version(), - 'SERVICE': 'WFS', - 'REQUEST': 'GetFeature', - self.get_typename_label(): type_names, - 'PROPERTYNAME': property_name, - 'OUTPUTFORMAT': 'json', - } if cql_filter: - params.update({'CQL_FILTER': cql_filter}) if filter_property_name and q: if 'case-insensitive' in kwargs: operator = 'ILIKE' else: operator = 'LIKE' - params['CQL_FILTER'] += ' AND %s %s \'%%%s%%\'' % (filter_property_name, operator, q) + cql_filter += ' AND %s %s \'%%%s%%\'' % (filter_property_name, operator, q) + params = self.build_get_features_params(type_names, property_name, cql_filter) response = self.requests.get(self.wfs_service_url, params=params) data = [] try: @@ -297,14 +322,7 @@ class OpenGIS(BaseResource): lon, lat = self.convert_coordinates(lon, lat) cql_filter = 'DWITHIN(the_geom,Point(%.6f %.6f),%s,meters)' % (lon, lat, self.search_radius) - params = { - 'VERSION': self.get_wfs_service_version(), - 'SERVICE': 'WFS', - 'REQUEST': 'GetFeature', - self.get_typename_label(): self.query_layer, - 'OUTPUTFORMAT': 'json', - 'CQL_FILTER': cql_filter - } + params = self.build_get_features_params(typename=self.query_layer, cql_filter=cql_filter) response = self.requests.get(self.wfs_service_url, params=params) if not response.ok: raise APIError('Webservice returned status code %s' % response.status_code) diff --git a/tests/test_opengis.py b/tests/test_opengis.py index 07d3fdfb..fa5e2de7 100644 --- a/tests/test_opengis.py +++ b/tests/test_opengis.py @@ -39,6 +39,16 @@ FAKE_SERVICE_CAPABILITIES = ''' WFS2.0.0 + + + + + application/gml+xml; version=3.2 + application/json; subtype=geojson + + + + ''' FAKE_SERVICE_CAPABILITIES_V1_0_0 = ''' @@ -50,6 +60,16 @@ FAKE_SERVICE_CAPABILITIES_V1_0_0 = ''' WFS1.0.0 + + + + + application/gml+xml; version=3.2 + application/json; subtype=geojson + + + + ''' FAKE_FEATURES_JSON = ''' @@ -326,12 +346,12 @@ def test_get_feature(mocked_get, app, connector): assert endpoint == '/opengis/test/features' mocked_get.side_effect = geoserver_responses resp = app.get(endpoint, params={'type_names': 'ref_metro_limites_communales', 'property_name': 'nom'}) - assert mocked_get.call_args[1]['params']['REQUEST'] == 'GetFeature' - assert mocked_get.call_args[1]['params']['PROPERTYNAME'] == 'nom' + assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' + assert mocked_get.call_args[1]['params']['propertyName'] == 'nom' assert mocked_get.call_args[1]['params']['TYPENAMES'] == 'ref_metro_limites_communales' - assert mocked_get.call_args[1]['params']['OUTPUTFORMAT'] == 'json' - assert mocked_get.call_args[1]['params']['SERVICE'] == 'WFS' - assert mocked_get.call_args[1]['params']['VERSION'] == connector.get_wfs_service_version() + assert 'json' in mocked_get.call_args[1]['params']['outputFormat'] + assert mocked_get.call_args[1]['params']['service'] == 'WFS' + assert mocked_get.call_args[1]['params']['version'] == connector.get_wfs_service_version() assert len(resp.json['data']) == 7 for item in resp.json['data']: assert 'text' in item @@ -347,7 +367,7 @@ def test_get_filtered_feature(mocked_get, app, connector): 'property_name': 'nom', 'cql_filter': 'nom=\'Fontaine\'' }) - assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'nom=\'Fontaine\'' + assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\'' @mock.patch('passerelle.utils.Request.get') @@ -358,16 +378,16 @@ def test_get_filtered_by_property_feature(mocked_get, app, connector): 'property_name': 'nom', 'cql_filter': 'nom=\'Fontaine\'', 'filter_property_name': 'nom'} app.get(endpoint, params=params) - assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'nom=\'Fontaine\'' + assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\'' params['q'] = 'bens' app.get(endpoint, params=params) - assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'nom=\'Fontaine\' AND nom LIKE \'%bens%\'' + assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\' AND nom LIKE \'%bens%\'' params['case-insensitive'] = True app.get(endpoint, params=params) - assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'nom=\'Fontaine\' AND nom ILIKE \'%bens%\'' + assert mocked_get.call_args[1]['params']['cql_filter'] == 'nom=\'Fontaine\' AND nom ILIKE \'%bens%\'' params.pop('cql_filter') app.get(endpoint, params=params) - assert 'CQL_FILTER' not in mocked_get.call_args[1]['params'] + assert 'cql_filter' not in mocked_get.call_args[1]['params'] @mock.patch('passerelle.utils.Request.get') @@ -379,12 +399,12 @@ def test_get_feature_error(mocked_get, app, connector): 'type_names': 'ref_metro_limites_communales', 'property_name': 'nom' }) - assert mocked_get.call_args[1]['params']['REQUEST'] == 'GetFeature' - assert mocked_get.call_args[1]['params']['PROPERTYNAME'] == 'nom' + assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' + assert mocked_get.call_args[1]['params']['propertyName'] == 'nom' assert mocked_get.call_args[1]['params']['TYPENAMES'] == 'ref_metro_limites_communales' - assert mocked_get.call_args[1]['params']['OUTPUTFORMAT'] == 'json' - assert mocked_get.call_args[1]['params']['SERVICE'] == 'WFS' - assert mocked_get.call_args[1]['params']['VERSION'] == connector.get_wfs_service_version() + assert 'json' in mocked_get.call_args[1]['params']['outputFormat'] + assert mocked_get.call_args[1]['params']['service'] == 'WFS' + assert mocked_get.call_args[1]['params']['version'] == connector.get_wfs_service_version() result = resp.json assert result['err'] == 1 assert result['err_desc'] == 'OpenGIS Error: NoApplicableCode' @@ -400,12 +420,12 @@ def test_get_feature_error2(mocked_get, app, connector): 'type_names': 'ref_metro_limites_communales', 'property_name': 'nom' }) - assert mocked_get.call_args[1]['params']['REQUEST'] == 'GetFeature' - assert mocked_get.call_args[1]['params']['PROPERTYNAME'] == 'nom' + assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' + assert mocked_get.call_args[1]['params']['propertyName'] == 'nom' assert mocked_get.call_args[1]['params']['TYPENAMES'] == 'ref_metro_limites_communales' - assert mocked_get.call_args[1]['params']['OUTPUTFORMAT'] == 'json' - assert mocked_get.call_args[1]['params']['SERVICE'] == 'WFS' - assert mocked_get.call_args[1]['params']['VERSION'] == connector.get_wfs_service_version() + assert 'json' in mocked_get.call_args[1]['params']['outputFormat'] + assert mocked_get.call_args[1]['params']['service'] == 'WFS' + assert mocked_get.call_args[1]['params']['version'] == connector.get_wfs_service_version() result = resp.json assert result['err'] == 1 assert result['err_desc'] == 'OpenGIS Error: unparsable error' @@ -421,8 +441,8 @@ def test_typename_parameter_upgrade(mocked_get, server_responses, version, typen assert endpoint == '/opengis/test/features' mocked_get.side_effect = server_responses resp = app.get(endpoint, params={'type_names': '...', 'property_name': '...'}) - assert mocked_get.call_args[1]['params']['REQUEST'] == 'GetFeature' - assert mocked_get.call_args[1]['params']['VERSION'] == version + assert mocked_get.call_args[1]['params']['request'] == 'GetFeature' + assert mocked_get.call_args[1]['params']['version'] == version assert typename_label in mocked_get.call_args[1]['params'].keys() @@ -445,7 +465,7 @@ def test_reverse_geocoding(mocked_get, app, connector): 'lat': '45.1893469606986', 'lon': '5.72462060798' }) - assert (mocked_get.call_args[1]['params']['CQL_FILTER'] + assert (mocked_get.call_args[1]['params']['cql_filter'] == 'DWITHIN(the_geom,Point(1914061.486036 4224640.457791),45,meters)') assert resp.json['lon'] == '5.724077' assert resp.json['lat'] == '45.189397' @@ -464,7 +484,7 @@ def test_reverse_geocoding(mocked_get, app, connector): 'lat': '45.183784', 'lon': '5.714885' }) - assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(5.714885 45.183784),10,meters)' + assert mocked_get.call_args[1]['params']['cql_filter'] == 'DWITHIN(the_geom,Point(5.714885 45.183784),10,meters)' assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'Unable to geocode'