Sam & Max » mixin http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Explication de code: des mixins et des décorateurs de méthode pour Django 20 http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/ http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/#comments Tue, 21 Aug 2012 14:37:57 +0000 http://sametmax.com/?p=1812 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.

]]>
http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/feed/ 20