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