Mieux utiliser Django pour supprimer du code antédiluvien

Archéocode #1 - Les coulisses (poussiéreuses) de Zeste de Savoir

Zeste de Savoir est un projet au long cours, avec presque 8 ans depuis la sortie officielle du site, encore plus depuis le début du développement, et plus encore quand on considère que la base de code est issue d’un projet antérieur. Avec le temps et la succession des développeurs, il n’est pas rare de voir apparaître quelques signes d’obsolescence, qu’il est de bon ton de corriger au fur et à mesure pour maintenir le code dans un état le plus optimal possible, sur la durée.

Cette série de billet intitulée Archéocode vise à montrer comment le code de Zeste de Savoir tente de garder une certaine jeunesse, loin des nouvelles fonctionnalités visibles pour les utilisateurs.

Dans ce premier billet, je raconte comment nous avons supprimé quelques classes de la base de code de Zeste de Savoir… en utilisant tout simplement ce que propose déjà le framework Django.

Le besoin : vérifier le login et les permissions

Dans notre code, nous avons besoin pour diverses actions

  • très souvent, de vérifier que l’utilisateur est authentifié (logged in)
  • plus rarement, de vérifier qu’il a les bonnes permissions.

L’authentification concerne toutes les fonctionnalités réservées aux membres, par exemple poster des messages sur les forums. Les permissions concernent la plupart du temps des tâches de validation, par exemple valider et publier un tutoriel.

Dans la pratique, le déroulé d’une action ressemble à ceci :

  • l’utilisateur soumet une requête (pour afficher une page ou soumettre un formulaire par exemple) ;
  • le serveur reçoit la requête et la transmet au code (une vue) chargée de son traitement ;
  • la vue vérifie le login : si l’utilisateur est logué, on passe à la suite, sinon on le renvoie vers la page de login ;
  • s’il y a besoin de vérifier des permissions, on le fait : en cas de succès, on effectue l’action et sinon on répond à l’utilisateur qu’il n’a pas la permission.

Ce genre de vérifications sont faites sur énormément de requêtes différentes, typiquement tout ce qui n’est pas juste de la consultation nécessite d’être logué et une grosse part des actions réservées au staff passe par une vérification de permissions1.

Cette ubiquité fait qu’on dispose d’une manière de gérer ça facilement partout !


  1. Les droits d’accès sont vérifiés quoi qu’il arrive, mais une partie du site fonctionne avec un autre système moins flexible, qui n’est pas basé sur les permissions, mais sur des groupes.

La solution : des mixins pour gérer facilement la vérification

La solution utilisée dans le code pour rendre la vérification d’authentification ou de permission facile à faire est celle d’utiliser des mixins.

Concrètement, cela signifie que pour une classe chargée de gérer une requête (une vue typique, par exemple la soumission d’un formulaire), il suffit de lui faire hériter d’une classe qui va modifier son comportement pour lui ajouter les vérifications nécessaires.

Grossièrement, tout se passe comme si on programmait la classe « sans rien vérifier » et qu’on disait « ah oui, au fait, avant de faire tout ça, vérifie le login, s’il-te-plaît ». Et magiquement, la classe fait toutes les vérifications minutieuses nécessaires.

Voilà un exemple d’utilisation :

class ActivateJSFiddleInContent(LoginRequiredMixin, PermissionRequiredMixin, FormView):
    """Handles changes a validator or staff member can do on the js fiddle support of the provided content
    Only these users can do it"""

    permissions = ["tutorialv2.change_publishablecontent"]
    form_class = JsFiddleActivationForm
    http_method_names = ["post"]

    def form_valid(self, form):
        """Change the js fiddle support of content and redirect to the view page"""

        content = get_object_or_404(PublishableContent, pk=form.cleaned_data["pk"])
        # forbidden for content without a validation before publication
        if not content.load_version().requires_validation():
            raise PermissionDenied
        content.update(js_support=form.cleaned_data["js_support"])
        return redirect(content.load_version().get_absolute_url())

Les mixin dans la base de code de Zest de Savoir définis comme ci-dessous. En gros, ils redéfinissent dispatch, ce qui permet d’ajouter le comportement souhaité (vérification du login, permissions) lors du traitement de la requête.

class LoginRequiredMixin:
    """
    Represent the basic code that a Generic Class Based View has to use when
    the required action needs the user to be logged in.
    If the user is not logged in, the user is redirected to the connection form and the former action
    is not executed.
    """

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


