Projet

Général

Profil

0003-base-add-generic-BaseQuery-20535.patch

Valentin Deniaud, 26 mars 2020 17:58

Télécharger (14,6 ko)

Voir les différences:

Subject: [PATCH 3/6] base: add generic BaseQuery (#20535)

 passerelle/apps/arcgis/models.py              | 136 ++++--------------
 .../templates/arcgis/arcgis_detail.html       |  16 ---
 passerelle/base/models.py                     |  85 +++++++++++
 passerelle/utils/api.py                       |  12 +-
 tests/test_arcgis.py                          |  10 +-
 5 files changed, 129 insertions(+), 130 deletions(-)
 delete mode 100644 passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html
passerelle/apps/arcgis/models.py
29 29
from passerelle.utils.jsonresponse import APIError
30 30
from passerelle.utils.api import endpoint
31 31
from passerelle.utils.templates import render_to_string, validate_template
32
from passerelle.base.models import BaseResource, HTTPResource
32
from passerelle.base.models import BaseResource, HTTPResource, BaseQuery
33 33

  
34 34

  
35 35
class ArcGISError(APIError):
......
195 195
              description=_('Query'),
196 196
              pattern=r'^(?P<query_slug>[\w:_-]+)/$',
197 197
              perm='can_access',
198
              parameters={
199
                  'q': {'description': _('Search text in display field')},
200
                  'full': {
201
                      'description': _('Returns all ArcGIS informations (geometry, metadata)'),
202
                      'type': 'bool',
203
                  },
204
              },
205 198
              show=False)
206 199
    def q(self, request, query_slug, q=None, full=False, **kwargs):
207 200
        query = get_object_or_404(Query, resource=self, slug=query_slug)
......
226 219
        if instance and overwrite:
227 220
            Query.objects.filter(resource=instance).delete()
228 221
        for query in queries:
222
            query['resource'] = instance
229 223
            q = Query.import_json(query)
230
            q.resource = instance
231 224
            new.append(q)
232 225
        Query.objects.bulk_create(new)
233 226
        return instance
......
257 250
            raise ValidationError(_('invalid reference'))
258 251

  
259 252

  
260
@six.python_2_unicode_compatible
261
class Query(models.Model):
253
class Query(BaseQuery):
262 254
    resource = models.ForeignKey(
263 255
        to=ArcGIS,
264 256
        verbose_name=_('Resource'))
265 257

  
266
    name = models.CharField(
267
        verbose_name=_('Name'),
268
        max_length=128)
269
    slug = models.SlugField(
270
        verbose_name=_('Slug'),
271
        max_length=128)
272
    description = models.TextField(
273
        verbose_name=_('Description'),
274
        blank=True)
275

  
276 258
    folder = models.CharField(
277 259
        verbose_name=_('ArcGis Folder'),
278 260
        max_length=64,
......
308 290
        validators=[validate_template],
309 291
        blank=True)
310 292

  
311
    class Meta:
312
        unique_together = [
313
            ('resource', 'name'),
314
            ('resource', 'slug'),
315
        ]
316
        ordering = ['name']
293
    def __init__(self, *args, **kwargs):
294
        super(Query, self).__init__(*args, **kwargs)
295
        self.endpoint = self.resource.mapservice_query
296
        self.called_from = self.resource.q
317 297

  
318 298
    @property
319 299
    def where_references(self):
......
339 319
            format_kwargs = {key: request.GET.get(key, '') for key, klass in self.where_references}
340 320
            formatter = SqlFormatter()
341 321
            kwargs['where'] = formatter.format(self.where, **format_kwargs)
342
        return self.resource.mapservice_query(request, q=q, full=full, **kwargs)
343

  
344
    class QueryEndpoint:
345
        http_method = 'get'
346

  
347
        def __init__(self, query):
348
            self.object = query.resource
349
            self.query = query
350
            self.name = 'q/%s/' % query.slug
351

  
352
        @property
353
        def description(self):
354
            return self.query.name
355

  
356
        @property
357
        def long_description(self):
358
            return self.query.description
359

  
360
        def example_url(self):
361
            kwargs = {
362
                'connector': self.object.get_connector_slug(),
363
                'slug': self.object.slug,
364
                'endpoint': self.name,
365
            }
366
            query_string = ''
367
            if self.query.where_references:
368
                query_string = '?' + '&'.join(
369
                    ['%s=%s' % (ref, '') for ref, _ in self.query.where_references + [('q', ''), ('full', '')]])
370
            return reverse('generic-endpoint', kwargs=kwargs) + query_string
371

  
372
        def example_url_as_html(self):
