Explication de code: des mixins et des décorateurs de méthode pour Django

Suite à notre appel à l’envoi de code à expliquer, nous avons reçu ceci:

Pouvez-vous m’aider à comprendre ce code?

https://gist.github.com/3092600

La fonction dispatch, à quoi sert-elle?

L’appel via super, on appelle le parent de la classe?

Merci, je vous ferai une statue un jour!

Prépare le marbre.

Programmation orientée objet

Commençons par:

L’appel via super, on appelle le parent de la classe?

Oui, très exactement.

Et j’en profite pour rappeler que si vous utilisez les CBV, il va vous falloir vous toucher en programmation orientée objet: définition de classe, héritage multiple, overriding, et autres joyeusetés.

Comme ce genre d’infos peut faire l’objet d’une série d’articles à part entière, je vais être obligé de partir du principe que vous savez coder en objet pour expliquer ce code. Sinon, ce post ferait quelques milliers de lignes.

Je n’exclue pas de faire un article sur l’OO un de ces 4, puisqu’on a déjà eu des demandes, si c’est aussi un truc qui vous intéresse, faites le nous savoir en com.

Worflow des CBV

La fonction dispatch, à quoi sert-elle?

Les CBV ont un ordre d’éxécution pour leurs méthodes: render_to_response() retourne la vue, mais elle est appelée depuis get() ou post(), qui appellent aussi get_context_data() pour créer le context du template. Et get_context_data() appelle get_query_set() pour les vues qui utilisent l’ORM. Il faudrait que je fasse un gros schéma de tout ça un jour.

Dispatch() est la méthode qui appelle toutes les autres. Elle choisi notament si on appelle la méthode get() ou post(). C’est en quelque sorte la méthode mère. Si on veut faire un truc avant que la vue ne travaille, c’est la dedans qu’il faut agir.

Les décorateurs de méthode

Comme vous l’avez vu, le code utilise @method_decorator. Encore une fois, je vais partir du principe que vous vous touchez avec les décorateurs, si ce n’est pas le cas, il y a un article pour ça™.

Néanmoins, dans l’article on ne parle que des décorateurs de fonctions. Comment décorer une méthode ? Et bien c’est pareil, sauf qu’il faut que votre décorateur accepte self en plus comme argument dans la fonction qu’il retourne.

Afin d’éviter de réécrire tous les décorateurs en double, on utilise @method_decorator qui est un décorateur… pour décorateurs ^^ Il transforme un décorateur pour fonction afin qu’il soit applicable à une méthode.

Ainsi quand vous voyez @method_decorator(login_required), ça veut dire “tranformer le décorateur login_required pour qu’il marche sur les méthodes, et l’apppliquer sur la méthode juste en dessous”

Les mixins

Je suis sûr que vous n’avez pas pu vous empêcher de vous demander, à la vue de ça:

LoginRequiredMixin(object):
    """
        View mixin which requires that the user is authenticated.
    """

Mais c’est quoi un mixin non de diou ?

Alors, déjà, on se rassure, ce n’est pas encore un nouveau truc compliqué à apprendre, c’est juste le nom d’un truc que vous connaissez déjà.

Un mixin, c’est le nom qu’on donne à une classe dont le but est exclusivement d’être utilisé pour l’héritage, afin de rajouter une fonctionalité à la classe enfant.

En gros, ce n’est qu’un nom, ça n’a rien de spécial, c’est une classe normale. Mixin est juste le rôle de la classe.

Les mixins ne sont PAS des interfaces, car ils font toujours quelque chose. Ils sont utilisés quand on a un code générique qu’on veut réutiliser dans plusieurs classes.

Les mixins changent le comportement, mais PAS la nature de la classe enfant. Il n’y a pas de AnimalMixing dont hériterait une class Chien, ou Chat. Ca c’est de l’héritage normal. On ne parle de mixin que pour le comportement.

Les mixins ne marchent que dans les langages qui autorisent l’héritage multiple, car on doit pouvoir hériter de plein de mixins d’un coup pour que ça soit utile.

Explication du premier Snippet

Vous vous souvenez du temps où c’était si simple de protéger une vue ?

@login_required
def ma_vue(request):

Avec les CBV, ce temps là est finit mes amis. C’est pour ça que je n’ai pas beaucoup d’amour pour elles.

Pour protéger une CBV on a plusieurs choix:

class MaView(ListView):
    ...
 
ma_vue = login_required(MaView.as_view())

Et on importe ma_vue dans urls.py.

OU directement dans urls.py:

...
url('/this/is/not/a/url', login_required(MaView.as_view()))
...

OU la méthode recommandée par les mecs de Django, méga simple et intuitive:

class MaView(ListView):
    ...
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(ProtectedView, self).dispatch(*args, **kwargs)

On applique le décorateur @login_required à la méthode dispatch(). Comme c’est la méthode qui appelle toutes les autres, la vue est ainsi protégée.

Seulement voilà, tout celà est bien relou, et l’auteur des snippets y remédie:

