general: add generic endpoint view (#11204)

This commit is contained in:
Frédéric Péters 2016-06-05 10:00:59 +02:00
parent b2be2c604f
commit e644b3cfc8
29 changed files with 252 additions and 239 deletions

View File

@ -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

View File

@ -19,10 +19,10 @@ format.
</p>
<ul>
<li>{% trans 'Geocoding:' %} <a href="{% url 'base_adresse-search' slug=object.slug %}?q=169 rue du chateau, paris&format=json"
>{{ site_base_uri }}{% url 'base_adresse-search' slug=object.slug %}</a>?q=<i>q</i>&amp;format=json</li>
<li>{% trans 'Reverse geocoding:' %} <a href="{% url 'base_adresse-reverse' slug=object.slug %}?lat=48.833708&amp;lon=2.323349&amp;format=json"
>{{ site_base_uri }}{% url 'base_adresse-reverse' slug=object.slug %}</a>?lat=<i>lat</i>&amp;lon=<i>lon</i>&amp;format=json</li>
<li>{% trans 'Geocoding:' %} <a href="search?q=169 rue du chateau, paris&format=json"
>{{ site_base_uri }}{{ object.get_absolute_url }}search</a>?q=<i>q</i>&amp;format=json</li>
<li>{% trans 'Reverse geocoding:' %} <a href="reverse?lat=48.833708&amp;lon=2.323349&amp;format=json"
>{{ site_base_uri }}{{ object.get_absolute_url }}reverse</a>?lat=<i>lat</i>&amp;lon=<i>lon</i>&amp;format=json</li>
</ul>
</div>

View File

@ -7,8 +7,5 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', BaseAdresseDetailView.as_view(), name='base_adresse-view'),
url(r'^(?P<slug>[\w,-]+)/search$', SearchView.as_view(), name='base_adresse-search'),
url(r'^(?P<slug>[\w,-]+)/search/(?P<path>.*)$', SearchPathView.as_view(), name='base_adresse-path-search'),
url(r'^(?P<slug>[\w,-]+)/reverse$', ReverseView.as_view(), name='base_adresse-reverse'),
)

View File

@ -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
class SearchPathView(GenericEndpointView):
def get_connector(self, **kwargs):
return 'base_adresse'
def get_query(self, request, *args, **kwargs):
if not 'q' in request.GET:
return HttpResponseBadRequest('missing parameter')
return request.GET['q']
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()
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
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(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)

View File

@ -36,7 +36,7 @@ class ChoositSMSGateway(BaseResource, SMSGatewayMixin):
def get_absolute_url(self):
return reverse('choosit-view', kwargs={'slug': self.slug})
def send(self, text, sender, destinations):
def send_msg(self, text, sender, destinations):
"""Send a SMS using the Choosit provider"""
# from http://sms.choosit.com/documentation_technique.html
# unfortunately it lacks a batch API...
@ -146,4 +146,3 @@ class ChoositRegisterGateway(BaseResource):
reg = ChoositRegisterWS(self.url, self.key)
ws = reg.update(subscriptions, user)
return {"message": ws['status']}

View File

@ -3,7 +3,6 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', ChoositDetailView.as_view(), name='choosit-view'),
url(r'^(?P<slug>[\w,-]+)/send$', ChoositSendView.as_view(), name='choosit-send'),
url(r'^register/(?P<slug>[\w,-]+)/$', ChoositRegisterDetailView.as_view(), name='choosit-register-view'),
url(r'^register/(?P<slug>[\w,-]+)/register$', ChoositRegisterView.as_view(), name='choosit-register'),
)

View File

@ -7,7 +7,6 @@ from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from passerelle.base.views import ResourceView
from passerelle.sms.views import SendView
from passerelle import utils
from .models import ChoositSMSGateway, ChoositRegisterGateway
@ -19,10 +18,6 @@ class ChoositDetailView(ResourceView):
template_name = 'passerelle/manage/messages_service_view.html'
class ChoositSendView(SendView):
model = ChoositSMSGateway
class ChoositRegisterDetailView(ResourceView):
model = ChoositRegisterGateway
template_name = 'choosit/choosit_register_detail.html'

View File