373
            kwargs = {
374
                'connector': self.object.get_connector_slug(),
375
                'slug': self.object.slug,
376
                'endpoint': self.name,
377
            }
378
            query_string = ''
379
            if self.query.where_references:
380
                query_string = '?' + '&amp;'.join(
381
                    [format_html('{0}=<i class="varname">{1}</i>', ref, ref)
382
                     for ref, klass in self.query.where_references + [('q', ''), ('full', '')]])
383
            return mark_safe(reverse('generic-endpoint', kwargs=kwargs) + query_string)
384

  
385
        def get_params(self):
386
            params = []
387
            for ref, klass in self.query.where_references:
388
                params.append({
389
                    'name': ref,
390
                    'type': 'integer' if klass is int else 'string',
391
                })
392

  
393
            # Copy generic params descriptions from mapservice_query if they
394
            # are not overloaded by the query
395
            for param in self.object.mapservice_query.endpoint_info.get_params():
396
                if (param['name'] in ('folder', 'service', 'layer', 'id_template')
397
                        and getattr(self.query, param['name'])):
398
                    continue
399
                if param['name'] == 'template' and self.query.text_template:
400
                    continue
401
                params.append(param)
402

  
403
            return params
322
        return self.endpoint(request, q=q, full=full, **kwargs)
404 323

  
405
    @property
406
    def endpoint(self):
407
        return self.QueryEndpoint(self)
324
    def as_endpoint(self):
325
        endpoint = super(Query, self).as_endpoint()
326
        endpoint.exclude_params = [param for param in ('folder', 'service', 'layer', 'id_template')
327
                                   if getattr(self, param)]
328
        endpoint.exclude_params.extend(['lat', 'lon', 'latmin', 'lonmin', 'latmax', 'lonmax'])
329
        if self.text_template:
330
            endpoint.exclude_params.append('template')
408 331

  
409
    def __str__(self):
410
        return self.name
332
        if not self.where_references:
333
            return endpoint
411 334

  
412
    def export_json(self):
413
        d = {}
414
        fields = [
415
            f for f in self.__class__._meta.get_fields()
416
            if f.concrete and (
417
                not f.is_relation
418
                or f.one_to_one
419
                or (f.many_to_one and f.related_model)
420
            ) and f.name not in ['id', 'resource']
421
        ]
422
        for field in fields:
423
            d[field.name] = getattr(self, field.name)
424
        return d
335
        for ref, klass in self.where_references:
336
            endpoint.extra_params.append({
337
                'name': ref,
338
                'type': 'integer' if klass is int else 'string',
339
            })
425 340

  
426
    @classmethod
427
    def import_json(cls, d):
428
        return cls(**d)
341
        params = [ref for ref, _ in self.where_references]
342
        params = {param: {'example_value': ''} for param in params}
343
        endpoint.parameters.update(params)
344
        return endpoint
429 345

  
430 346
    def delete_url(self):
