Eine andere Möglichkeit GET und POST in Django zu verarbeiten

Letztens bin ich über einen Blog-Eintrag von Dan Fairs gestolpert, in dem er eine Lösung anbietet, welche das Problem sauber lösen soll, dass man sehr oft eine if-else Konstruktion in den Views braucht um GET und POST Anfragen unterschiedlich zu behandeln.

Er ersetzt dazu die bisherigen View Funktionen gegen eine Klasse, in welcher verschiedene Methoden mit @get, @post etc. dekoriert werden um die entsprechende Anfrage zu verarbeiten. Spontan dachte ich dabei an web.py, ein sehr minimalistisches Python Web-Framework, welches eigentlich genauso arbeitet. URLs werden auf Klassen gemappt und dort gibt es pro HTTP-Methode eine Methode die diese Anfrage verarbeitet. Statt jedoch (umständlich) mit Dekoratoren zu arbeiten, gibt es einfach die Konvention, dass die Methode, welche z.B. HTTP-GET-Anfragen verarbeitet GET() heißt.

Kombiniert man die beiden Ansätze gelange ich zu folgender Lösung:

Zuerst definiere ich eine Basis-Klasse für alle späteren View-Klassen, in diesem Fall die Klasse BaseView:

from django.http import Http404

class BaseView(object):
    ALLOWED_HTTP_METHODS = ('get', 'post')
    def __call__(self, request, *args, **kwargs):
        if request.method.lower() not in self.ALLOWED_HTTP_METHODS:
            raise Http404 #should be 405 ...
        if not hasattr(self, request.method.lower()):
            raise Http404
        return getattr(self, request.method.lower())(request, *args, **kwargs)

In diesem Beispiel verarbeite ich nur GET und POST Anfragen, weitere HTTP-Methoden (z.B. HEAD) hinzuzufügen ist trivial, man trägt einfach den Namen in Kleinbuchstaben in das Tupel ALLOWED_HTTP_METHODS ein.

Der nächste Schritt ist jetzt eine View-Klasse zu schreiben, welche die tatsächlichen Anfragen verarbeitet. Diese Klasse erbt von der BaseView Klasse und ich nenne sie beispielhaft FooView:

from somewhere import BaseView
from django.http import HttpResponse, HttpResponseRedirect

class FooView(BaseView):
    def get(self, request, id):
        #do something
        return HttpResponse('get %s' % id)

    def post(self, request, id):
        #do post processing
        return HttpResponseRedirect('/')

Die Klasse FooView definiert zwei Methoden: get() und post(), d.h. sowohl HTTP-GET als auch HTTP-POST Anfragen werden angenommen. Würde es eine der Methoden nicht geben, würde der Client einen 404 Fehler bekommen (Implementiert in der BaseView Klasse). Eigentlich sollte man hier zwar einen HTTP 405 Method Not Allowed Status zurückgeben, aber der Einfachheit halber habe ich die Http404 Exception von Django benutzt. Einen 405er zu Implementieren bleibt Aufgabe für den Leser.

Nachdem nun die View-Klasse fertig ist folgt noch die Konfiguration in der urls.py um die Anfragen auch an diese Klasse zu leiten:

from django.conf.urls.defaults import *
from somewhere import FooView

urlpatterns = patterns('',
     (r'^test/(?P[\d]+)/', FooView()),
)

Der Code sollte eigentlich selbsterklärend sein. Alle Anfragen an "test/id/", wobei id eine Zahl ist, werden an eine Instanz der FooView-Klasse gesendet. Da die Klasse eine __call__-Methode definiert ist sie callable und kann wie eine Funktion aufgerufen werden. Diese Funktionalität ist in der BaseView-Klasse implementiert. Der id-Parameter im URL-Muster ist ein Beispiel, damit man sieht, dass diese Paramter wie bisher an der Funktion ankommen, die die Anfrage verarbeitet.

So sieht also meine Lösung für das Problem aus dem Blog-Eintrag von Dan aus. Ich finde diese Lösung etwas schöner, weil sie mit deutlich weniger Code auskommt, aber vielleicht habe ich auch etwas übersehen. Einen kleinen Bonus gibt es aber noch. Man strebt ja immer das Don't-Repeat-Yourself (DRY) Prinzip an und auch wenn ich keinen konreten Einsatzzweck habe, so gibt es doch eine Möglichkeit, wie diese Lösung mit View-Klassen DRY unterstützen könnte.

Man könnte die View-Klasse mit einem Konstruktor versehen, welcher die Klasse nach unterschiedlichen Vorgaben initialisiert, so dass man eine View-Klasse für mehrere ähnliche Views benutzen könnte. Ein Beispiel (wenn auch ein sinnloses) verdeutlich wohl am besten was ich meine. Zuerst bekommt die FooView-Klasse einen Konstruktor:

class FooView(BaseView):
    def __init__(self, foobar):
        self.foobar = foobar
    ...

Jetzt kann man in der urls.py mehrere Instanzen der View-Klasse verwenden, jeweils mit unterschiedlichen Werten initialisiert:

from django.conf.urls.defaults import *
from somewhere import FooView

urlpatterns = patterns('',
     (r'^test/(?P[\d]+)/', FooView(foobar='test')),
     (r'^example/(?P[\d]+)/', FooView(foobar='example')),
)

Und um das Beispiel zu vervollständigen, hier eine leicht geänderte Fassung der get()-Methode:

def get(self, request, id):
    #do something
    return HttpResponse('get %s - %s' % (id, self.foobar))

Diese get()-Methode benutzt nun den Wert, mit welchem die Klasse in der ` urls.py initialisiert wurde. Ich gebe zu das Beispiel ist etwas zu trivial, aber wenn man die View-Klasse relativ generisch hällt und die Konfiguration in der urls.py macht, kann man sich sehr leicht eine Reihe von Klasse bauen, die ähnlich wie generic-views, für oft wiederkehrende Aufgaben geeignet sind und außerdem eine schöne Trennung zwischen den unterschiedlichen HTTP-Methoden ermöglichen.


Kommentare