Projet

Général

Profil

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

Valentin Deniaud, 06 avril 2020 18:28

Télécharger (15 ko)

Voir les différences:

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

 passerelle/apps/opengis/forms.py              | 40 ++++++++
 .../migrations/0007_auto_20200401_1032.py     | 45 +++++++++
 passerelle/apps/opengis/models.py             | 91 +++++++++++++++++--
 .../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, 242 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
        widgets = {
29
            'filter_expression': forms.Textarea(),
30
        }
31
        fields = '__all__'
32
        exclude = ['resource']
33

  
34
    def clean_filter_expression(self):
35
        filter_expression = self.cleaned_data['filter_expression']
36
        try:
37
            ET.fromstring(filter_expression)
38
        except ET.ParseError:
39
            raise forms.ValidationError(_('Filter is not valid XML.'))
40
        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-01 08:32
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
                '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
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import copy
17 18
import math
18 19
import xml.etree.ElementTree as ET
19 20

  
......
24 25
from django.core.cache import cache
25 26
from django.db import models
26 27
from django.http import HttpResponse
28
from django.shortcuts import get_object_or_404
29
from django.urls import reverse
27 30
from django.utils.six.moves.html_parser import HTMLParser
28 31
from django.utils.text import slugify
29 32
from django.utils.translation import ugettext_lazy as _
30 33

  
31
from passerelle.base.models import BaseResource
34
from passerelle.base.models import BaseResource, BaseQuery
32 35
from passerelle.utils.api import endpoint
33 36
from passerelle.utils.conversion import num2deg
34 37
from passerelle.utils.jsonresponse import APIError
......
143 146
                })
144 147
            response.raise_for_status()
145 148

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

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

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

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

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

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

  
404

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

  
412
    typename = models.CharField(
413
        verbose_name=_('Feature type'),
414
        max_length=256,
415
        blank=True)
416
    filter_expression = models.CharField(
417
        verbose_name=_('XML filter'),
418
        max_length=4096,
419
        blank=True)
420

  
421
    delete_view = 'opengis-query-delete'
422
    edit_view = 'opengis-query-edit'
423

  
424
    def as_endpoint(self):
425
        resource_endpoint = self.resource.query.endpoint_info
426
        endpoint = super(Query, self).as_endpoint(path=resource_endpoint.name)
427
        # use parameters added by resource endpoint
428
        endpoint.func = resource_endpoint.func
429
        endpoint.show_undocumented_params = False
430
        endpoint.parameters = copy.deepcopy(resource_endpoint.parameters)
431
        return endpoint
432

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