class LoginRequiredMixin(object):
    """
      View mixin which requires that the user is authenticated.
    """
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(LoginRequiredMixin, self).dispatch(
            self, request, *args, **kwargs)

Ce faisant, il créé un mixin qui va appliquer ce décorateur. Mixin qu’on peut réutiliser ainsi:

class MaView(LoginRequiredMixin, ListView):
    ...

Et protéger une vue redevient simple à nouveau !

Attention, il faut bien mettre le mixin en premier dans la liste de parents. En effet, dispatch() de MaView va appeler celui de LoginRequiredMixin qui va appeler super() qui va va ainsi appeler dispatch() de ListView. C’est ce qu’on appelle le MRO (Method Résolution Order), l’ordre à vachement d’importance, et ça mériterait un article à lui tout seul.

Je résume:

dispatch() est la méthode d’un vue qui appelle toutes les autres. LoginRequiredMixin a sa méthode dispatch() protégée pour qu’elle ne soit accessible que par les utilisateurs enregistrés. MaView hérite du mixin pour intégrer cette fonctionalité. Du coup son dispatch() va utiliser le dispatch() de LoginRequiredMixin, qui est protégé. Comme LoginRequiredMixin est bien faites, son dispatch() appelle celui de ListView, et ainsi la vue fonctionne correctement.

Les autres snippets

On peut maintenant aller plus vite.

Le deuxième mixin permet de n’autoriser l’accès à une vue que si on a les permissions nécessaires (Django vient en effet avec toute une gestion des permissions dans son app contrib ‘auth’)

class PermissionsRequiredMixin(object):
    # les permissions nécessaires sont stockées dans cet attribut
    required_permissions = ()
 
    # dispatch est encore une fois protégée contre les users non loggés
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # on rajoute une subtilité:
        # on vérifie si le user a les permissions exigées avant
        # d'appler le dispatch() du parent
        # si on a pas les permissions, on rajoute un message d'erreur
        # et on redirige vers la page de login
        # sinon on appelle le dispatch() du parent comme d'hab
        if not request.user.has_perms(self.required_permissions):
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(PermissionsRequiredMixin, self).dispatch(
            request, *args, **kwargs)

Ca s’utilise comme ça:

class MaVue(PermissionsRequiredMixin, ListView):
    required_permissions = (
        'dealer.create_cocaine',
        'dealer.delete_cocaine',
    )

Un utilisateur qui n’a pas les permissions create_cocaine et delete_cocaine de l’app ‘dealer’ sera redirigé vers la page de login.

En effet, MaVue.dispatch appelle la méthode PermissionsRequiredMixin.dispatch, mais avec une subtilité: le self passé en paramètre est celui de MaVue. self.required_permissions dans PermissionsRequiredMixin.dispatch est donc en vérité MaVue.required_permissions. Relisez ce paragraphe plusieurs fois.

Notez en revanche que la page de redirection n’est pas configurable, ce qui est bien dommage. Ca se corrige facilement:

class PermissionsRequiredMixin(object):
    required_permissions = ()
    redirect_url = settings.LOGIN_URL # <== HOP
 
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        if not request.user.has_perms(self.required_permissions):
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(self.redirect_url) # <== HOP
        return super(PermissionsRequiredMixin, self).dispatch(
            request, *args, **kwargs)

Du coup on a plus de marge de manoeuvre.

Le 3eme et 4eme snippet, c’est la même chose. Kiff kiff. Pareil.

Mais au lieu de vérifier les permissions, on vérifie juste si l’utilisateur à accès à l’admin Django (is_staff) ou que l’utilisateur est un superutilisateur (is_superuser: il a toutes les permissions):

class StaffRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # j'ai pas besoin de vous expliquer ça quand même ?
        if not request.user.is_staff:
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(StaffRequiredMixin, self).dispatch(request,
            *args, **kwargs)
 
class SuperUserRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # je pense que c'est assez explicite
        if not request.user.is_superuser:
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(SuperUserRequiredMixin, self).dispatch(request,
            *args, **kwargs)

Comme je le disais plus haut, on peut utiliser plusieurs mixins en même temps. Par exemple, si je veux limiter l’accès à une vue aux dealers qui font partie de mon staff:

class MaVue(PermissionsRequiredMixin, StaffRequiredMixin, ListView):
    required_permissions = (
        'dealer.create_cocaine',
        'dealer.delete_cocaine',
    )

Notez encore une fois que les mixins sont là en premier, car MaVue.dispatch appelle PermissionsRequiredMixin.dispatch qui appelle StaffRequiredMixin.dispatch qui appelle ListView.dispatch.

En revanche, dans ce cas précis l’ordre des mixins, entre eux, n’a pas d’importance. Ce ne sera pas toujours le cas: il faut comprendre comment les mixins agissent pour savoir si certains doivent être mis en premier. C’est tout le problème des CBV: pour en faire un usage productif, la somme de choses à savoir est assez démente.

No related posts.