@ -1,8 +1,23 @@
try:
import SOAPpy
except ImportError:
SOAPpy = None
try:
import phpserialize
except ImportError:
phpserialize = None
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
def phpserialize_loads(s):
return phpserialize.loads(s.encode('utf-8'))
class Gdc(BaseResource):
@ -21,3 +36,23 @@ class Gdc(BaseResource):
@classmethod
def get_icon_class(cls):
return 'ressources'
@endpoint()
def communes(self, request, *args, **kwargs):
server = SOAPpy.SOAPProxy(self.service_url)
soap_result = phpserialize_loads(server.getListeCommune()['listeCommune'])
result = []
for k, v in soap_result.items():
result.append({'id': k, 'text': unicode(v, 'utf-8')})
result.sort(lambda x,y: cmp(x['id'], y['id']))
return result
@endpoint()
def objets(self, request, *args, **kwargs):
server = SOAPpy.SOAPProxy(self.service_url)
soap_result = phpserialize_loads(server.getListeObjet()['listeObjet'])
result = []
for k, v in soap_result.items():
result.append({'id': k, 'text': unicode(v, 'utf-8')})
result.sort(lambda x,y: cmp(x['id'], y['id']))
return result

View File

@ -19,12 +19,12 @@ Service URL : {{ object.service_url }}
<div>
<h3>{% trans 'Endpoints' %}</h3>
<ul>
<li>{% trans 'Listing communes:' %} <a href="{% url 'gdc-communes' slug=object.slug %}"
>{{ site_base_uri }}{% url 'gdc-communes' slug=object.slug %}</a></li>
<li>{% trans 'Listing communes:' %} <a href="{% url 'generic-endpoint' connector="gdc" slug=object.slug endpoint="communes" %}"
>{{ site_base_uri }}{% url 'generic-endpoint' connector="gdc" slug=object.slug endpoint="communes" %}</a></li>
<li>{% trans 'Listing streets:' %} <a href="{% url 'gdc-voies' slug=object.slug insee=34022%}"
>{{ site_base_uri }}{% url 'gdc-view' slug=object.slug %}/voies/<i>&lt;insee&gt;</i></a></li>
<li>{% trans 'Listing subjects:' %} <a href="{% url 'gdc-objets' slug=object.slug %}"
>{{ site_base_uri }}{% url 'gdc-objets' slug=object.slug %}</a></li>
<li>{% trans 'Listing subjects:' %} <a href="{% url 'generic-endpoint' connector="gdc" slug=object.slug endpoint="objets" %}"
>{{ site_base_uri }}{% url 'generic-endpoint' connector="gdc" slug=object.slug endpoint="objets" %}</a></li>
<li>{% trans 'Posting a new request:' %} <a href="{% url 'gdc-post' slug=object.slug %}"
>{{ site_base_uri }}{% url 'gdc-post' slug=object.slug %}</a> (POST)</li>
</ul>

View File

@ -5,9 +5,7 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', GdcDetailView.as_view(), name='gdc-view'),
url(r'^(?P<slug>[\w,-]+)/communes$', CommunesView.as_view(), name='gdc-communes'),
url(r'^(?P<slug>[\w,-]+)/voies/(?P<insee>\d+)$', VoiesView.as_view(), name='gdc-voies'),
url(r'^(?P<slug>[\w,-]+)/objets$', ObjetsView.as_view(), name='gdc-objets'),
url(r'^(?P<slug>[\w,-]+)/post/demande$', csrf_exempt(PostDemandeView.as_view()), name='gdc-post'),
url(r'^(?P<slug>[\w,-]+)/status/(?P<ref>\d+)', StatusView.as_view(), name='gdc-status'),
)

View File

@ -1,58 +1,14 @@
import json
import unicodedata
try:
import SOAPpy
except ImportError:
SOAPpy = None
try:
import phpserialize
except ImportError:
phpserialize = None
from django.http import Http404
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin, DetailView
from passerelle import utils
from .models import Gdc
from .models import Gdc, phpserialize, phpserialize_loads, SOAPpy
def phpserialize_loads(s):
if phpserialize is None:
raise Http404
return phpserialize.loads(s.encode('utf-8'))
class CommunesView(View, SingleObjectMixin):
model = Gdc
def get(self, request, *args, **kwargs):
if SOAPpy is None:
raise Http404
server = SOAPpy.SOAPProxy(self.get_object().service_url)
soap_result = phpserialize_loads(server.getListeCommune()['listeCommune'])
result = []
for k, v in soap_result.items():
result.append({'id': k, 'text': unicode(v, 'utf-8')})
result.sort(lambda x,y: cmp(x['id'], y['id']))
return utils.response_for_json(request, {'data': result})
class ObjetsView(View, SingleObjectMixin):
model = Gdc
def get(self, request, *args, **kwargs):
if SOAPpy is None:
raise Http404
server = SOAPpy.SOAPProxy(self.get_object().service_url)
soap_result = phpserialize_loads(server.getListeObjet()['listeObjet'])
result = []
for k, v in soap_result.items():
result.append({'id': k, 'text': unicode(v, 'utf-8')})
result.sort(lambda x,y: cmp(x['id'], y['id']))
return utils.response_for_json(request, {'data': result})
class StatusView(View, SingleObjectMixin):

