Middleware Django

Un middleware c'est quoi?

Nous avons vu précédent comment l'objet request passe de la table de routage d'urls à la vue pour ensuite renvoyer une réponse au client. Ce principe est très simple d'utilisation, peut être même trop. Comment intercepter l'objet request avant qu'il n'atteigne les URL ou la vue? Et bien il existe une solution : les middlewares.

Le middleware est une classe qui contient 5 fonctions, chacune d'entres elles est appelée à un moment précis.

process_request
(request)
est appelée à chaque requête, avant que Django décide quelle vue exécuter. Elle doit retourner un objet HttpResponse (ou None). Si elle retourne None, Django continuera à traiter la requête, exécutant tous les autres process_request() middleware puis process_view() et enfin la vue appropiée. Si elle retourne un objet HttpResponse, Django n'appellera aucune autre requête, vue ou Middleware exception.
process_view
(request, view_func, view_args, view_kwargs)
est appelée juste avant que Django appelle la vue. view_func est la fonction python que Django est sur le point d'utiliser. (Il s'agit de la fonction réelle et non de son nom). View_args est un dictionnaire des mots clé passé à la vue. Si la fonction process_view retourne un HttpResponse, aucune autre fonction middleware ne sera exécutée.
process_template_response (request, response)response est le TemplateResponse (ou équivalent) retournée par la vue Django ou par un middleware. process_template_response est appelée juste après que la vue ait été exécutée. Elle doit retourner une réponse objet contenant une méthode "render".
process_response
(request, response)
est appelée sur toutes les réponses avant qu'elles ne soit retournées au navigateur. C'est la function de la dernière étape du processus.
process_exception
(request, exception)
l'argument exception est un objet d'exception soulevée par la fonction de la vue. process_exception est appelée lorsqu'une vue soulève une exception, celle-ci doit retourner un objet réponse ou None.

Django implémente de base plusieurs middlewares. Vous pouvez voir la liste des middlewares utilisés par Django dans la variable MIDDLEWARE_CLASSES du fichier de configuration de votre projet (settings.py)

La pratique

Bon assez de théorie, passons à la pratique. Voici comment créer un middleware de base:

eboutique/settings.py

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    [...],
    'backoffice.middleware.ClassMiddleware'
)

backoffice/middleware.py

class ClassMiddleware(object):
  
    # exécutée quand Django reçoit une requête et doit décider la vue à utiliser
    def process_request(self, request):
        pass
     
    # exécutée lorsque Django apelle la vue. On peut donc récupérer les arguments de la vue
    # view_func est la fonction Python que Django est sur le point d'utiliser. 
    def process_view(self, request, view_func, view_args, view_kwargs):
        pass
     
    # executée lorsque la vue a levé une exception
    def process_exeption(self, request, exception):
        pass
     
    # La vue a été executée mais pas encore de compilation de template 
    # ( il est encore possible de chager le template )
    def process_template_response(self, request, response):
        return response
     
    # Tout est executée, dernier recours avant le retour client 
    def process_response(self, request, response):
        return response 

Exemple de temps de calcul d'une requête

Un exemple des plus connus est de calculer le temps nécessaire à une requête pour être exécutée.

backoffice/middleware.py

import datetime

class ExecutionTimeMiddleWare(object):
  
    def process_request(self, request):
        pass
     
    def process_view(self, request, view_func, view_args, views_kwargs):
        request.time = datetime.datetime.now()
     
    def process_exeption(self, request, response, view_func, view_args, views_kwargs):
        pass
     
    def process_template_response(self, request, response):
        return response
     
    def process_response(self, request, response):
        if hasattr(request, 'time'):
            execution_time = datetime.datetime.now() - request.time 
        else:
            execution_time = "Unknown"
        print  "Execution time: %s secondes " % execution_time
        return response

Vous devriez voir le temps d'exécution de votre vue dans la console sous la forme suivante:
Execution time: 0:00:00.059770 secondes

Autre exemple de middleware

Nous souhaitons afficher dans notre terminal les requêtes SQL executées par la vue:

from django.conf import settings
from django.db import connection
from django.template import Template, Context

class SQLMiddleWare(object):
    def process_response(self, request, response): 
        if settings.DEBUG and connection.queries:
            execution_sql_time = sum([float(q['time']) for q in connection.queries])        
            t = Template(
"""
{{nb_sql}} requet{{nb_sql|pluralize:"e,es"}} en {{execution_sql_time}} second{{execution_sql_time|pluralize:"e,es"}}:
{% for sql in sql_log %}
[{{forloop.counter}}] {{sql.time}}s: {{sql.sql|safe}}
{% endfor %}         
""")
            print "---------------"
            print t.render(Context({'sql_log':connection.queries,'nb_sql':len(connection.queries),'execution_sql_time':execution_sql_time}))    
            print "---------------"        
        return response

Une base de données par client

Les middleware permettent également de changer de base de données en fonction de paramètres:

eboutique/settings.py

DATABASES = {
    'default' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'base1',
        'USER': 'user1',
        'PASSWORD': 'xxxxxx',
        'HOST': '',                     
        'PORT': '',                      
    },
    'customer1' : {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'customer1',
        'USER': 'user2',
        'PASSWORD': 'xxxxxx', 
    } 
 
}

backoffice/middleware.py

from django.db import connection

class SelectBaseMiddleWare(object):
 
    def process_request(self, request):
         
        if request.get_host() == "localhost:8001":
            cursor = connection.cursor()
            cursor.execute("USE base1; ")
         
    def process_response(self, request, response):
         
        if request.get_host() == "localhost:8001":
            cursor = connection.cursor()
            cursor.execute("USE base1; ")
         
        return response     

conseil de lecture: Middlewares