• 40 hours
  • Medium

Free online content available in this course.

Paperback available in this course

course.header.alt.is_certifying

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 11/15/19

Les signaux et middlewares

Log in or subscribe for free to enjoy all this course has to offer!

Django délimite proprement et nettement ses différentes composantes. Il est impossible de se charger du routage des URL depuis un template, et il est impossible de créer des modèles dans les vues. Si cette structuration a bien évidemment des avantages (propreté du code, réutilisation, etc.), sa lourdeur peut parfois empêcher de réaliser certaines actions.

En effet, comment effectuer une action précise à chaque fois qu’une entrée d’un modèle est supprimée, et ce depuis n’importe où dans le code ? Ou comment analyser toutes les requêtes d’un visiteur pour s’assurer que son adresse IP n’est pas bannie ? Pour ces situations un peu spéciales qui nécessitent de répéter la même action à plusieurs moments et endroits dans le code, Django intègre deux mécanismes différents qui permettent de résoudre ce genre de problèmes : les signaux et les middlewares.

Notifiez avec les signaux

Premier mécanisme : les signaux. Un signal est une notification envoyée par une application à Django lorsqu’une action se déroule, et renvoyée par le framework à toutes les autres parties d’applications qui se sont enregistrées pour savoir quand ce type d’action se déroule, et comment.

Reprenons l’exemple de la suppression d’un modèle : imaginons que nous ayons plusieurs fichiers sur le disque dur, liés à une instance d’un modèle. Lorsque l’instance est supprimée, nous souhaitons que les fichiers associés soient également supprimés. Cependant, cette entrée peut être supprimée depuis n’importe où dans le code, et vous ne pouvez pas à chaque fois rajouter un appel vers une fonction qui se charge de la suppression des fichiers associés (parce que ce serait trop lourd ou que cela ne dépend simplement pas de vous). Les signaux sont la solution parfaite à ce problème.

Pour résoudre ce problème, une fois que vous avez écrit la fonction de suppression des fichiers associés, vous n’avez qu’à indiquer à Django d’appeler cette fonction à chaque fois qu’une entrée de modèle est supprimée. En pratique, cela se fait ainsi :

from django.db.models.signals import post_delete

post_delete.connect(ma_fonction_de_suppression, sender=MonModele)

La méthode est plutôt simple : il suffit d’importer le signal et d’utiliser la méthode connect  pour connecter une fonction à ce signal. Le signal ici importé est post_delete, et comme son nom l’indique, il est notifié à chaque fois qu’une instance a été supprimée. Chaque fois que Django recevra le signal, il le transmettra en appelant la fonction passée en argument (ma_fonction_de_suppression  ici). Cette méthode peut prendre plusieurs paramètres, comme par exemple ici sender, qui permet de restreindre l’envoi de signaux à un seul modèle (MonModele  donc), sans quoi la fonction sera appelée pour toute entrée supprimée, et, quel que soit le modèle dont elle dépend.

Une fonction appelée par un signal prend souvent plusieurs arguments. Généralement, elle prend presque toujours un argument appelé sender. Son contenu dépend du type de signal en lui-même (par exemple, pour post_delete, la variable sender  passée en argument sera toujours le modèle concerné, comme vu précédemment). Chaque type de signal possède ses propres arguments. post_delete  en prend trois :

  • sender  : le modèle concerné, comme vu précédemment ;

  • instance  : l’instance du modèle supprimée (celle-ci étant supprimée, il est très déconseillé de modifier ses données ou de tenter de la sauvegarder) ;

  • using  : l’alias de la base de données utilisée (si vous utilisez plusieurs bases de données, il s’agit d’un point particulier et inutile la plupart du temps).

Notre fonction ma_fonction_de_suppression  pourrait donc s’écrire de la sorte :

def ma_fonction_de_suppression(sender, instance, **kwargs):
	# processus de suppression selon les données fournies par instance

Pourquoi spécifier un **kwargs ?

Vous ne pouvez jamais être certain qu’un signal renverra bien tous les arguments possibles, cela dépend du contexte. Dès lors, il est toujours important de spécifier un dictionnaire pour récupérer les valeurs supplémentaires, et si vous avez éventuellement besoin d’une de ces valeurs, il suffit de vérifier si la clé est bien présente dans le dictionnaire.