flattr this!

8 comments

  1. Brillant l’article! J’ai suivi la mécanique de la démonstration globalement mais je me heurte
    à des questions métaphysiques!
    La fonction dispatch, pas de souci avec ses arguments extraits des URLs.
    Par contre un “zoom” sur sa finalité s’impose.
    Le dispatch, c’est l’aiguillage, le 2 en 1 du get et du post? True?
    Petite question :
    une soumission par un formulaire, je vais avoir du post. Est ce que le dispatch de la vue
    pourrait être géré une fonction post? je suppose que oui.
    Mais alors quelle est la finalité d’utiliser la fonction dispatch de la classe parent de la vue?
    Je crois que vos explications seront les bienvenue!

  2. Voilà le code de la méthode dispatch:

        def dispatch(self, request, *args, **kwargs):
            # Try to dispatch to the right method; if a method doesn't exist,
            # defer to the error handler. Also defer to the error handler if the
            # request method isn't on the approved list.
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return handler(request, *args, **kwargs)

    Le rôle de dispatch() est donc multiple:

    - appeler la méthode post() ou get() (ou head() ou option(), etc. car tous les verbes HTTP sont disponibles) de la vue selon le type de requête;
    - si le verbe HTTP n’est pas un verbe connu, appeler la méthode http_method_not_allowed();
    - si le verbe HTTP fait parti des methodes interdites pour cette vue, appeler http_method_not_allowed();
    - ajouter à l’instance courante de la vue les attributs request, args et kwargs.

    On appelle donc TOUJOURS la méthode dispatch du parent, car toutes ces étapes sont nécessaires, pour toute les vues.

    Typiquement, dans le cas d’une vue de formulaire, on aura toujours du GET (pour l’affichage du formulaire à vide) et du POST (pour l’envoie des données du formulaire), donc c’est toujours utile dans ce cas.

    C’est d’ailleurs exactement ce que fait la vue générique ProcessFormView:

    https://docs.djangoproject.com/en/1.4/ref/class-based-views/#processformview

    Mais même dans le cas hypothétique d’une vue qui n’accepte que POST (ce qui est dommage, HEAD étant un verbe HTTP très utilisé par les navigateurs WEB pour la gestion du cache), on souhaite quand même garder dispatch()car elle a un rôle de sécurité: elle appelle http_method_not_allowed pour tous les autres verbes HTTP. Sans compter que sans self.request et self.kwargs, on ne peut pas faire grand chose dans une CBV.

  3. Excellent, mais…! Pourquoi le dispatch du parent? La classe fille implémente aussi dispatch(). Je ne saisis pas la subtilité. Je vous ferai une statue quand j’aurai bien tout compris!

  4. Là on tombe dans des considérations de POO, cf mon alerte en début d’article.

    Quand on fait:

    class MaView(ListView):
        pass

    MaView n’implémente pas dispatch, donc quand MaView.dispatch sera appelée, ListView.dispatch sera utilisé automatiquement.

    Quand on fait:

    class MaView(ListView):
        ...
        @method_decorator(login_required)
        def dispatch(self, *args, **kwargs):
            ...

    MaView implémente dispatch. Du coup tout le code du parent (dont nous avons vu l’importance dans le commentaire précédent), n’est pas appelé, et la vue ne peut pas marcher. D’où le super(), qui permet d’appeler à la fin le dispatch du parent:

    class MaView(ListView):
        ...
        @method_decorator(login_required)
        def dispatch(self, *args, **kwargs):
            return super(ProtectedView, self).dispatch(*args, **kwargs)

    Dans le cas d’un mixin:

    class MaVue(PermissionsRequiredMixin, ListView):
        required_permissions = (
            'dealer.create_cocaine',
            'dealer.delete_cocaine',
        )

    MaVue n’implémente PAS dispatch. Donc quand on appelle MaVue.dispatch, c’est le PermissionsRequiredMixin.dispatch qui est appelé. Qui heureusement appelle ListView.dispatch ce qui permet à la vue de marcher.

  5. Je pense avoir pigé, la version “lourde” (avec la batterie de méthodes) du dispatch est dans le parent…

  6. Très bonne explication, très didactique, juste pour mentionner que le code soumis en snippets provient de l’initiative django-braces : https://github.com/brack3t/django-braces

  7. @Guillaume: voilà. C’est le but de l’héritage: ne pas avoir à tout réécrire.

  8. FoxMaSk

    Superbe article, limpide,  pour le neophite que je suis. Ya pas à dire, quand on maîtrise son sujet il est facile de mettre en lumière les subtilités comme celles ci. Du coup ça serait cool effectivement d’avoir un crobar du worflow des CBV. Dans le cas d’une page d’un profil utilisateur,  Ça me  permettrait d’éclaircir comment, après avoir surchargé le model User, je gère la form et la view, pour prendre en compte ces nouvelles propriétés lors de la soumission du formulaire.

Flux RSS des commentaires

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

Jouer à mario en attendant que les autres répondent