Django-Views nach HTTP-Accept-Header auswählen

Beim Design von Software nach den Prinzipien des REST wird die Antwort auf einen HTTP-Request als Representation einer Resource angesehen. Diese Resource wird durch eine URI bezeichnet und kann unterschiedliche Representationen haben. Wenn man also die Informationen einer URI in mehreren Formaten anbietet, erstellt man praktisch einen RESTful-Webservice (read-only) und bedient so nicht nur das fuer Menschen lesbare Web, sondern auch Maschinen können etwas mit den Inhalten anfangen. Mashups und Aggregation steht so nichts mehr im Wege.

Es wäre moeglich, für jede Art von Response-Typ eine eigene URL anzubieten. Allerdings handelt es sich um eine einzelne Resource, die eindeutig über eine URL zu erreichen sein sollte. Ebenfalls möglich aber etwas unelegant ist die Auswahl über URL-Parameter. Zum Beispiel www.beispiel.de/anzeige.php?format=json oder www.beispiel.de/anzeige.php?format=xml
Das HTTProtokol bietet dem Client jedoch eine Möglichkeit zu bestimmen welche Response-Typen er erwartet. Im Header ‘HTTP_ACCEPT’ kann dazu eine Liste Mimetypes übergeben werden.

Zum Beispiel koennte man sich denken, das eine URL die Abrufstatistik einer Webseite darstellt. www.beispiel.de/stat/jan/ Hier koennte nun wenn per Header HTML (text/html) angefordert wird, eine Seite zuruekgegeben werden, die sowohl eine textuelle oder tabellarische Darstellung der Daten enthaelt als auch eine Grafik zur Visualisierung. Wird aber ein Header geschickt, der eine Bilddatei anfordert, zum Beispiel image/png oder image/svg, wuerde vom Webserver nur mit einer grafischen Darstellung geantwortet. Falls ein Datenformat wie text/xml, text/comma-separated-values oder application/json verlangt wird, wuerden die reinen Daten -entsprechend formatiert- zur automatischen Weiterverarbeitung bereitgestellt.

Ruby on Rails bietet in der aktuellen Version Funktionaliät um abhängig vom Accept-Header einen entsprechenden View auszugeben. Bei Django wird dies nicht mitgeliefert, allerdings ist es unaufwändig einen ähnlichen Mechanismus zu implementieren.

Anhand eines Beispiels ist leicht nachzuvollziehen, wie dies funktioniert.

Angenommen, es sollten die letzten Login-Zeiten der User einer Django -Anwendung als eine Resource im Web angeboten werden. Der ORM von Django stellt die User einer Anwendung über die Klasse User zur verfügung. Eine Liste von User-Objekten lässt sich über User.objects.all() abrufen.

Die einfachste Möglichkeit den Client das Ausgabeformat wählen zu lassen, ist über eine If-Konstruktion:

from django.shortcuts import render_to_response
from django.http import HttpResponse
from django.utils import simplejson

def lastLogin( request ):
accepted_headers = [a.split(';')[0] for a in request.META['HTTP_ACCEPT'].split(',')]

if 'text/html' in accepted_headers:
users = User.objects.all()
return render_to_response(  'last_login_list.html', { users : users } )

if 'application/json' in accepted_headers:
udict = {}
for usr in User.objects.all():
udict[ usr.name ] = usr.lastlogin
return HttpResponse(content=simplejson.dumps(udict), mimetype='application/json')

In der ersten Zeile der View-Funktion wird der vom Browser gesendete Accept-Header zerlegt. Dieser Header ist ein String, der ungefähr so aussehen kann:
“text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5″
Dies ist eine von Kommata separierte Liste der akzeptierten Mimetypes. Hier sind Typ und Subtyp von einem Schrägstrich getrennt. Danach können hinter einem Semikolon weitere, optionale Parameter folgen. Relevant sind für dieses Beispiel nur die Typinformationen, deshalb brechen wir in Zeile 6 den String anhand der Kommata auf ( request.META['HTTP_ACCEPT'].split(‘,’) ) und verwerfen jeweils den Teil nach dem Semikolon ( a.split(‘;’)[0] )
Jetzt würde die Liste mit den MimeTypen so ausehen:
accepted_headers = [ 'text/xml', 'application/xml', 'application/xhtml+xml' , 'text/html', 'text/plain', 'image/png', '*/*' ]

