From d79b9044bead5125f990ce06bd5dbb9752a631a2 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 24 Mar 2020 16:39:37 +0100 Subject: [PATCH 2/5] base: add generic BaseQuery (#20535) --- passerelle/apps/arcgis/models.py | 143 +++--------------- .../templates/arcgis/arcgis_detail.html | 16 -- passerelle/base/models.py | 72 +++++++++ tests/test_arcgis.py | 10 +- 4 files changed, 106 insertions(+), 135 deletions(-) delete mode 100644 passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html diff --git a/passerelle/apps/arcgis/models.py b/passerelle/apps/arcgis/models.py index 01c64876..bda8ec6b 100644 --- a/passerelle/apps/arcgis/models.py +++ b/passerelle/apps/arcgis/models.py @@ -31,7 +31,7 @@ from passerelle.utils.jsonresponse import APIError from passerelle.utils.api import endpoint from passerelle.utils.conversion import num2deg from passerelle.utils.templates import render_to_string, validate_template -from passerelle.base.models import BaseResource, HTTPResource +from passerelle.base.models import BaseResource, HTTPResource, BaseQuery class ArcGISError(APIError): @@ -224,13 +224,6 @@ class ArcGIS(BaseResource, HTTPResource): description=_('Query'), pattern=r'^(?P[\w:_-]+)/$', perm='can_access', - parameters={ - 'q': {'description': _('Search text in display field')}, - 'full': { - 'description': _('Returns all ArcGIS informations (geometry, metadata)'), - 'type': 'bool', - }, - }, show=False) def q(self, request, query_slug, q=None, full=False, **kwargs): query = get_object_or_404(Query, resource=self, slug=query_slug) @@ -286,23 +279,12 @@ def validate_where(format_string): raise ValidationError(_('invalid reference')) -@six.python_2_unicode_compatible -class Query(models.Model): +class Query(BaseQuery): resource = models.ForeignKey( to=ArcGIS, related_name='queries', verbose_name=_('Resource')) - name = models.CharField( - verbose_name=_('Name'), - max_length=128) - slug = models.SlugField( - verbose_name=_('Slug'), - max_length=128) - description = models.TextField( - verbose_name=_('Description'), - blank=True) - folder = models.CharField( verbose_name=_('ArcGis Folder'), max_length=64, @@ -338,12 +320,8 @@ class Query(models.Model): validators=[validate_template], blank=True) - class Meta: - unique_together = [ - ('resource', 'name'), - ('resource', 'slug'), - ] - ordering = ['name'] + delete_view = 'arcgis-query-delete' + edit_view = 'arcgis-query-edit' @property def where_references(self): @@ -371,96 +349,25 @@ class Query(models.Model): kwargs['where'] = formatter.format(self.where, **format_kwargs) return self.resource.mapservice_query(request, q=q, full=full, **kwargs) - class QueryEndpoint: - http_method = 'get' - - def __init__(self, query): - self.object = query.resource - self.query = query - self.name = 'q/%s/' % query.slug - - @property - def description(self): - return self.query.name - - @property - def long_description(self): - return self.query.description - - def example_url(self): - kwargs = { - 'connector': self.object.get_connector_slug(), - 'slug': self.object.slug, - 'endpoint': self.name, - } - query_string = '' - if self.query.where_references: - query_string = '?' + '&'.join( - ['%s=%s' % (ref, '') for ref, _ in self.query.where_references + [('q', ''), ('full', '')]]) - return reverse('generic-endpoint', kwargs=kwargs) + query_string - - def example_url_as_html(self): - kwargs = { - 'connector': self.object.get_connector_slug(), - 'slug': self.object.slug, - 'endpoint': self.name, + def as_endpoint(self): + endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name) + + mapservice_endpoint = self.resource.mapservice_query.endpoint_info + endpoint.func = mapservice_endpoint.func + endpoint.show_undocumented_params = False + + # Copy generic params descriptions from mapservice_query if they + # are not overloaded by the query + for param in mapservice_endpoint.parameters: + if param in ('folder', 'service', 'layer', 'id_template') and getattr(self, param): + continue + if param == 'template' and self.text_template: + continue + endpoint.parameters[param] = mapservice_endpoint.parameters[param] + + for ref, klass in self.where_references: + endpoint.parameters[ref] = { + 'type': 'integer' if klass is int else 'string', + 'example_value': '', } - query_string = '' - if self.query.where_references: - query_string = '?' + '&'.join( - [format_html('{0}={1}', ref, ref) - for ref, klass in self.query.where_references + [('q', ''), ('full', '')]]) - return mark_safe(reverse('generic-endpoint', kwargs=kwargs) + query_string) - - def get_params(self): - params = [] - for ref, klass in self.query.where_references: - params.append({ - 'name': ref, - 'type': 'integer' if klass is int else 'string', - }) - - # Copy generic params descriptions from mapservice_query if they - # are not overloaded by the query - for param in self.object.mapservice_query.endpoint_info.get_params(): - if (param['name'] in ('folder', 'service', 'layer', 'id_template') - and getattr(self.query, param['name'])): - continue - if param['name'] == 'template' and self.query.text_template: - continue - params.append(param) - - return params - - @property - def endpoint(self): - return self.QueryEndpoint(self) - - def __str__(self): - return self.name - - def export_json(self): - d = {} - fields = [ - f for f in self.__class__._meta.get_fields() - if f.concrete and ( - not f.is_relation - or f.one_to_one - or (f.many_to_one and f.related_model) - ) and f.name not in ['id', 'resource'] - ] - for field in fields: - d[field.name] = getattr(self, field.name) - return d - - @classmethod - def import_json(cls, d): - return cls(**d) - - def delete_url(self): - return reverse('arcgis-query-delete', - kwargs={'slug': self.resource.slug, 'pk': self.pk}) - - def edit_url(self): - return reverse('arcgis-query-edit', - kwargs={'slug': self.resource.slug, 'pk': self.pk}) + return endpoint diff --git a/passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html b/passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html deleted file mode 100644 index 18af32dc..00000000 --- a/passerelle/apps/arcgis/templates/arcgis/arcgis_detail.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "passerelle/manage/service_view.html" %} -{% load i18n passerelle %} - -{% block endpoints %} -{{ block.super }} - - -{% if object.query_set.exists %} -

{% trans "Custom queries" %}

- -{% endif %} -{% endblock %} diff --git a/passerelle/base/models.py b/passerelle/base/models.py index ebcb5208..cb41b917 100644 --- a/passerelle/base/models.py +++ b/passerelle/base/models.py @@ -257,8 +257,15 @@ class BaseResource(models.Model): endpoint_info.http_method = http_method endpoints.append(endpoint_info) endpoints.sort(key=lambda x: (x.name or '', x.pattern or '')) + if hasattr(self, 'queries'): + self.append_custom_queries(endpoints) return endpoints + def append_custom_queries(self, endpoints): + for query in self.queries.all(): + if hasattr(query, 'as_endpoint'): + endpoints.append(query.as_endpoint()) + def get_connector_permissions(self): perms = {} for endpoint_info in self.get_endpoints_infos(): @@ -971,3 +978,68 @@ class SMSLog(models.Model): def __str__(self): return '%s %s %s' % (self.timestamp, self.appname, self.slug) + + +@six.python_2_unicode_compatible +class BaseQuery(models.Model): + '''Base for building custom queries. + + It must define "resource" attribute as a ForeignKey to a BaseResource subclass, + and probably extend its "as_endpoint" method to document its parameters. + ''' + + name = models.CharField( + verbose_name=_('Name'), + max_length=128) + slug = models.SlugField( + verbose_name=_('Slug'), + max_length=128) + description = models.TextField( + verbose_name=_('Description'), + blank=True) + + http_method = 'get' + + class Meta: + abstract = True + unique_together = [ + ('resource', 'name'), + ('resource', 'slug'), + ] + ordering = ['name'] + + def as_endpoint(self, path=''): + name = '%s/%s/' % (path, self.slug) if path else self.slug + '/' + e = endpoint(name=name, description=self.name, long_description=self.description) + e.http_method = self.http_method + e.object = self.resource + return e + + def __str__(self): + return self.name + + def export_json(self): + d = {} + fields = [ + f for f in self.__class__._meta.get_fields() + if f.concrete and ( + not f.is_relation + or f.one_to_one + or (f.many_to_one and f.related_model) + ) and f.name not in ['id', 'resource'] + ] + for field in fields: + d[field.name] = getattr(self, field.name) + return d + + @classmethod + def import_json(cls, d): + return cls(**d) + + def delete_url(self): + return reverse(self.delete_view, + kwargs={'slug': self.resource.slug, 'pk': self.pk}) + + def edit_url(self): + return reverse(self.edit_view, + kwargs={'slug': self.resource.slug, 'pk': self.pk}) diff --git a/tests/test_arcgis.py b/tests/test_arcgis.py index ffb898c9..6feadeca 100644 --- a/tests/test_arcgis.py +++ b/tests/test_arcgis.py @@ -281,7 +281,7 @@ def query(arcgis): resource=arcgis, name='Adresses', slug='adresses', - description='Recherche d\'une adresse', + description='Rechercher une adresse', id_template='{{ attributes.ident }}', text_template='{{ attributes.address }} - {{ attributes.codepost }}', folder='fold', @@ -379,6 +379,14 @@ def test_tile_endpoint(arcgis, app): assert resp.content_type == 'image/png' +def test_query_documentation(arcgis, query, app): + resp = app.get(arcgis.get_absolute_url()) + assert query.name in resp.text + assert query.description in resp.text + # additional parameter appears in endpoint documentation + assert 'adress' in resp.text + + def test_export_import(query): assert ArcGIS.objects.count() == 1 assert Query.objects.count() == 1 -- 2.20.1