431 347
        return reverse('arcgis-query-delete',
passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
3

  
4
{% block endpoints %}
5
{{ block.super }}
6

  
7

  
8
{% if object.query_set.exists %}
9
<h2>{% trans "Custom queries" %}</h2>
10
<ul class="endpoints">
11
  {% for query in object.query_set.all %}
12
    {% include "passerelle/manage/endpoint.html" with endpoint=query.endpoint %}
13
  {% endfor %}
14
</ul>
15
{% endif %}
16
{% endblock %}
passerelle/base/models.py
256 256
                    endpoint_info.http_method = http_method
257 257
                    endpoints.append(endpoint_info)
258 258
        endpoints.sort(key=lambda x: (x.name or '', x.pattern or ''))
259
        if hasattr(self, 'query_set'):
260
            self.append_custom_queries(endpoints)
259 261
        return endpoints
260 262

  
263
    def append_custom_queries(self, endpoints):
264
        for query in self.query_set.all():
265
            if hasattr(query, 'as_endpoint'):
266
                endpoints.append(query.as_endpoint())
267

  
261 268
    def get_connector_permissions(self):
262 269
        perms = {}
263 270
        for endpoint_info in self.get_endpoints_infos():
......
955 962

  
956 963
    class Meta:
957 964
        abstract = True
965

  
966

  
967
@six.python_2_unicode_compatible
968
class BaseQuery(models.Model):
969
    '''Base for building custom queries.
970

  
971
    A query stores parameters, which are then used to call an endpoint.
972
    It must define "resource" attribute as a ForeignKey to a BaseResource subclass,
973
    as well as "endpoint" which should be the endpoint to call, and "called_from"
974
    which should be the endpoint that triggers the query.
975
    '''
976

  
977
    name = models.CharField(
978
        verbose_name=_('Name'),
979
        max_length=128)
980
    slug = models.SlugField(
981
        verbose_name=_('Slug'),
982
        max_length=128)
983
    description = models.TextField(
984
        verbose_name=_('Description'),
985
        blank=True)
986

  
987
    http_method = 'get'
988

  
989
    class Meta:
990
        abstract = True
991
        unique_together = [
992
            ('resource', 'name'),
993
            ('resource', 'slug'),
994
        ]
995
        ordering = ['name']
996

  
997
    def as_endpoint(self):
998
        # deepcopying is too dangerous, instead copy then deepcopy only relevant attributes
999
        endpoint = copy.copy(self.endpoint.endpoint_info)
1000
        attr_to_change = ['descriptions', 'long_descriptions', 'parameters', 'extra_params']
1001
        for attr in attr_to_change:
1002
            setattr(endpoint, attr, copy.deepcopy(getattr(endpoint, attr)))
1003
        endpoint.name = '%s/%s/' % (self.called_from.endpoint_info.name, self.slug)
1004
        endpoint.http_method = self.http_method
1005
        endpoint.descriptions[self.http_method] = self.name
1006
        endpoint.long_descriptions[self.http_method] = self.description
1007
        new_params = self.called_from.endpoint_info.parameters
1008
        endpoint.parameters.update(new_params)
1009
        for param, info in new_params.items():
1010
            d = {'name': param}
1011
            d.update(info)
1012
            endpoint.extra_params.append(d)
1013
        return endpoint
1014

  
1015
    def __str__(self):
1016
        return self.name
1017

  
1018
    def export_json(self):
1019
        d = {}
1020
        fields = [
1021
            f for f in self.__class__._meta.get_fields()
1022
            if f.concrete and (
1023
                not f.is_relation
1024
                or f.one_to_one
1025
                or (f.many_to_one and f.related_model)
1026
            ) and f.name not in ['id', 'resource']
1027
        ]
1028
        for field in fields:
1029
            d[field.name] = getattr(self, field.name)
1030
        return d
1031

  
1032
    @classmethod
1033
    def import_json(cls, d):
1034
        return cls(**d)
1035

  
1036
    def delete_url(self):
1037
        return reverse(self.delete_view,
1038
                       kwargs={'slug': self.resource.slug, 'pk': self.pk})
1039

  
1040
    def edit_url(self):
1041
        return reverse(self.edit_view,
1042
                       kwargs={'slug': self.resource.slug, 'pk': self.pk})
passerelle/utils/api.py
43 43
                 parameters=None,
44 44
                 cache_duration=None,
45 45
                 post=None,
46
                 show=True):
46
                 show=True,
47
                 extra_params=None,
48
                 exclude_params=None):
47 49
        self.perm = perm
48 50
        self.methods = methods
49 51
        self.serializer_type = serializer_type
......
71 73
            if post.get('long_description'):
72 74
                self.long_descriptions['post'] = post.get('long_description')
73 75
        self.show = show
76
        self.extra_params = extra_params or []
77
        self.exclude_params = exclude_params or set()
74 78

  
75 79
    def __call__(self, func):
76 80
        func.endpoint_info = self
......
81 85

  
82 86
    def get_example_params(self):
83 87
        return {param: info['example_value'] for param, info in self.parameters.items()
84
                if 'example_value' in info}
88
                if not param in self.exclude_params and 'example_value' in info}
85 89

  
86 90
    def get_query_parameters(self):
87 91
        query_parameters = []
......
190 194
                    typ = type_to_str(defaults[param])
191 195
                    if typ:
192 196
                        param_info['type'] = typ
193
            params.append(param_info)
197
            if param not in self.exclude_params:
198
                params.append(param_info)
199
        params.extend(self.extra_params)
194 200
        return params
tests/test_arcgis.py
281 281
        resource=arcgis,
282 282
        name='Adresses',
283 283
        slug='adresses',
284
        description='Recherche d\'une adresse',
284
        description='Rechercher une adresse',
285 285
        id_template='{{ attributes.ident }}',
286 286
        text_template='{{ attributes.address }} - {{ attributes.codepost }}',
287 287
        folder='fold',
......
338 338
        }
339 339

  
340 340

  
341
def test_query_documentation(arcgis, query, app):
342
    resp = app.get(arcgis.get_absolute_url())
343
    assert query.name in resp.text
344
    assert query.description in resp.text
345
    # additional parameter appears in endpoint documentation
346
    assert '<span class="param-name">adress</span>' in resp.text
347

  
348

  
341 349
def test_export_import(query):
342 350
    assert ArcGIS.objects.count() == 1
343 351
    assert Query.objects.count() == 1
344
-