View File

@ -50,7 +50,7 @@ class MobytSMSGateway(BaseResource, SMSGatewayMixin):
def get_delete_url(self):
return reverse('delete-connector', kwargs={'connector': 'mobyt', 'slug': self.slug})
def send(self, text, sender, destinations):
def send_msg(self, text, sender, destinations):
"""Send a SMS using the Mobyt provider"""
# unfortunately it lacks a batch API...
destinations = self.clean_numbers(destinations, self.default_country_code)

View File

@ -3,5 +3,4 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', MobytDetailView.as_view(), name='mobyt-view'),
url(r'^(?P<slug>[\w,-]+)/send$', MobytSendView.as_view(), name='mobyt-send'),
)

View File

@ -1,5 +1,4 @@
from passerelle.base.views import ResourceView
from passerelle.sms.views import SendView
from .models import MobytSMSGateway
@ -7,7 +6,3 @@ from .models import MobytSMSGateway
class MobytDetailView(ResourceView):
model = MobytSMSGateway
template_name = 'passerelle/manage/messages_service_view.html'
class MobytSendView(SendView):
model = MobytSMSGateway

View File

@ -41,7 +41,7 @@ class OrangeSMSGateway(BaseResource, SMSGatewayMixin):
def get_delete_url(self):
return reverse('delete-connector', kwargs={'connector': 'orange', 'slug': self.slug})
def send(self, text, sender, destinations):
def send_msg(self, text, sender, destinations):
logger = logging.getLogger('passerelle.apps.orange')
"""Send a SMS using the Orange provider"""
# unfortunately it lacks a batch API...
@ -49,4 +49,4 @@ class OrangeSMSGateway(BaseResource, SMSGatewayMixin):
logger.info(u'sending sms with sender %s to %s', sender, u', '.join(destinations))
logger.debug(u'sms content: %s', text)
soap.ContactEveryoneSoap(instance=self).send_advanced_message(destinations, sender, text)
#soap.ContactEveryoneSoap(instance=self).send_advanced_message(destinations, sender, text)

View File

@ -3,5 +3,4 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', OrangeDetailView.as_view(), name='orange-view'),
url(r'^(?P<slug>[\w,-]+)/send$', OrangeSendView.as_view(), name='orange-send'),
)

View File

@ -1,5 +1,4 @@
from passerelle.base.views import ResourceView
from passerelle.sms.views import SendView
from .models import OrangeSMSGateway
@ -7,7 +6,3 @@ from .models import OrangeSMSGateway
class OrangeDetailView(ResourceView):
model = OrangeSMSGateway
template_name = 'passerelle/manage/messages_service_view.html'
class OrangeSendView(SendView):
model = OrangeSMSGateway

View File

@ -56,7 +56,7 @@ class OVHSMSGateway(BaseResource, SMSGatewayMixin):
def get_delete_url(self):
return reverse('delete-connector', kwargs={'connector': 'ovh', 'slug': self.slug})
def send(self, text, sender, destinations):
def send_msg(self, text, sender, destinations):
"""Send a SMS using the OVH provider"""
# unfortunately it lacks a batch API...
destinations = self.clean_numbers(destinations, self.default_country_code)

View File

@ -3,5 +3,4 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', OvhDetailView.as_view(), name='ovh-view'),
url(r'^(?P<slug>[\w,-]+)/send$', OvhSendView.as_view(), name='ovh-send'),
)

View File

@ -1,5 +1,4 @@
from passerelle.base.views import ResourceView
from passerelle.sms.views import SendView
from .models import OVHSMSGateway
@ -7,7 +6,3 @@ from .models import OVHSMSGateway
class OvhDetailView(ResourceView):
model = OVHSMSGateway
template_name = 'passerelle/manage/messages_service_view.html'
class OvhSendView(SendView):
model = OVHSMSGateway

View File

