Projet

Général

Profil

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

Valentin Deniaud, 06 avril 2020 18:28

Télécharger (13,2 ko)

Voir les différences:

Subject: [PATCH 2/5] base: add generic BaseQuery (#20535)

 passerelle/apps/arcgis/models.py              | 146 +++---------------
 .../templates/arcgis/arcgis_detail.html       |  16 --
 passerelle/base/models.py                     |  72 +++++++++
 tests/test_arcgis.py                          |  14 +-
 4 files changed, 108 insertions(+), 140 deletions(-)
 delete mode 100644 passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html
passerelle/apps/arcgis/models.py
31 31
from passerelle.utils.api import endpoint
32 32
from passerelle.utils.conversion import num2deg
33 33
from passerelle.utils.templates import render_to_string, validate_template
34
from passerelle.base.models import BaseResource, HTTPResource
34
from passerelle.base.models import BaseResource, HTTPResource, BaseQuery
35 35

  
36 36

  
37 37
class ArcGISError(APIError):
......
224 224
              description=_('Query'),
225 225
              pattern=r'^(?P<query_slug>[\w:_-]+)/$',
226 226
              perm='can_access',
227
              parameters={
228
                  'q': {'description': _('Search text in display field')},
229
                  'full': {
230
                      'description': _('Returns all ArcGIS informations (geometry, metadata)'),
231
                      'type': 'bool',
232
                  },
233
              },
234 227
              show=False)
235 228
    def q(self, request, query_slug, q=None, full=False, **kwargs):
236 229
        query = get_object_or_404(Query, resource=self, slug=query_slug)
......
240 233
        for key in request.GET:
241 234
            if key not in refs:
242 235
                kwargs[key] = request.GET[key]
243
        return query.q(request, q=None, full=full, **kwargs)
236
        return query.query(request, q=None, full=full, **kwargs)
244 237

  
245 238
    def export_json(self):
246 239
        d = super(ArcGIS, self).export_json()
......
286 279
            raise ValidationError(_('invalid reference'))
287 280

  
288 281

  
289
@six.python_2_unicode_compatible
290
class Query(models.Model):
282
class Query(BaseQuery):
291 283
    resource = models.ForeignKey(
292 284
        to=ArcGIS,
293 285
        related_name='queries',
294 286
        verbose_name=_('Resource'))
295 287

  
296
    name = models.CharField(
297
        verbose_name=_('Name'),
298
        max_length=128)
299
    slug = models.SlugField(
300
        verbose_name=_('Slug'),
301
        max_length=128)
302
    description = models.TextField(
303
        verbose_name=_('Description'),
304
        blank=True)
305

  
306 288
    folder = models.CharField(
307 289
        verbose_name=_('ArcGis Folder'),
308 290
        max_length=64,
......
338 320
        validators=[validate_template],
339 321
        blank=True)
340 322

  
341
    class Meta:
342
        unique_together = [
343
            ('resource', 'name'),
344
            ('resource', 'slug'),
345
        ]
346
        ordering = ['name']
323
    delete_view = 'arcgis-query-delete'
324
    edit_view = 'arcgis-query-edit'
347 325

  
348 326
    @property
349 327
    def where_references(self):
......
353 331
        else:
354 332
            return []
355 333

  
356
    def q(self, request, q=None, full=False, **kwargs):
334
    def query(self, request, q=None, full=False, **kwargs):
357 335
        kwargs.update({
358 336
            'service': self.service,
359 337
        })
......
371 349
            kwargs['where'] = formatter.format(self.where, **format_kwargs)
372 350
        return self.resource.mapservice_query(request, q=q, full=full, **kwargs)
373 351

  
374
    class QueryEndpoint:
375
        http_method = 'get'
376

  
377
        def __init__(self, query):
378
            self.object = query.resource
379
            self.query = query
380
            self.name = 'q/%s/' % query.slug
381

  
382
        @property
383
        def description(self):
384
            return self.query.name
385

  
386
        @property
387
        def long_description(self):
388
            return self.query.description
389

  
390
        def example_url(self):
391
            kwargs = {
392
                'connector': self.object.get_connector_slug(),
393
                'slug': self.object.slug,
394
                'endpoint': self.name,
395
            }
396
            query_string = ''
397
            if self.query.where_references:
398
                query_string = '?' + '&'.join(
399
                    ['%s=%s' % (ref, '') for ref, _ in self.query.where_references + [('q', ''), ('full', '')]])
400
            return reverse('generic-endpoint', kwargs=kwargs) + query_string
401

  
402
        def example_url_as_html(self):
403
            kwargs = {
404
                'connector': self.object.get_connector_slug(),
405
                'slug': self.object.slug,
406
                'endpoint': self.name,
407
            }
408
            query_string = ''
409
            if self.query.where_references:
410
                query_string = '?' + '&amp;'.join(
411
                    [format_html('{0}=<i class="varname">{1}</i>', ref, ref)
412
                     for ref, klass in self.query.where_references + [('q', ''), ('full', '')]])
413
            return mark_safe(reverse('generic-endpoint', kwargs=kwargs) + query_string)
414

  
415
        def get_params(self):
416
            params = []
417
            for ref, klass in self.query.where_references:
418
                params.append({
419
                    'name': ref,
420
                    'type': 'integer' if klass is int else 'string',
421
                })
422

  
423
            # Copy generic params descriptions from mapservice_query if they
424
            # are not overloaded by the query
425
            for param in self.object.mapservice_query.endpoint_info.get_params():
426
                if (param['name'] in ('folder', 'service', 'layer', 'id_template')
427
                        and getattr(self.query, param['name'])):
428
                    continue
429
                if param['name'] == 'template' and self.query.text_template:
