Projet

Général

Profil

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

Valentin Deniaud, 26 mars 2020 17:58

Télécharger (13,8 ko)

Voir les différences:

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

 passerelle/apps/opengis/forms.py              | 37 ++++++++
 .../migrations/0007_auto_20200324_1019.py     | 44 ++++++++++
 passerelle/apps/opengis/models.py             | 85 +++++++++++++++++--
 .../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, 234 insertions(+), 5 deletions(-)
 create mode 100644 passerelle/apps/opengis/forms.py
 create mode 100644 passerelle/apps/opengis/migrations/0007_auto_20200324_1019.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
        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
-