0004-opengis-add-query-system-for-features-20535.patch
passerelle/apps/opengis/forms.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from xml.etree import ElementTree as ET |
|
18 | ||
19 |
from django import forms |
|
20 |
from django.utils.translation import ugettext_lazy as _ |
|
21 | ||
22 |
from . import models |
|
23 | ||
24 | ||
25 |
class QueryForm(forms.ModelForm): |
|
26 |
class Meta: |
|
27 |
model = models.Query |
|
28 |
fields = '__all__' |
|
29 |
exclude = ['resource'] |
|
30 | ||
31 |
def clean_filter_expression(self): |
|
32 |
filter_expression = self.cleaned_data['filter_expression'] |
|
33 |
try: |
|
34 |
ET.fromstring(filter_expression) |
|
35 |
except ET.ParseError: |
|
36 |
raise forms.ValidationError(_('Filter is not valid XML.')) |
|
37 |
return filter_expression |
passerelle/apps/opengis/migrations/0007_auto_20200324_1019.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-03-24 09:19 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 |
import django.db.models.deletion |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
('opengis', '0006_auto_20181118_0807'), |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.CreateModel( |
|
17 |
name='Query', |
|
18 |
fields=[ |
|
19 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
20 |
('name', models.CharField(max_length=128, verbose_name='Name')), |
|
21 |
('slug', models.SlugField(max_length=128, verbose_name='Slug')), |
|
22 |
('description', models.TextField(blank=True, verbose_name='Description')), |
|
23 |
('typename', models.CharField(blank=True, max_length=256, verbose_name='Feature type')), |
|
24 |
('filter_expression', models.CharField(blank=True, max_length=4096, verbose_name='XML filter')), |
|
25 |
], |
|
26 |
options={ |
|
27 |
'ordering': ['name'], |
|
28 |
}, |
|
29 |
), |
|
30 |
migrations.AlterField( |
|
31 |
model_name='opengis', |
|
32 |
name='projection', |
|
33 |
field=models.CharField(choices=[('EPSG:2154', 'EPSG:2154 (Lambert-93)'), ('EPSG:3857', 'EPSG:3857 (WGS 84 / Pseudo-Mercator)'), ('EPSG:3945', 'EPSG:3945 (CC45)'), ('EPSG:4326', 'EPSG:4326 (WGS 84)')], default='EPSG:3857', max_length=16, verbose_name='GIS projection'), |
|
34 |
), |
|
35 |
migrations.AddField( |
|
36 |
model_name='query', |
|
37 |
name='resource', |
|
38 |
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opengis.OpenGIS', verbose_name='Resource'), |
|
39 |
), |
|
40 |
migrations.AlterUniqueTogether( |
|
41 |
name='query', |
|
42 |
unique_together=set([('resource', 'name'), ('resource', 'slug')]), |
|
43 |
), |
|
44 |
] |
passerelle/apps/opengis/models.py | ||
---|---|---|
24 | 24 |
from django.core.cache import cache |
25 | 25 |
from django.db import models |
26 | 26 |
from django.http import HttpResponse |
27 |
from django.shortcuts import get_object_or_404 |
|
28 |
from django.urls import reverse |
|
27 | 29 |
from django.utils.six.moves.html_parser import HTMLParser |
28 | 30 |
from django.utils.text import slugify |
29 | 31 |
from django.utils.translation import ugettext_lazy as _ |
30 | 32 | |
31 |
from passerelle.base.models import BaseResource |
|
33 |
from passerelle.base.models import BaseResource, BaseQuery
|
|
32 | 34 |
from passerelle.utils.api import endpoint |
33 | 35 |
from passerelle.utils.jsonresponse import APIError |
34 | 36 | |
... | ... | |
144 | 146 |
'case-insensitive': { |
145 | 147 |
'description': _('Enables case-insensitive search'), |
146 | 148 |
'example_value': 'true' |
149 |
}, |
|
150 |
'xml_filter': { |
|
151 |
'description': _('Filter applied to the query'), |
|
152 |
'example_value': '<Filter><PropertyIsEqualTo><PropertyName>typeparking' |
|
153 |
'</PropertyName></PropertyIsEqualTo></Filter>' |
|
147 | 154 |
} |
148 | 155 |
}) |
149 | 156 |
def features(self, request, type_names, property_name, cql_filter=None, |
150 |
filter_property_name=None, q=None, **kwargs): |
|
157 |
filter_property_name=None, q=None, xml_filter=None, **kwargs):
|
|
151 | 158 |
params = { |
152 | 159 |
'VERSION': self.get_wfs_service_version(), |
153 | 160 |
'SERVICE': 'WFS', |
154 | 161 |
'REQUEST': 'GetFeature', |
155 | 162 |
self.get_typename_label(): type_names, |
156 |
'PROPERTYNAME': property_name, |
|
157 | 163 |
'OUTPUTFORMAT': 'json', |
158 | 164 |
} |
165 |
if property_name: |
|
166 |
params['PROPERTYNAME'] = property_name |
|
159 | 167 |
if cql_filter: |
160 | 168 |
params.update({'CQL_FILTER': cql_filter}) |
161 | 169 |
if filter_property_name and q: |
... | ... | |
164 | 172 |
else: |
165 | 173 |
operator = 'LIKE' |
166 | 174 |
params['CQL_FILTER'] += ' AND %s %s \'%%%s%%\'' % (filter_property_name, operator, q) |
175 |
if xml_filter: |
|
176 |
params['FILTER'] = xml_filter |
|
167 | 177 |
response = self.requests.get(self.wfs_service_url, params=params) |
168 | 178 |
data = [] |
169 | 179 |
try: |
170 |
response.json() |
|
180 |
response = response.json()
|
|
171 | 181 |
except ValueError: |
172 | 182 |
self.handle_opengis_error(response) |
173 | 183 |
# if handle_opengis_error did not raise an error, we raise a generic one |
174 | 184 |
raise APIError(u'OpenGIS Error: unparsable error', |
175 | 185 |
data={'content': repr(response.content[:1024])}) |
176 |
for feature in response.json()['features']:
|
|
186 |
for feature in response['features']: |
|
177 | 187 |
feature['text'] = feature['properties'].get(property_name) |
178 | 188 |
data.append(feature) |
179 | 189 |
return {'data': data} |
... | ... | |
343 | 353 |
break |
344 | 354 |
return result |
345 | 355 |
raise APIError('Unable to geocode') |
356 | ||
357 |
@endpoint(name='q', |
|
358 |
description=_('Query'), |
|
359 |
pattern=r'^(?P<query_slug>[\w:_-]+)/$', |
|
360 |
perm='can_access', |
|
361 |
show=False) |
|
362 |
def q(self, request, query_slug): |
|
363 |
query = get_object_or_404(Query, resource=self, slug=query_slug) |
|
364 |
return query.q(request) |
|
365 | ||
366 |
def export_json(self): |
|
367 |
d = super(OpenGIS, self).export_json() |
|
368 |
d['queries'] = [query.export_json() for query in self.query_set.all()] |
|
369 |
return d |
|
370 | ||
371 |
@classmethod |
|
372 |
def import_json_real(cls, overwrite, instance, d, **kwargs): |
|
373 |
queries = d.pop('queries', []) |
|
374 |
instance = super(OpenGIS, cls).import_json_real(overwrite, instance, d, **kwargs) |
|
375 |
new = [] |
|
376 |
if instance and overwrite: |
|
377 |
Query.objects.filter(resource=instance).delete() |
|
378 |
for query in queries: |
|
379 |
query['resource'] = instance |
|
380 |
q = Query.import_json(query) |
|
381 |
new.append(q) |
|
382 |
Query.objects.bulk_create(new) |
|
383 |
return instance |
|
384 | ||
385 |
def create_query_url(self): |
|
386 |
return reverse('opengis-query-new', kwargs={'slug': self.slug}) |
|
387 | ||
388 | ||
389 |
class Query(BaseQuery): |
|
390 |
resource = models.ForeignKey( |
|
391 |
to=OpenGIS, |
|
392 |
on_delete=models.CASCADE, |
|
393 |
verbose_name=_('Resource')) |
|
394 | ||
395 |
typename = models.CharField( |
|
396 |
verbose_name=_('Feature type'), |
|
397 |
max_length=256, |
|
398 |
blank=True) |
|
399 |
filter_expression = models.CharField( |
|
400 |
verbose_name=_('XML filter'), |
|
401 |
max_length=4096, |
|
402 |
blank=True) |
|
403 | ||
404 |
delete_view = 'opengis-query-delete' |
|
405 |
edit_view = 'opengis-query-edit' |
|
406 | ||
407 |
def __init__(self, *args, **kwargs): |
|
408 |
super(Query, self).__init__(*args, **kwargs) |
|
409 |
self.endpoint = self.resource.features |
|
410 |
self.called_from = self.resource.q |
|
411 | ||
412 |
def as_endpoint(self): |
|
413 |
endpoint = super(Query, self).as_endpoint() |
|
414 |
# include no original parameters |
|
415 |
endpoint.exclude_params = list(self.endpoint.endpoint_info.parameters) |
|
416 |
return endpoint |
|
417 | ||
418 |
def q(self, request): |
|
419 |
return self.endpoint(request, self.typename, property_name=None, |
|
420 |
xml_filter=self.filter_expression) |
passerelle/apps/opengis/templates/opengis/query_confirm_delete.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/resource_child_confirm_delete.html" %} |
passerelle/apps/opengis/templates/opengis/query_form.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/resource_child_form.html" %} |
passerelle/apps/opengis/urls.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.conf.urls import url |
|
18 | ||
19 |
from . import views |
|
20 | ||
21 |
management_urlpatterns = [ |
|
22 |
url(r'^(?P<slug>[\w,-]+)/query/new/$', |
|
23 |
views.QueryNew.as_view(), name='opengis-query-new'), |
|
24 |
url(r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/$', |
|
25 |
views.QueryEdit.as_view(), name='opengis-query-edit'), |
|
26 |
url(r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/delete/$', |
|
27 |
views.QueryDelete.as_view(), name='opengis-query-delete'), |
|
28 |
] |
passerelle/apps/opengis/views.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.views.generic import UpdateView, CreateView, DeleteView |
|
18 | ||
19 |
from passerelle.base.mixins import ResourceChildViewMixin |
|
20 | ||
21 |
from . import models |
|
22 |
from .forms import QueryForm |
|
23 | ||
24 | ||
25 |
class QueryNew(ResourceChildViewMixin, CreateView): |
|
26 |
model = models.Query |
|
27 |
form_class = QueryForm |
|
28 | ||
29 |
def form_valid(self, form): |
|
30 |
form.instance.resource = self.resource |
|
31 |
return super(QueryNew, self).form_valid(form) |
|
32 | ||
33 |
def get_changed_url(self): |
|
34 |
return self.object.get_absolute_url() |
|
35 | ||
36 | ||
37 |
class QueryEdit(ResourceChildViewMixin, UpdateView): |
|
38 |
model = models.Query |
|
39 |
form_class = QueryForm |
|
40 | ||
41 | ||
42 |
class QueryDelete(ResourceChildViewMixin, DeleteView): |
|
43 |
model = models.Query |
|
0 |
- |