@ -38,7 +38,7 @@ class OxydSMSGateway(BaseResource, SMSGatewayMixin):
def get_delete_url(self):
return reverse('delete-connector', kwargs={'connector': 'oxyd', 'slug': self.slug})
def send(self, text, sender, destinations):
def send_msg(self, text, sender, destinations):
"""Send a SMS using the Oxyd provider"""
# unfortunately it lacks a batch API...
destinations = self.clean_numbers(destinations,

View File

@ -3,5 +3,4 @@ from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', OxydDetailView.as_view(), name='oxyd-view'),
url(r'^(?P<slug>[\w,-]+)/send$', OxydSendView.as_view(), name='oxyd-send'),
)

View File

@ -1,5 +1,4 @@
from passerelle.base.views import ResourceView
from passerelle.sms.views import SendView
from .models import OxydSMSGateway
@ -7,7 +6,3 @@ from .models import OxydSMSGateway
class OxydDetailView(ResourceView):
model = OxydSMSGateway
template_name = 'passerelle/manage/messages_service_view.html'
class OxydSendView(SendView):
model = OxydSMSGateway

View File

@ -1,6 +1,9 @@
import json
import logging
import re
from django.utils.translation import ugettext_lazy as _
from passerelle.utils.api import endpoint
class SMSGatewayMixin(object):
category = _('SMS Providers')
@ -23,3 +26,18 @@ class SMSGatewayMixin(object):
numbers.append(prefix + number)
return numbers
@endpoint('json-api', perm='can_send_messages', methods=['post'])
def send(self, request, *args, **kwargs):
data = json.loads(request.body)
assert isinstance(data, dict), 'JSON payload is not a dict'
assert 'message' in data, 'missing "message" in JSON payload'
assert 'from' in data, 'missing "from" in JSON payload'
assert 'to' in data, 'missing "to" in JSON payload'
assert isinstance(data['message'], unicode), 'message is not a string'
assert isinstance(data['from'], unicode), 'from is not a string'
assert all(map(lambda x: isinstance(x, unicode), data['to'])), \
'to is not a list of strings'
logging.info('sending message %r to %r with sending number %r',
data['message'], data['to'], data['from'])
self.send_msg(data['message'], data['from'], data['to'])
return {'err': 0}

View File

@ -1,30 +0,0 @@
import logging
import json
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin
from passerelle import utils
class SendView(View, SingleObjectMixin):
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super(SendView, self).dispatch(*args, **kwargs)
@utils.to_json('api')
@utils.protected_api('can_send_messages')
def post(self, request, *args, **kwargs):
data = json.loads(request.body)
assert isinstance(data, dict), 'JSON payload is not a dict'
assert 'message' in data, 'missing "message" in JSON payload'
assert 'from' in data, 'missing "from" in JSON payload'
assert 'to' in data, 'missing "to" in JSON payload'
assert isinstance(data['message'], unicode), 'message is not a string'
assert isinstance(data['from'], unicode), 'from is not a string'
assert all(map(lambda x: isinstance(x, unicode), data['to'])), \
'to is not a list of strings'
logging.info('sending message %r to %r with sending number %r',
data['message'], data['to'], data['from'])
return self.get_object().send(data['message'], data['from'], data['to'])

View File

@ -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
@ -84,5 +84,10 @@ urlpatterns += patterns('',
GenericEditConnectorView.as_view(), name='edit-connector'),
)))))
urlpatterns += patterns('',
url(r'^(?P<connector>[\w,-]+)/(?P<slug>[\w,-]+)/(?P<endpoint>[\w,-]+)$',
GenericEndpointView.as_view(), name='generic-endpoint')
)
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()

25
passerelle/utils/api.py Normal file
View File

@ -0,0 +1,25 @@
# 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 <http://www.gnu.org/licenses/>.
class endpoint(object):
def __init__(self, serializer_type='json', perm=None, methods=['get']):
self.perm = perm
self.methods = methods
self.serializer_type = serializer_type
def __call__(self, func):
func.endpoint_info = self
return func

View File

