From 06502437236036b9377887375aa41d1fe338c572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Sun, 5 Jun 2016 10:00:59 +0200 Subject: [PATCH] general: add generic endpoint view (#11204) --- passerelle/apps/base_adresse/models.py | 66 +++++++++++++ .../base_adresse/base_adresse_detail.html | 8 +- passerelle/apps/base_adresse/urls.py | 3 - passerelle/apps/base_adresse/views.py | 106 +++------------------ passerelle/urls.py | 7 +- passerelle/utils/api.py | 19 ++++ passerelle/views.py | 27 +++++- 7 files changed, 130 insertions(+), 106 deletions(-) create mode 100644 passerelle/utils/api.py diff --git a/passerelle/apps/base_adresse/models.py b/passerelle/apps/base_adresse/models.py index 2e27487..1b81bc2 100644 --- a/passerelle/apps/base_adresse/models.py +++ b/passerelle/apps/base_adresse/models.py @@ -1,8 +1,13 @@ +import requests +import urllib +import urlparse + from django.core.urlresolvers import reverse from django.db import models from django.utils.translation import ugettext_lazy as _ from passerelle.base.models import BaseResource +from passerelle.utils.api import endpoint class BaseAdresse(BaseResource): @@ -22,3 +27,64 @@ class BaseAdresse(BaseResource): @classmethod def get_icon_class(cls): return 'gis' + + @endpoint + def search(self, request, q, format='json'): + if format != 'json': + raise NotImplementedError() + + scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.service_url) + path = '/search/' + query = urllib.urlencode({'q': q, 'limit': 1}) + url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) + + result_response = requests.get(url) + result = [] + + for feature in result_response.json().get('features'): + if not feature['geometry']['type'] == 'Point': + continue # skip unknown + result.append({ + 'lon': str(feature['geometry']['coordinates'][0]), + 'lat': str(feature['geometry']['coordinates'][1]), + 'display_name': feature['properties']['label'], + }) + break + + return result + + @endpoint + def reverse(self, request, lat, lon, format='json'): + response_format = 'json' + if format != 'json': + raise NotImplementedError() + + scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.service_url) + path = '/reverse/' + query = urllib.urlencode({'lat': lat, 'lon': lon}) + url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) + + result_response = requests.get(url) + result = None + + for feature in result_response.json().get('features'): + if not feature['geometry']['type'] == 'Point': + continue # skip unknown + result = {} + result['lon'] = str(feature['geometry']['coordinates'][0]) + result['lat'] = str(feature['geometry']['coordinates'][1]) + result['address'] = {'country': 'France'} + for prop in feature['properties']: + if prop in ('city', 'postcode'): + result['address'][prop] = feature['properties'][prop] + elif prop == 'housenumber': + result['address']['house_number'] = feature['properties'][prop] + elif prop == 'label': + result['display_name'] = feature['properties'][prop] + elif prop == 'name': + house_number = feature['properties'].get('housenumber') + value = feature['properties'][prop] + if house_number and value.startswith(house_number): + value = value[len(house_number):].strip() + result['address']['road'] = value + return result diff --git a/passerelle/apps/base_adresse/templates/base_adresse/base_adresse_detail.html b/passerelle/apps/base_adresse/templates/base_adresse/base_adresse_detail.html index 11f5a82..274d709 100644 --- a/passerelle/apps/base_adresse/templates/base_adresse/base_adresse_detail.html +++ b/passerelle/apps/base_adresse/templates/base_adresse/base_adresse_detail.html @@ -19,10 +19,10 @@ format.

diff --git a/passerelle/apps/base_adresse/urls.py b/passerelle/apps/base_adresse/urls.py index 0cf88c3..3805e57 100644 --- a/passerelle/apps/base_adresse/urls.py +++ b/passerelle/apps/base_adresse/urls.py @@ -7,8 +7,5 @@ from views import * urlpatterns = patterns('', url(r'^(?P[\w,-]+)/$', BaseAdresseDetailView.as_view(), name='base_adresse-view'), - - url(r'^(?P[\w,-]+)/search$', SearchView.as_view(), name='base_adresse-search'), url(r'^(?P[\w,-]+)/search/(?P.*)$', SearchPathView.as_view(), name='base_adresse-path-search'), - url(r'^(?P[\w,-]+)/reverse$', ReverseView.as_view(), name='base_adresse-reverse'), ) diff --git a/passerelle/apps/base_adresse/views.py b/passerelle/apps/base_adresse/views.py index ae75ef6..23975f2 100644 --- a/passerelle/apps/base_adresse/views.py +++ b/passerelle/apps/base_adresse/views.py @@ -1,13 +1,5 @@ -import requests -import urllib -import urlparse - -from django.http import HttpResponseBadRequest -from django.views.generic.base import View -from django.views.generic.detail import SingleObjectMixin, DetailView - -from passerelle import utils - +from django.views.generic.detail import DetailView +from passerelle.views import GenericEndpointView from .models import BaseAdresse @@ -20,91 +12,15 @@ class BaseAdresseDetailView(DetailView): return context -class SearchView(View, SingleObjectMixin): - model = BaseAdresse - - def get_query(self, request, *args, **kwargs): - if not 'q' in request.GET: - return HttpResponseBadRequest('missing parameter') - return request.GET['q'] - - def get(self, request, *args, **kwargs): - response_format = 'json' - if 'format' in request.GET: - response_format = request.GET['format'] - if response_format != 'json': - raise NotImplementedError() - - scheme, netloc, path, params, query, fragment = urlparse.urlparse( - self.get_object().service_url) - path = '/search/' - query = urllib.urlencode({'q': self.get_query(request, *args, **kwargs), 'limit': 1}) - url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) - - result_response = requests.get(url) - result = [] - - for feature in result_response.json().get('features'): - if not feature['geometry']['type'] == 'Point': - continue # skip unknown - result.append({ - 'lon': str(feature['geometry']['coordinates'][0]), - 'lat': str(feature['geometry']['coordinates'][1]), - 'display_name': feature['properties']['label'], - }) - break +class SearchPathView(GenericEndpointView): + def get_connector(self, **kwargs): + return 'base_adresse' - return utils.response_for_json(request, result) - - -class SearchPathView(SearchView): - def get_query(self, request, *args, **kwargs): - return kwargs.get('path') - - -class ReverseView(View, SingleObjectMixin): - model = BaseAdresse + def get_params(self, request, *args, **kwargs): + params = super(SearchPathView, self).get_params(request, *args, **kwargs) + params['q'] = kwargs.pop('path') + return params def get(self, request, *args, **kwargs): - response_format = 'json' - if 'format' in request.GET: - response_format = request.GET['format'] - if response_format != 'json': - raise NotImplementedError() - - if not 'lat' in request.GET or not 'lon' in request.GET: - return HttpResponseBadRequest('missing parameter') - - lat = request.GET['lat'] - lon = request.GET['lon'] - - scheme, netloc, path, params, query, fragment = urlparse.urlparse( - self.get_object().service_url) - path = '/reverse/' - query = urllib.urlencode({'lat': lat, 'lon': lon}) - url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) - - result_response = requests.get(url) - result = None - - for feature in result_response.json().get('features'): - if not feature['geometry']['type'] == 'Point': - continue # skip unknown - result = {} - result['lon'] = str(feature['geometry']['coordinates'][0]) - result['lat'] = str(feature['geometry']['coordinates'][1]) - result['address'] = {'country': 'France'} - for prop in feature['properties']: - if prop in ('city', 'postcode'): - result['address'][prop] = feature['properties'][prop] - elif prop == 'housenumber': - result['address']['house_number'] = feature['properties'][prop] - elif prop == 'label': - result['display_name'] = feature['properties'][prop] - elif prop == 'name': - house_number = feature['properties'].get('housenumber') - value = feature['properties'][prop] - if house_number and value.startswith(house_number): - value = value[len(house_number):].strip() - result['address']['road'] = value - return utils.response_for_json(request, result) + kwargs['endpoint'] = 'search' + return super(SearchPathView, self).get(request, *args, **kwargs) diff --git a/passerelle/urls.py b/passerelle/urls.py index 72008cb..71fb41a 100644 --- a/passerelle/urls.py +++ b/passerelle/urls.py @@ -8,7 +8,7 @@ from django.views.static import serve as static_serve from .views import (HomePageView, ManageView, ManageAddView, GenericCreateConnectorView, GenericDeleteConnectorView, - GenericEditConnectorView, + GenericEditConnectorView, GenericEndpointView, LEGACY_APPS_PATTERNS, LegacyPageView, login, logout) from .urls_utils import decorated_includes, required, app_enabled from .base.urls import access_urlpatterns @@ -74,6 +74,11 @@ for app, d in LEGACY_APPS_PATTERNS.items(): ) urlpatterns += patterns('', + url(r'^(?P[\w,-]+)/(?P[\w,-]+)/(?P[\w,-]+)$', + GenericEndpointView.as_view()) +) + +urlpatterns += patterns('', url(r'^manage/(?P[\w,-]+)/', decorated_includes(login_required, include(patterns('', url(r'^add$', diff --git a/passerelle/utils/api.py b/passerelle/utils/api.py new file mode 100644 index 0000000..b11c1a9 --- /dev/null +++ b/passerelle/utils/api.py @@ -0,0 +1,19 @@ +# passerelle - uniform access to multiple data sources and services +# Copyright (C) 2016 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +def endpoint(func): + func.endpoint_info = {} + return func diff --git a/passerelle/views.py b/passerelle/views.py index b1f5034..78dd48e 100644 --- a/passerelle/views.py +++ b/passerelle/views.py @@ -1,8 +1,9 @@ from django.apps import apps from django.contrib.auth import logout as auth_logout from django.contrib.auth import views as auth_views -from django.http import HttpResponseRedirect, Http404 -from django.views.generic import TemplateView, CreateView, DeleteView, UpdateView +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, Http404 +from django.views.generic import View, TemplateView, CreateView, DeleteView, UpdateView +from django.views.generic.detail import SingleObjectMixin from django.conf import settings from django.db import models from django.shortcuts import resolve_url @@ -17,6 +18,7 @@ except ImportError: from passerelle.base.models import BaseResource +from .utils import response_for_json from .forms import GenericConnectorForm @@ -76,8 +78,11 @@ class ManageAddView(TemplateView): class GenericConnectorMixin(object): + def get_connector(self, **kwargs): + return kwargs.get('connector') + def dispatch(self, request, *args, **kwargs): - connector = kwargs.get('connector') + connector = self.get_connector(**kwargs) try: app = apps.get_app_config(connector.replace('-', '_')) except LookupError: @@ -107,6 +112,22 @@ class GenericDeleteConnectorView(GenericConnectorMixin, DeleteView): return reverse('manage-home') +class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View): + def get_params(self, request, *args, **kwargs): + return dict([(x, request.GET[x]) for x in request.GET.keys()]) + + def get(self, request, *args, **kwargs): + endpoint = getattr(self.get_object(), kwargs.get('endpoint'), None) + endpoint_info = getattr(endpoint, 'endpoint_info', None) + if endpoint_info is None: + raise Http404() + try: + result = endpoint(request, **self.get_params(request, *args, **kwargs)) + except TypeError as e: + return HttpResponseBadRequest(unicode(e)) + return response_for_json(request, result) + + # legacy LEGACY_APPS_PATTERNS = { 'datasources': {'url': 'data', 'name': 'Data Sources'}, -- 2.8.1