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. Si ce n’est pas votre cas, lisez notre dossier sur la POO, et revenez après.

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.

24 thoughts on “Explication de code: des mixins et des décorateurs de méthode pour Django

  • Guillaume

    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!

  • Sam Post author

    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.

  • Guillaume

    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!

  • Sam Post author

    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.

  • Guillaume

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

  • Sam Post author

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

  • 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.

  • Biganon

    T’as l’art de rendre les trucs compréhensibles même quand on les voit pour la 1ère fois…

  • rakweine

    Bonjour Sam & Max

    D’abord merci beaucoup pour ton tutoriel très bien expliqué!

    sauf que pour ma part j’aime que les chose soit tout simplement limpide dans ma tête donc voilà:

    quant tu dis je cite plus haut :

    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

    est ce que je pourrais avoir une exemple clair parce que je pense ce principe des MRO est on ne peut plus important lorsqu’on code sous django, et actuellement je bosse sur un projet et je me rends compte que je devrais clairement comprendre les CBV avant de continuer vue qu’elle facilite relativement certains process.

    Je sais que j’arrive tard dans votre blog, je suis plutôt dev c++, swift, …. et pour des besoins technique je bosse sous django je vous prirais de me repondre s’il vous plait ?

    Merci d’avance.

  • Sam Post author

    Faudrait faire un schéma de l’odre des appels des méthodes. Je vais voir si je peux faire ça.

  • rakweine

    d’accord !

    mais s’il vous plait plutôt que d’attendre (je propose même que vous preniez votre temps afin que cela ressemble au tuto actuel)

    je suis habituer à me servir des documentations bien expliquées, jai néanmoins trouvé ceci ccbv sa ressemble beaucoup à Qt (C++) dans la fàçon dont les classe sont hierarchisées, il y’a aussi l’api des CBV que j’ai trouvé dans le site de django, Ce que je veux maintenant c’est ce fameux MRO bien expliqué si jamais il y’a un article qui puisse aussi bien que vous expliquez celà dans un schema je prend.

  • Sam Post author

    Tu veux explication sur le MRO en Python en général, ou voir le MRO de certaines classes en particulier ?

  • rakweine

    Une derniere question est ce qu’il serait possible dans le schema que vous ferez d’indiquer les eletments que les methodes que vous choisirez retournent ?

    comment je pourrais transmettre des paramètres aux autres méthodes de ma CBV (eg: MaVue(ListView)) lorsque je modifie la methode dispatch ?

    un exemple vaudrais mieux alors voici un très simple pris comme ça…:

    disons que j’ai un formulaire de recherche d’articles ou autres, ce formulaire utilise la Get method, bien ! lorsque les donnés sont transmises je souhaite appellé la methode dispatch afin de limiter la recherche aux articles susceptibles d’exister dans ma bdd (dans les cas d’un formulaire avec des autocompletions ou autres…).

    Alors moi j’aimerais savoir comment je pourrais transmettre les paramètres analysé aux autres methode de la même vue

    code:

     
    class MaVue(ListView):
     
    def dispatch(self, request, *args, **kwargs):
     
    #je recupere les donnees transmise via get
     
    seach_article = self.request.GET.get("elt") &lt;---- données recherché par un eventuels user
     
    try:
     
    get_elt_search = MonModel.objects.get(article_name=search_article)
     
    except MonModel.DoesNotExist:
     
    #je retourne une render_to_response ou autre
     
    #---- et c'est ici que je bloque j'aimerais savoir comment je pourrais transmettre ma get_elt_search à ma super method comme explique dans le tutos.

    pourquoi: parce que bah je souhaite reutiliser cette get_elt_search dans ma queryset method histoire de customiser un peu plus ma requête .

    Voilà j’espère que vous m’avez compris et que vous me repondrai aussi vite.

  • rakweine

    Désolé de ne pas rafraichir la page tout le temps donc:

    Oui,

    Je souhaite avoir le mro de certaines classe en particulier les voici:

    ListView, DetailView, Formview, createView, UpdateView.

    et si possible indiquez les elements retournés par chacune des methode de la vue.

  • Sam Post author

    Le MRO d’une classe en Python est disponible dans l’attribut __mro__ :

    >>> from collection import defaultdict
    >>> defaultdict.__mro__
    (collections.defaultdict, dict, object)
    

    Pour le schéma, on verra quand j’aurais le temps de pondre ça.

  • rakweine

    Je repose ma question vue que tu ne l’as pas vu:

    comment je pourrais transmettre des paramètres aux autres méthodes de ma CBV (eg: MaVue(ListView)) lorsque je modifie la methode dispatch ?

    un exemple vaudrais mieux alors voici un très simple pris comme ça…:

    disons que j’ai un formulaire de recherche d’articles ou autres, ce formulaire utilise la Get method, bien ! lorsque les donnés sont transmises je souhaite appellé la methode dispatch afin de limiter la recherche aux articles susceptibles d’exister dans ma bdd (dans les cas d’un formulaire avec des autocompletions ou autres…).

    Alors moi j’aimerais savoir comment je pourrais transmettre les paramètres analysé aux autres methode de la même vue.

    code:

     
    class MaVue(ListView):
     
    def dispatch(self, request, *args, **kwargs):
     
    je recupere les donnees transmise via get
     
    seach_article = self.request.GET.get("elt") &lt;---- données recherché par un eventuels user
     
    try:
     
    get_elt_search = MonModel.objects.get(article_name=search_article)
     
    except MonModel.DoesNotExist:
     
    je retourne une render_to_response ou autre
     
    ---- et c'est ici que je bloque j'aimerais savoir comment je pourrais transmettre ma get_elt_search à ma super method comme explique dans le tutos.

    pourquoi: parce que bah je souhaite reutiliser cette get_elt_search dans ma queryset method histoire de customiser un peu plus ma requête .

    Voilà j’espère que vous m’avez compris et que vous me repondrai aussi vite.

  • Sam Post author

    Si tu veux filtrer ton queryset, fait le en overridant get_queryset(). Tu n’as pas besoin de toucher à dispatch()

  • rakweine

    Bah! résolu !

    j’ai pu constaté que la dispatch method est fournie avec une batterie de variables interressantes et j’ai compris le rôle de la super method !(desolé j’ai tendance à utiliser le plus souvent l’anglais ne m’en voule pas trop)

    donc c’est un souci resolu:

    et pour ce que sa interresse voici ma façon de procéder:

    problème: soit un formulaire de recherche d’articles en get method (<form… method=”GET”>…), le dis formulaire est autocomplete(grace à js + ajax) afin de faciliter la recherche d’un article. sauf que l’user(utilisateur malveillant ou autre..) peut indiquer un nom absent dans la bdd ou la jumping list qui apparaît

    je souhaitais à partir de cet exemple aleatoirement choisi savoir comment gérer ce genre de cas et j’ai choisi d’utliser une liste view pour l’affichage du resultat et la dispatch method pour le traitement des données transmises.

    class MaVue(ListView):

    def dispatch(self, request, *args, **kwargs):

    try:

    get_elt_search = MonModel.objects.get(article_name=search_article)

    except MonModel.DoesNotExist:

    self.kwargs.update(csrf(self.request)

    self.kwargs['nom_erreur']= erreur

    return template_to_response(self.template_name, kwargs)

    super(MaVue, self).dispatch(request, *args, **kwargs)

    ----- reste du code de la vue ------

    Bon c’est tout et ça fonctionne correctement.

  • rakweine

    Et oui je souhaite faire un filtre plus tard, et oui je vais me servir au moment là de la query_set method unique pour le filtre

    l’avantage c’est que j’aurai beaucoup moins de traitement conditionnels à faire vue que les variables auront déjà été trité dans ma dispatch. Mon code sera alors plus facile à maintenir.

  • the_knights_of_ni

    Bonjour,

    j’ai une petite question sur les Mixin, venant du monde Java (oui personne n’est parfait lol), je me demandais si les Mixin n’etait pas une sorte de classe Abstraite en Java ?

    Juste pour que mon cerveau soit rassuré….

  • Sam Post author

    Les mixins sont utilisés comme des classes abstraites dans le sens où elle ne peuvent être instanciées par elles mêmes : on doit forcément en hériter. Mais contrairement aux classes abstraites, toutes les méthodes des mixins sont concrètes.

  • the_knights_of_ni

    Ok merci pour la réponse… ça va m’aider à passer à autre chose lol

  • Sekun

    Bonjour,

    J’aurais une remarque sur la différence entre l’ajout d’un décorateur à la méthode dispatch de la classe courante, et hériter d’une vue LoginRequiredMixin, sur laquelle ce décorateur a été ajouté.

    J’ai du mal avec le rendu de la balide <code>Voir exemple sur 0bin

    Les vues Test1 et Test2 ne redirigeront pas vers la page de login, car dans le case de Test1, la méthode dispatch est appelée après avoir vérifié si l’utilisateur a bien les permissions nécessaires, et dans le cas de Test2, aucune redirection n’est faite car la réponse est stockée dans une variable, qui ne sera jamais retournée.

    Avec Test3, la fonction dispatch est remplacée par une autre fonction grâce au décorateur, elle redirige bien vers la page de login.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.