430
                    continue
431
                params.append(param)
432

  
433
            return params
434

  
435
    @property
436
    def endpoint(self):
437
        return self.QueryEndpoint(self)
438

  
439
    def __str__(self):
440
        return self.name
441

  
442
    def export_json(self):
443
        d = {}
444
        fields = [
445
            f for f in self.__class__._meta.get_fields()
446
            if f.concrete and (
447
                not f.is_relation
448
                or f.one_to_one
449
                or (f.many_to_one and f.related_model)
450
            ) and f.name not in ['id', 'resource']
451
        ]
452
        for field in fields:
453
            d[field.name] = getattr(self, field.name)
454
        return d
455

  
456
    @classmethod
457
    def import_json(cls, d):
458
        return cls(**d)
459

  
460
    def delete_url(self):
461
        return reverse('arcgis-query-delete',
462
                       kwargs={'slug': self.resource.slug, 'pk': self.pk})
463

  
464
    def edit_url(self):
465
        return reverse('arcgis-query-edit',
466
                       kwargs={'slug': self.resource.slug, 'pk': self.pk})
352
    def as_endpoint(self):
353
        endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
354

  
355
        # build parameters list from original mapservice endpoint
356
        mapservice_endpoint = self.resource.mapservice_query.endpoint_info
357
        endpoint.func = mapservice_endpoint.func
358
        endpoint.show_undocumented_params = False
359
        keep_params = ['folder', 'service', 'layer', 'id_template', 'q', 'full']
360
        if not self.text_template:
361
            keep_params.append('template')
362
        endpoint.parameters = {name: mapservice_endpoint.parameters[name]
363
                               for name in keep_params if not getattr(self, name, None)}
364

  
365
        if not self.where_references:
366
            return endpoint
367

  
368
        for ref, klass in self.where_references:
369
            endpoint.parameters[ref] = {'type': 'integer' if klass is int else 'string'}
370
        return endpoint
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
257 257
                    endpoint_info.http_method = http_method
258 258
                    endpoints.append(endpoint_info)
259 259
        endpoints.sort(key=lambda x: (x.name or '', x.pattern or ''))
260
        if hasattr(self, 'queries'):
261
            self.append_custom_queries(endpoints)
260 262
        return endpoints
261 263

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

  
262 269
    def get_connector_permissions(self):
263 270
        perms = {}
264 271
        for endpoint_info in self.get_endpoints_infos():
......
971 978

  
972 979
    def __str__(self):
973 980
        return '%s %s %s' % (self.timestamp, self.appname, self.slug)
981

  
982

  
983
@six.python_2_unicode_compatible
984
class BaseQuery(models.Model):
985
    '''Base for building custom queries.
986

  
987
    It must define "resource" attribute as a ForeignKey to a BaseResource subclass,
988
    and probably extend its "as_endpoint" method to document its parameters.
989
    '''
990

  
991
    name = models.CharField(
992
        verbose_name=_('Name'),
993
        max_length=128)
994
    slug = models.SlugField(
995
        verbose_name=_('Slug'),
996
        max_length=128)
997
    description = models.TextField(
998
        verbose_name=_('Description'),
999
        blank=True)
1000

  
1001
    http_method = 'get'
1002

  
1003
    class Meta:
1004
        abstract = True
1005
        unique_together = [
1006
            ('resource', 'name'),
1007
            ('resource', 'slug'),
1008
        ]
1009
        ordering = ['name']
1010

  
1011
    def as_endpoint(self, path=''):
1012
        name = '%s/%s/' % (path, self.slug) if path else self.slug + '/'
1013
        e = endpoint(name=name, description=self.name, long_description=self.description)
1014
        e.http_method = self.http_method
1015
        e.object = self.resource
1016
        return e
1017

  
1018
    def __str__(self):
1019
        return self.name
1020

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

  
1035
    @classmethod
1036
    def import_json(cls, d):
1037
        return cls(**d)
1038

  
1039
    def delete_url(self):
1040
        return reverse(self.delete_view,
1041
                       kwargs={'slug': self.resource.slug, 'pk': self.pk})
1042

  
1043
    def edit_url(self):
1044
        return reverse(self.edit_view,
1045
                       kwargs={'slug': self.resource.slug, 'pk': self.pk})
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',
......
290 290
        where='adress LIKE {adress:s}')
291 291

  
292 292

  
293
def test_query_q_method(arcgis, query, rf):
293
def test_query_query_method(arcgis, query, rf):
294 294
    with mock.patch('passerelle.utils.Request.get') as requests_get:
295 295
        requests_get.return_value = utils.FakedResponse(content=STATES,
296 296
                                                        status_code=200)
297
        query.q(rf.get('/', data={'adress': "AVENUE D'ANNAM"}), full=True) == []
297
        query.query(rf.get('/', data={'adress': "AVENUE D'ANNAM"}), full=True) == []
298 298
        assert requests_get.call_count == 1
299 299
        assert requests_get.call_args[0][0] == 'https://arcgis.example.net/services/fold/serv/MapServer/1/query'
300 300
        args = requests_get.call_args[1]['params']
......
379 379
        assert resp.content_type == 'image/png'
380 380

  
381 381

  
382
def test_query_documentation(arcgis, query, app):
383
    resp = app.get(arcgis.get_absolute_url())
384
    assert query.name in resp.text
385
    assert query.description in resp.text
386
    # additional parameter appears in endpoint documentation
387
    assert '<span class="param-name">adress</span>' in resp.text
388

  
389

  
382 390
def test_export_import(query):
383 391
    assert ArcGIS.objects.count() == 1
384 392
    assert Query.objects.count() == 1
385
-