Projet

Général

Profil

0003-opengis-add-query-system-for-features-20535.patch

Valentin Deniaud, 08 avril 2020 14:40

Télécharger (14,6 ko)

Voir les différences:

Subject: [PATCH 3/5] opengis: add query system for features (#20535)

 passerelle/apps/opengis/forms.py              | 38 +++++++++
 .../migrations/0007_auto_20200401_1032.py     | 45 ++++++++++
 passerelle/apps/opengis/models.py             | 84 +++++++++++++++++--
 .../opengis/query_confirm_delete.html         |  1 +
 .../opengis/templates/opengis/query_form.html |  1 +
 passerelle/apps/opengis/urls.py               | 28 +++++++
 passerelle/apps/opengis/views.py              | 43 ++++++++++
 7 files changed, 233 insertions(+), 7 deletions(-)
 create mode 100644 passerelle/apps/opengis/forms.py
 create mode 100644 passerelle/apps/opengis/migrations/0007_auto_20200401_1032.py
 create mode 100644 passerelle/apps/opengis/templates/opengis/query_confirm_delete.html
 create mode 100644 passerelle/apps/opengis/templates/opengis/query_form.html
 create mode 100644 passerelle/apps/opengis/urls.py
 create mode 100644 passerelle/apps/opengis/views.py
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
        if filter_expression:
34
            try:
35
                ET.fromstring(filter_expression)
36
            except ET.ParseError:
37
                raise forms.ValidationError(_('Filter is not valid XML.'))
38
        return filter_expression
passerelle/apps/opengis/migrations/0007_auto_20200401_1032.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-04-08 09:54
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(max_length=256, verbose_name='Feature type')),
24
                ('filter_expression', models.TextField(blank=True, verbose_name='XML filter')),
25
            ],
26
            options={
27
                'ordering': ['name'],
28
                'abstract': False,
29
            },
30
        ),
31
        migrations.AlterField(
32
            model_name='opengis',
33
            name='projection',
34
            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'),
35
        ),
36
        migrations.AddField(
37
            model_name='query',
38
            name='resource',
39
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queries', to='opengis.OpenGIS', verbose_name='Resource'),
40
        ),
41
        migrations.AlterUniqueTogether(
42
            name='query',
43
            unique_together=set([('resource', 'name'), ('resource', 'slug')]),
44
        ),
45
    ]
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.conversion import num2deg
34 36
from passerelle.utils.jsonresponse import APIError
......
143 145
                })
144 146
            response.raise_for_status()
145 147

  
146
    def build_get_features_params(self, typename=None, property_name=None, cql_filter=None):
148
    def build_get_features_params(self, typename=None, property_name=None, cql_filter=None,
149
                                  xml_filter=None):
147 150
        params = {
148 151
            'version': self.get_wfs_service_version(),
149 152
            'service': 'WFS',
150 153
            'request': 'GetFeature',
151 154
            self.get_typename_label(): typename,
152
            'propertyName': property_name,
153 155
            'outputFormat': self.get_output_format(),
154 156
        }
157
        if property_name:
158
            params['propertyName'] = property_name
155 159
        if cql_filter:
156 160
            params['cql_filter'] = cql_filter
161
        if xml_filter:
162
            params['filter'] = xml_filter
157 163
        return params
158 164

  
159 165
    @endpoint(perm='can_access', description='Get features',
......
180 186
                  },
181 187
                  'case_insensitive': {
182 188
                      'description': _('Enables case-insensitive search'),
189
                  },
190
                  'xml_filter': {
191
                      'description': _('Filter applied to the query'),
192
                      'example_value': '<Filter><PropertyIsEqualTo><PropertyName>typeparking'
193
                      '</PropertyName></PropertyIsEqualTo></Filter>'
183 194
                  }
184 195
              })
185 196
    def features(self, request, type_names, property_name, cql_filter=None,
186
                 filter_property_name=None, q=None, case_insensitive=False, **kwargs):
197
                 filter_property_name=None, q=None, case_insensitive=False,
198
                 xml_filter=None, **kwargs):
187 199
        if cql_filter:
188 200
            if filter_property_name and q:
189 201
                if 'case-insensitive' in kwargs or case_insensitive:
......
191 203
                else:
192 204
                    operator = 'LIKE'
193 205
                cql_filter += ' AND %s %s \'%%%s%%\'' % (filter_property_name, operator, q)
194
        params = self.build_get_features_params(type_names, property_name, cql_filter)
206
        params = self.build_get_features_params(type_names, property_name, cql_filter, xml_filter)
195 207
        response = self.requests.get(self.wfs_service_url, params=params)
196 208
        data = []
197 209
        try:
198
            response.json()
210
            response = response.json()
199 211
        except ValueError:
200 212
            self.handle_opengis_error(response)
201 213
            # if handle_opengis_error did not raise an error, we raise a generic one
202 214
            raise APIError(u'OpenGIS Error: unparsable error',
203 215
                           data={'content': repr(response.content[:1024])})
204
        for feature in response.json()['features']:
216
        for feature in response['features']:
205 217
            feature['text'] = feature['properties'].get(property_name)
206 218
            data.append(feature)
207 219
        return {'data': data}
......
356 368
                        break
357 369
            return result
358 370
        raise APIError('Unable to geocode')
371

  
372
    @endpoint(name='query',
373
              description=_('Query'),
374
              pattern=r'^(?P<query_slug>[\w:_-]+)/$',
375
              perm='can_access',
376
              show=False)
377
    def query(self, request, query_slug):
378
        query = get_object_or_404(Query, resource=self, slug=query_slug)
379
        return query.q(request)
380

  
381
    def export_json(self):
382
        d = super(OpenGIS, self).export_json()
383
        d['queries'] = [query.export_json() for query in self.queries.all()]
384
        return d
385

  
386
    @classmethod
387
    def import_json_real(cls, overwrite, instance, d, **kwargs):
388
        queries = d.pop('queries', [])
389
        instance = super(OpenGIS, cls).import_json_real(overwrite, instance, d, **kwargs)
390
        new = []
391
        if instance and overwrite:
392
            Query.objects.filter(resource=instance).delete()
393
        for query in queries:
394
            q = Query.import_json(query)
395
            q.resource = instance
396
            new.append(q)
397
        Query.objects.bulk_create(new)
398
        return instance
399

  
400
    def create_query_url(self):
401
        return reverse('opengis-query-new', kwargs={'slug': self.slug})
402

  
403

  
404
class Query(BaseQuery):
405
    resource = models.ForeignKey(
406
        to=OpenGIS,
407
        on_delete=models.CASCADE,
408
        related_name='queries',
409
        verbose_name=_('Resource'))
410

  
411
    typename = models.CharField(_('Feature type'), max_length=256)
412
    filter_expression = models.TextField(_('XML filter'), blank=True)
413

  
414
    delete_view = 'opengis-query-delete'
415
    edit_view = 'opengis-query-edit'
416

  
417
    def as_endpoint(self):
418
        resource_endpoint = self.resource.query.endpoint_info
419
        endpoint = super(Query, self).as_endpoint(path=resource_endpoint.name)
420
        # use parameters added by resource endpoint
421
        endpoint.func = resource_endpoint.func
422
        endpoint.show_undocumented_params = False
423
        endpoint.parameters = resource_endpoint.parameters
424
        return endpoint
425

  
426
    def q(self, request):
427
        return self.resource.features(request, self.typename, property_name=None,
428
                             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
-