Cette fonction et sa connexion peuvent être mises n’importe où, tant que Django charge le fichier afin qu’il puisse faire la connexion directement. Le framework charge déjà par défaut certains fichiers comme les models.pyurls.py, etc. Le meilleur endroit serait donc un de ces fichiers. Généralement, nous choisissons un models.py  (étant donné que certains signaux agissent à partir d’actions sur des modèles, c’est plutôt un bon choix !).

Petit détail, il est également possible d’enregistrer une fonction à un signal directement lors de sa déclaration avec un décorateur. En reprenant l’exemple ci-dessus :

from django.db.models.signals import post_delete
from django.dispatch import receiver

@receiver(post_delete, sender=MonModele)
def ma_fonction_de_suppression(sender, instance, **kwargs):
	# processus de suppression selon les données fournies par instance

Il existe bien entendu d’autres types de signaux, voici une liste non exhaustive contenant les principaux, avec les arguments transmis avec la notification :

Nom

Description

Arguments

django.db.models.signals.pre_save

Envoyé avant qu’une instance de modèle ne soit enregistrée.

  • sender  : le modèle concerné

  • instance  : l’instance du modèle concernée

  • using  : l’alias de la BDD utilisée

  • raw  : un booléen, mis à True  si l’instance sera enregistrée telle qu’elle est présentée depuis l’argument

django.db.models.signals.post_save

Envoyé après qu’une instance de modèle a été enregistrée.

  • sender  : le modèle concerné

  • instance  : l’instance du modèle concernée

  • using  : l’alias de la BDD utilisée

  • raw  : un booléen, mis à True  si l’instance sera enregistrée telle qu’elle est présentée depuis l’argument

  • created  : un booléen, mis à True  si l’instance a été correctement enregistrée

django.db.models.signals.pre_delete

Envoyé avant qu’une instance de modèle ne soit supprimée.

  • sender  : le modèle concerné

  • instance  : l’instance du modèle concernée

  • using  : l’alias de la BDD utilisée

django.db.models.signals.post_delete

Envoyé après qu’une instance de modèle a été supprimée.

  • sender  : le modèle concerné

  • instance  : l’instance du modèle concernée

  • using  : l’alias de la BDD utilisée

django.core.signals.request_started

Envoyé à chaque fois que Django reçoit une nouvelle requête HTTP.

  • sender  : la classe qui a envoyé la requête, par exemple WsgiHandler

django.core.signals.request_finished

Envoyé à chaque fois que Django termine de répondre à une requête HTTP.

  • sender  : la classe qui a envoyé la requête, par exemple WsgiHandler

Il existe d’autres signaux inclus par défaut. Ils sont expliqués dans la documentation officielle.

Sachez que vous pouvez tester tous ces signaux simplement en créant une fonction affichant une ligne dans la console (avec print) et en liant cette fonction aux signaux désirés.

Heureusement, si vous vous sentez limité par la liste des types de signaux fournis par Django, sachez que vous pouvez en créer vous-même. Le processus est plutôt simple.

Chaque signal est en fait une instance de django.dispatch.Signal. Pour créer un nouveau signal, il suffit donc de créer une nouvelle instance, et de lui dire quels arguments le signal peut transmettre :

from django.dispatch import Signal

crepe_finie = Signal(providing_args=["adresse", "prix"])

Ici, nous créons un nouveau signal nommé crepe_finie. Nous lui indiquons une liste contenant les noms d’éventuels arguments (les arguments de signaux n’étant jamais fixes, vous pouvez la modifier à tout moment) qu’il peut transmettre, et c’est tout !

Nous pourrions dès lors enregistrer une fonction sur ce signal, comme vu précédemment :

crepe_finie.connect(faire_livraison)   # Quand crepe_finie est lancé, appeler 'faire_livraison'

Lorsque nous souhaitons lancer une notification à toutes les fonctions enregistrées au signal, il suffit d’utiliser la méthode send, et ceci depuis n’importe où. Nous l’avons fait depuis un modèle :

class Crepe(models.Model):
	nom_recette = models.CharField(max_length=255)
	prix = models.IntegerField()
	# ...

	def preparer(self, adresse):
		# Nous préparons la crêpe pour l'expédier à l'adresse transmise
		crepe_finie.send(sender=self, adresse=adresse, prix=self.prix)

À chaque fois que la méthode preparer  d’une crêpe sera appelée, la fonction faire_livraison  le sera également avec les arguments adéquats.
Notons ici qu’il est toujours obligatoire de préciser un argument sender  lorsque nous utilisons la méthode send. Libre à vous de choisir ce que vous souhaitez transmettre, mais il est censé représenter l’entité qui est à l’origine du signal. Nous avons ici choisi d’envoyer directement l’instance du modèle.