class PermissionRequiredMixin:
    """
    Represent the basic code that a Generic Class Based View has to use when one or more
    permissions are required simultaneously to execute the view
    """

    permissions = []

    def check_permissions(self):
        if False in [self.request.user.has_perm(p) for p in self.permissions]:
            raise PermissionDenied

    def dispatch(self, *args, **kwargs):
        self.check_permissions()
        return super().dispatch(*args, **kwargs)

En soi, il n’y a aucun problème majeur avec ce code, il fonctionne correctement. Mais, ce n’est pas pour autant qu’il devrait exister !

La meilleure solution : se servir du framework efficacement

Au cours de la revue d’une modification qui n’avait rien à voir avec ça1, je me suis rendu compte qu’il y avait en fait deux LoginRequiredMixin différents utilisés dans notre code :

  • le nôtre
  • et celui de Django (identique en fonctionnalités).

Cela m’a poussé à regarder la page de la doc en question et découvrir que Django propose également un PermissionRequiredMixin en tout point semblable au nôtre (voire mieux).

Notre besoin est donc déjà couvert par le framework !

Aurait-on pu éviter de réinventer la roue ?

Il se trouve que Django propose depuis des années des fonctionnalités similaires à celle qu’on a réimplémentées. D’ailleurs, ce genre d’implémentation a probablement fini dans Django car c’était une manière idiomatique de le faire dans ce framework.

Cependant, à la décharge des contributeurs du passé, ce n’était pas le cas au début de ZdS. ZdS a démarré avec Django 1.8 (voire peut-être même une version antérieure ?), et les mixins en question sont arrivés dans Django avec la 1.10.

On a donc gardé ce lest dans le code depuis un certain nombre de versions, puisque le code de ZdS utilise désormais la version 3.2.

Quels gains à changer quelque chose qui marche ?

Il n’y a clairement pas de gain décisif à changer nos implémentations par d’autres. Cependant le coût est très minime (modifications assez faciles à tester et peu de risques) et la somme des petits gains associés au changement en vaut la peine.

Voilà une liste des avantages que je vois à ce changement.

  • Moins de code à maintenir. Certes le code était extrêmement stable et fonctionnel, mais ça fait toujours ça de moins à garder en vue à long terme.
  • Utilisation d’une fonctionnalité mieux testée. Le code de Django est beaucoup plus utilisé et testé que le nôtre, ce qui veut dire qu’utiliser leur code nous apporte de meilleures garanties d’avoir quelque chose de qualitatif, performant, sûr et de ne pas avoir laissé passer une petite bêtise.
  • Plus de fonctionnalités « gratuites ». Les implémentations de Django offrent quelques réglages supplémentaires. On peut s’en servir désormais si le besoin se fait sentir, sans effort supplémentaire.
  • Meilleure documentation. Notre code n’était pas documenté aussi bien que celui de Django, et donc il fallait lire le code pour comprendre le détail. Avec Django, on peut lire le code aussi, mais la doc se suffit aussi à elle-même en première approche.
  • Plus facile à aborder pour les externes. Si on connaît déjà Django et qu’on voit ces mixins, on sait à quoi s’attendre. Dans notre code actuel, les noms étaient les mêmes, mais les détails d’implémentation ne l’étaient pas et sont une source de surprise dont on se passe allégrement.

  1. En l’occurence, celle-là), qui m’a fait me plonger dans nos mixins.

Voilà, de la poussière en moins sous le tapis ! Les changements mentionnés dans ce billet se retrouveront bientôt en production, sans que ça ne change rien pour les utilisateurs, mais cela fera un grain de sable en moins pour les développeurs.

Pour les curieux, voilà les deux PR implémentant ces changements :

À bientôt, je l’espère, pour parler d’une autre vieillerie découverte dans le code de Zeste de Savoir et du coup de ménage associé !

Liens utiles

3 commentaires

Dans un projet Django personnel, il m’est arrivé quelque chose de similaire mais avec autre chose : je refaisais ma propre version du helper get_object_or_404.

Helper que vous utilisez apparemment :

content = get_object_or_404(PublishableContent, pk=form.cleaned_data["pk"])

Maintenant, quand je commence à me répéter avec Django, je sens que ça ne va pas forcément dans le bon sens et mon réflexe est de regarder la documentation pour y trouver éventuellement la fonctionnalité déjà implémentée.

Si mes souvenirs sont exacts à l’époque on venait à peine de passer à django 1.7 ou 1.8 (eh oui le site a commencé avec django 1.6, ça fait un baille qu’on est là) et on commençait à peine à voir ce que les Class Based View pouvait nous apporter comme souplesse. Donc c’est probablement un mix de nous deux.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte