from django.urls import path
from django.http import JsonResponse, HttpResponseNotAllowed
from django.shortcuts import render
from django.conf import settings
from banjo import http
from banjo import views
from banjo.forms import ApiRouteForm
import json
urlpatterns = [
path(settings.API_PREFIX, views.api, name="api"),
path(settings.API_PREFIX + ".json", views.api_json, name="api_json"),
]
user_defined_routes = []
[docs]
class JsonRequiresDict(TypeError):
pass
[docs]
def validate_args(args):
"""Checks that args (the param type signature) is valid.
"""
if args is None: return
allowed_types = [str, bool, int, float]
if not isinstance(args, dict):
raise ValueError("args must be a dict if provided")
for param, type_ in args.items():
if type_ not in allowed_types:
raise ValueError("param {} in args must be one of: {}".format(
param, ', '.join(str(t) for t in allowed_types)))
[docs]
def create_view(fn, method, args=None):
"""Creates a django view function.
The view function checks that the HTTP method is correct,
extracts the request's params, passes them to ``fn``,
and returns the result. Args, if provided, should be
a dict of ``{param_name: type}``, where type is in
``[str, bool, int, float]``.
"""
method = method.upper()
def view(request):
if request.method != method:
return HttpResponseNotAllowed([method])
if args:
if request.headers.get('Content-Type') == 'application/json':
data = json.loads(request.body.decode("utf-8"))
elif method == 'GET':
data = request.GET
else:
data = request.POST
FormClass = ApiRouteForm.for_args(args)
form = FormClass(data)
if form.is_valid():
try:
result = fn(form.cleaned_data)
if not isinstance(result, dict):
raise JsonRequiresDict(f"Banjo views must return a dict, not {result}.")
return JsonResponse(result)
except http.BadRequest as e:
return JsonResponse({'error': str(e)}, status=e.status_code)
else:
return JsonResponse({'errors': form.errors}, status=400)
else:
try:
result = fn({})
if not isinstance(result, dict):
raise JsonRequiresDict(f"Banjo views must return a dict, not {result}.")
return JsonResponse(result)
except http.BadRequest as e:
return JsonResponse({'error': str(e)}, status=e.status_code)
view.__name__ = fn.__name__
view.__doc__ = fn.__doc__
view.method = method
validate_args(args)
view.args = args or {}
return view
[docs]
def create_api_view(url, fn, method, args=None):
"""Creates an API view.
An API view responds to GET requests with a HTML template--
the response data for GET routes and a form for POST routes.
For POST routes, the view processes the form.
"""
method = method.upper()
fn.method = method
def api_view(request):
fn.args = args or {}
route = views.describe_route(path(url, fn, name=fn.__name__))
if method == "GET" and request.method == "GET":
if args:
form = ApiRouteForm.for_args(args)(request.GET)
if form.is_valid():
try:
result = fn(form.cleaned_data)
except http.BadRequest as e:
result = {'error': str(e), 'status_code': e.status_code}
else:
result = None
else:
form = None
try:
result = fn({})
except http.BadRequest as e:
result = {'error': str(e), 'status_code': e.status_code}
return render(request, "banjo/api_get.html", {
"route": route,
"result": json.dumps(result, indent=True) if result else None,
"form": form,
})
if method == "POST":
if request.method == "POST":
if args:
form = ApiRouteForm.for_args(args)(request.POST)
if form.is_valid():
try:
result = fn(form.cleaned_data)
except http.BadRequest as e:
result = {'error': str(e), 'status_code': e.status_code}
form = ApiRouteForm.for_args(args)()
else:
result = None
else:
form = None
try:
result = fn({})
except http.BadRequest as e:
result = {'error': str(e), 'status_code': e.status_code}
else:
if args:
form = ApiRouteForm.for_args(args)()
else:
form = None
result = None
return render(request, "banjo/api_post.html", {
"route": route,
"result": json.dumps(result, indent=True) if result else None,
"form": form,
})
return HttpResponseNotAllowed([request.method])
api_view.__name__ = fn.__name__
api_view.__doc__ = fn.__doc__
validate_args(args)
api_view.args = args or {}
return api_view
[docs]
def route_get(url, args=None):
"""A decorator which registers a HTTP GET route in the Banjo app.
"""
def bind_url_to_view(fn):
view = create_view(fn, "GET", args=args)
api_view = create_api_view(url, fn, "GET", args=args)
urlpatterns.append(path(url, view, name=view.__name__))
urlpatterns.append(path(settings.API_PREFIX + '/' + url, api_view,
name="api_" + view.__name__))
user_defined_routes.append(path(url, view, name=view.__name__))
return view
return bind_url_to_view
[docs]
def route_post(url, args=None):
"""A decorator which registers a HTTP POST route in the Banjo app.
"""
def bind_url_to_view(fn):
view = create_view(fn, "POST", args=args)
api_view = create_api_view(url, fn, "POST", args=args)
urlpatterns.append(path(url, view, name=view.__name__))
urlpatterns.append(path(settings.API_PREFIX + '/' + url, api_view,
name="api_" + view.__name__))
user_defined_routes.append(path(url, view, name=view.__name__))
return view
return bind_url_to_view