Aussi, la fonction send  retourne une liste de paires de variables, chaque paire étant un tuple de type (receveur, retour)  où le receveur est la fonction appelée, et le retour est la variable retournée par la fonction.
Par exemple, si nous n’avons que la fonction faire_livraison  connectée au signal crepe_finie, et que celle-ci retourne True  si la livraison s’est bien déroulée (considérons que c’est le cas maintenant), la liste renvoyée par send  serait [(faire_livraison, True)].

Pour terminer, il est également possible de déconnecter une fonction d’un signal. Pour ce faire, il faut utiliser la méthode disconnect  du signal. Cette dernière s’utilise comme connect  :

crepe_finie.disconnect(faire_livraison)

crepe_finie  n’appellera plus faire_livraison  si une notification est envoyée. Sachez que, si vous avez soumis un argument sender  lors de la connexion, vous devrez également le préciser lors de la déconnexion.

Contrôlez tout avec les middlewares

Deuxième mécanisme : les middlewares. Nous avons vu précédemment que lorsque Django recevait une requête HTTP, il analysait l’URL demandée et en fonction de celle-ci choisissait la vue adaptée, et cette dernière se chargeait de renvoyer une réponse au client (en utilisant éventuellement un template). Nous avons cependant omis une étape, qui se situe juste avant et après l’appel de la vue.

En effet, le framework va à ce moment exécuter certains bouts de code appelés des middlewares. Il s’agit en quelque sorte de fonctions qui seront exécutées à chaque requête, en "enrobant" la vue Django appelée.

Typiquement, les middlewares se chargent de modifier certaines variables ou d’interrompre le processus de traitement de la requête, et cela aux différents moments que nous allons lister ci-dessous.

Par défaut, Django inclut plusieurs middlewares dans la configuration par défaut :

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Il est conseillé de garder ces middlewares et d’ajouter les vôtres à la suite. Ils s’occupent de certaines tâches pratiques et permettent d’utiliser d’autres fonctionnalités du framework que nous verrons plus tard ou avons déjà vues (comme la sécurisation des formules contre les attaques CSRF, le système utilisateur, l’envoi de notifications aux visiteurs, etc.).

Un middleware se résume à une fonction qui retourne une fonction qui sera appelée à chaque fois qu’une requête est reçue par Django. La fonction principale prend en paramètre une méthode, fournie par Django, qui va nous permettre de lancer l’appel de la vue demandée par l’utilisateur et ainsi de lui renvoyer la réponse.

def simple_middleware(get_response):
    # Le code ici est appelé une seule fois, pour l'initialisation
    # et la configuration

    def middleware(request):
        # Code qui sera exécuté à chaque requête, et avant
        # le traitement de la réponse

        response = get_response(request)

        # Code qui sera exécuté à chaque requête, une fois la
        # réponse calculée, mais pas encore servie

        return response

    return middleware

Il est dès lors possible d’intercepter toute requête, d’en modifier ses paramètres puis de laisser continuer son exécution, ou même de décider de renvoyer une réponse tout à fait différente. De même, il est tout à fait possible de modifier une réponse calculée.

Dernier point avant de passer à la pratique : les middlewares sont appelés dans l’ordre précisé dans le settings.py, de haut en bas, pour toutes les méthodes appelées avant l’appel de la vue. Chaque fonction englobe donc la suivante. Ainsi, avec le code suivant, et dans l’ordre  (middleware1, middleware2)  :

def middleware1(get_response):
    def middleware(request):
        print("J'ouvre le bal de la requête")
        response = get_response(request)
        print("Et je clôture également le show.")
        return response

    return middleware
    

def middleware2(get_response):
    def middleware(request):
        print("J'englobe également la vue, mais après")
        response = get_response(request)
        print("Compris ?")
        return response

    return middleware
    
    
def ma_vue(request):
    print("Enfin, nous arrivons dans la vue !")
    return HttpResponse("Ma réponse")

On obtient le résultat suivant dans la console :

J'ouvre le bal de la requête
J'englobe également la vue, mais après
Enfin, nous arrivons dans la vue !
Compris ?
Et je clôture également le show.

Créons notre propre middleware

Comme exemple, nous avons choisi de coder un petit middleware simple, mais pratique, qui comptabilise le nombre de fois qu’une page est vue et affiche ce nombre à la fin de chaque page. Bien évidemment, vu le principe des middlewares, il n’est nullement nécessaire d’aller modifier une vue pour arriver à nos fins, et cela marche pour toutes nos vues !

Pour ce faire, et pour des raisons de propreté et de structuration du code, le middleware sera placé dans une nouvelle application nommée stats.
Pour rappel, pour créer une application, rien de plus simple :

python manage.py startapp stats

Une fois cela fait, la prochaine étape consiste à créer un nouveau modèle dans l’application qui permet de tenir compte du nombre de visites d’une page. Chaque entrée du modèle correspondra à une page.
Rien de spécial, en définitive :

from django.db import models

class Page(models.Model):
    url = models.URLField()
    nb_visites = models.IntegerField(default=1)

    def __str__(self):
        return self.url

Il suffit dès lors d’ajouter l’application au settings.py  et de lancer un manage.py migrate
Voici notre middleware que nous avons enregistré dans stats/middleware.py  :

from django.db.models import F
from models import Page

def stats_middleware(get_response):
    def middleware(request):
        # Avant chaque exécution de la vue, on incrémente 
        # le nombre de page vues à chaque appel de vues
        try:
            # Le compteur lié à la page est récupéré et incrémenté
            p = Page.objects.get(url=request.path)  
            p.nb_visites = F('nb_visites') + 1
            p.save()
        except Page.DoesNotExist:
            # Un nouveau compteur à 1 par défaut est créé
            p = Page.objects.create(url=request.path)
        
        # Appel de la vue Django
        response = get_response(request)
        
        # Une fois la vue exécutée, on ajoute à la fin le nombre
        # de vues de la page 
        response.content += bytes(
            "Cette page a été vue {0} fois.".format(p.nb_visites),
            "utf8"
        )
        # Et on retourne le résultat
        return response

    return middleware

Petite pause pour vous expliquer l’objet F. Comme l’objet Q que nous avons vu avant, F sert à construire des requêtes SQL plus élaborées. Au lieu de récupérer la valeur en Python et de l’incrémenter de 1,  F  prend en compte l’opération "+ 1" auquel il a été associé et va directement faire ces opérations dans la base de données, si possible. Si vous avez des notions de SQL, on aura ici un UPDATE stats_page SET nb_visites=nb_visites+1... au lieu d’un SELECT, puis d’un UPDATE. C’est un outil pratique pour optimiser de simples opérations numériques sur des champs.

Revenons-en aux middlewares, et mettons à jour MIDDLEWARE  dans votre settings.py

MIDDLEWARE = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'stats.middleware.stats_middleware',
)

Pour rappel, avant chaque appel de vue, Django appelle la méthode process_view  qui se charge ici de déterminer si l’URL de la page a déjà été appelée ou non (l’URL est accessible à partir de l’attribut request.path  ; n’hésitez pas à consulter la documentation pour connaître toutes les méthodes et attributs de HttpRequest). Si la page a déjà été appelée, il incrémente le compteur de l’entrée. Sinon, il crée une nouvelle entrée.

Au retour, on vérifie tout d’abord si la requête s’est bien déroulée en s’assurant que le code HTTP de la réponse est bien 200. Ensuite, nous reprenons le compteur et nous modifions le contenu de la réponse (inclus dans response.content  ; la documentation vous donnera également tout ce qu’il faut savoir sur l’objet HttpResponse). Bien évidemment, si vous renvoyez du HTML au client, la phrase ajoutée ne sera pas intégrée correctement au document. Néanmoins, vous pouvez très bien coder quelque chose de plus sophistiqué qui permet d’insérer la phrase à un endroit valide. En pratique, on ne modife jamais la réponse via un middleware, pensez aux template context processors !

En fin de compte, sur toutes vos pages, vous verrez la phrase avec le nombre de visites qui se rajoute toute seule, sans devoir modifier toutes les vues une à une !

En résumé

  • Un signal est une notification envoyée par une application à Django lorsqu’une action se déroule, et renvoyée par le framework à toutes les autres parties d’applications qui se sont enregistrées pour savoir quand ce type d’action se déroule, et comment.

  • Les signaux permettent d’effectuer des actions à chaque fois qu’un événement précis survient.

  • Les middlewares sont des classes instanciées à chaque requête, exception, ou encore génération de template, dans l’ordre donné par MIDDLEWARE.

  • Ils permettent d’effectuer une tâche précise à chaque appel.

Example of certificate of achievement
Example of certificate of achievement