In den folgenden Zeilen wird erst geprüft ob der Client eine HTML-Seite angefordert hat, in diesem Fall wird einem Template ( ‘last_login_list.html’ ) eine Liste mit User-Objekten übergeben, denen der Template-Autor nach belieben Informationen entehmen kann.
Falls vom Browser eine Antwort vom Typ application/json angefordert wurde, wird nun ein Dictionary (a.k.a. Hashmap, Referezielles Array) zusammengebaut (Zeilen 13-15), in eine JSON-Datenstruktur umgewandelt (Zeile 16) und an den Browser geschickt. ( return HttpResponse… )

Eigentlich ist hier das Ziel bereits erreicht. Allerdings muss die erste Zeile der View-Funktion in jeder View wiederholt werden. Dies verletzt das DRY -Prinzip und macht Aenderungen unnoetig kompliziert.

Zweite Ausbaustufe und Django Middleware

In Django gibt es die Möglichkeit, das Objekt ‘request’, das der View-Funktion übergeben wird automatisch zu manipulieren. So kann das Aufteilen des Accept-Header-Strings in eine praktischere Liste aus dem View-Code herausgenommen und zentralisiert (refaktorisiert ) werden.

Hierzu wird eine Python-Klasse geschrieben und in das Django-Middleware-Framework eingehängt.

Django verarbeitet HTTP-Requests, indem es einer Python-Funktion , einer View, ein Objekt übergibt, das die nötigen Informationen enthält. Man kann dieses Objekt über das MiddleWare-Framework abfangen und vor der Weitergabe an die View manipulieren. Hierzu wird eine Klasse angelegt, welche eine Methode process_request definiert, die ein HttpRequest-Objekt übernimmt. Um die Klasse in das Middleware-Framework einzhängen muss dem Tuple MIDDLEWARE_CLASSES in der Datei settings.py ein String angehängt werden, der Package und Klassenname referenziert. Falls sich die Middlewareklasse also in einer Datei middleware.py im Project-Root-Verzeichnis befindet, könnte es in der settings.py (Zeile 6) so aussehen:


MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.doc.XViewMiddleware',
'myproject.middleware.AcceptMiddleware',
)

In Zeile 9 bekommt das HttpRequest-Objekt die zusätzliche Eigenschaft ‘accepted_types’. Desweiteren wird eine Funktion accepts definiert, die einen String übernimmt und prüft ob sich dieser in den accepted_types des requests wiederfindet. In Zeile 10 wird diese Funktion dynamisch als eine neue Methode des Objekts request angehängt. Interessant hierbei ist, das es die Methode tatsächlich zur Laufzeit dem Objekt ohne Umweg über die Klassendefinition oder Vererbung zugefügt wird.

import new

def accepts( self, mime ):
return mime in self.accepted_types

class AcceptMiddleware(object):
def process_request(self, request):
acc = [a.split(';')[0] for a in request.META['HTTP_ACCEPT'].split(',')]
setattr(request, 'accepted_types', acc )
request.accepts = new.instancemethod(accepts, request, request.__class__)
return None</pre>

Auf request.accepted_types und request.accepts() kann nun in jeder View-Funktion zugegriffen werden.
Dies erlaubt uns nun das erste Codebeispiel wie folgt zu refaktorisieren:

from django.shortcuts import render_to_response
from django.http import HttpResponse
from django.utils import simplejson

def lastLogin( request ):
if request.accepts('text/html'):
users = User.objects.all()
return render_to_response(  'last_login_list.html', { users : users } )

if request.accepts('application/json'):
udict = {}
for usr in User.objects.all():
udict[ usr.name ] = usr.lastlogin
return HttpResponse(content=simplejson.dumps(udict), mimetype='application/json')

Diese Loesung ist gut lesbar und erlaubt es auf einfache Art und Weise uebersichtlichen Code zu schreiben.

Tags: , , ,