@ -1,8 +1,11 @@
from django.apps import apps
from django.core.exceptions import PermissionDenied
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 RedirectView, TemplateView, CreateView, DeleteView, UpdateView
from django.http import HttpResponseBadRequest, HttpResponseRedirect, Http404
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import RedirectView, 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 +20,7 @@ except ImportError:
from passerelle.base.models import BaseResource
from .utils import to_json, response_for_json, is_authorized
from .forms import GenericConnectorForm
@ -68,8 +72,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:
@ -99,6 +106,49 @@ 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()])
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(GenericEndpointView, self).dispatch(request, *args, **kwargs)
@property
def endpoint(self):
endpoint = getattr(self.get_object(), self.kwargs.get('endpoint'), None)
endpoint_info = getattr(endpoint, 'endpoint_info', None)
if endpoint_info is None:
raise Http404()
return endpoint
def _allowed_methods(self):
return [x.upper() for x in self.endpoint.endpoint_info.methods]
def check_perms(self, request):
perm = self.endpoint.endpoint_info.perm
if not perm:
return True
return is_authorized(request, self.get_object(), perm)
def perform(self, request, *args, **kwargs):
if request.method.lower() not in self.endpoint.endpoint_info.methods:
return self.http_method_not_allowed(request, *args, **kwargs)
if not self.check_perms(request):
raise PermissionDenied()
return self.endpoint(request, **self.get_params(request, *args, **kwargs))
def get(self, request, *args, **kwargs):
if self.endpoint.endpoint_info.serializer_type == 'json-api':
json_decorator = to_json('api')
else:
json_decorator = to_json('plain')
return json_decorator(self.perform)(request, **self.get_params(request, *args, **kwargs))
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
# legacy
LEGACY_APPS_PATTERNS = {
'datasources': {'url': 'data', 'name': 'Data Sources'},

View File

@ -27,8 +27,9 @@ def setup():
def test_anonymous_access(setup):
app, oxyd = setup
resp = app.post_json(reverse('oxyd-send', kwargs={'slug': oxyd.slug}),
{}, status=403)
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
resp = app.post_json(endpoint_url, {}, status=403)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
@ -46,7 +47,9 @@ def test_access_with_good_signature(setup):
resource_type=obj_type,
resource_pk=oxyd.pk,
)
url = signature.sign_url(reverse('oxyd-send', kwargs={'slug': oxyd.slug}) + '?orig=eservices', '12345')
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
url = signature.sign_url(endpoint_url + '?orig=eservices', '12345')
# for empty payload the connector returns 500 with
# {"err_desc": "missing \"message\" in JSON payload"}
resp = app.post_json(url, {}, status=500)
@ -70,8 +73,9 @@ def test_access_http_auth(setup):
resource_pk=oxyd.pk,
)
app.authorization = ('Basic', (username, password))
resp = app.post_json(reverse('oxyd-send', kwargs={'slug': oxyd.slug}), {},
status=500)
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
resp = app.post_json(endpoint_url, {}, status=500)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'missing "message" in JSON payload'
@ -91,11 +95,12 @@ def test_access_apikey(setup):
resource_pk=oxyd.pk,
)
params = {'message': 'test'}
url = reverse('oxyd-send', kwargs={'slug': oxyd.slug})
resp = app.post_json(url + '?apikey=' + password , params, status=500)
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
resp = app.post_json(endpoint_url + '?apikey=' + password , params, status=500)
resp.json['err'] == 1
assert resp.json['err_desc'] == 'missing "from" in JSON payload'
resp = app.post_json(url + '?apikey=' + password[:3] , params, status=403)
resp = app.post_json(endpoint_url + '?apikey=' + password[:3] , params, status=403)
resp.json['err'] == 1
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
@ -112,8 +117,9 @@ def test_access_apiuser_with_no_key(setup):
resource_pk=oxyd.pk,
)
params = {'message': 'test', 'from': 'test api'}
resp = app.post_json(reverse('oxyd-send', kwargs={'slug': oxyd.slug}),
params, status=500)
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
resp = app.post_json(endpoint_url, params, status=500)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'missing "to" in JSON payload'
@ -132,14 +138,16 @@ def test_access_apiuser_with_ip_restriction(setup):
resource_type=obj_type,
resource_pk=oxyd.pk,
)
resp = app.post_json(reverse('oxyd-send', kwargs={'slug': oxyd.slug}),
{}, extra_environ=[('REMOTE_ADDR', '127.0.0.1')],
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
resp = app.post_json(endpoint_url, {}, extra_environ=[('REMOTE_ADDR', '127.0.0.1')],
status=403)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
resp = app.post_json(reverse('oxyd-send', kwargs={'slug': oxyd.slug}),
{}, extra_environ=[('REMOTE_ADDR', authorized_ip)],
status=500)
endpoint_url = reverse('generic-endpoint',
kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
resp = app.post_json(endpoint_url, {},
extra_environ=[('REMOTE_ADDR', authorized_ip)], status=500)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'missing "message" in JSON payload'