Définitions des URL, anciennes et nouvelles

Archéocode #6 - 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 bon 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 sixième billet, je raconte comment on souhaite améliorer grandement la lisibilité des motifs d’URL en utilisant des fonctionnalités plus récentes de Django1.


  1. Par rapport au code du site.

L'état actuel des URL

Je parle d’URL, mais il s’agit en fait plutôt de motifs d’URL (c’est-à-dire la forme possible des URL valides), utilisés par Django pour associer des URL avec des vues (le code chargé de faire le traitement pour un type d’URL donné). Le principe est le suivant : les développeurs définissent des motifs associés à des vues ; Django s’arrange ensuite pour faire la correspondance entre l’URL et un motif, puis aiguiller la requête vers la bonne vue.

Concrètement, les définitions sont contenues dans des fichiers urls.py qui contiennent une liste de la forme suivante, ici pour le module de galeries :

urlpatterns = [
    # Index
    re_path(r"^$", ListGallery.as_view(), name="gallery-list"),
    # Gallery operation
    re_path(r"^nouveau/$", NewGallery.as_view(), name="gallery-new"),
    re_path(r"^(?P<pk>\d+)/(?P<slug>.+)/$", GalleryDetails.as_view(), name="gallery-details"),
    re_path(r"^editer/(?P<pk>\d+)/(?P<slug>.+)/$", EditGallery.as_view(), name="gallery-edit"),
    re_path(r"^membres/(?P<pk>\d+)/$", EditGalleryMembers.as_view(), name="gallery-members"),
    re_path(r"^supprimer/$", DeleteGalleries.as_view(), name="galleries-delete"),
    # Image operations
    re_path(r"^image/ajouter/(?P<pk_gallery>\d+)/$", NewImage.as_view(), name="gallery-image-new"),
    re_path(r"^image/supprimer/(?P<pk_gallery>\d+)/$", DeleteImages.as_view(), name="gallery-image-delete"),
    re_path(r"^image/editer/(?P<pk_gallery>\d+)/(?P<pk>\d+)/$", EditImage.as_view(), name="gallery-image-edit"),
    re_path(r"^image/importer/(?P<pk_gallery>\d+)/$", ImportImages.as_view(), name="gallery-image-import"),
]

On a donc une liste d’appels à re_path, qui prend trois arguments :

  • une expression régulière décrivant le motif,
  • une vue,
  • et enfin un nom pour le motif en question.

L’expression régulière permet de donner des noms aux arguments et vient avec tout son lot de symboles ésotériques, qui rendent les URL difficilement lisibles. Par exemple, la troisième est juste de la forme <pk>/<slug>/, mais la syntaxe brouille tout.

C’est un peu embêtant, parce que ces fichiers sont un point d’entrée très utile pour les développeurs. Ces URL sont découvrables très facilement avec les outils d’inspection des navigateurs et permettent ensuite de savoir dans quelle vue est fait le traitement et donc d’y aller directement. Si c’est illisible, ce cas d’usage n’est pas facilité.

Par exemple, si je voulais modifier la suppression d’images de la galerie, il me suffit d’aller dans mon navigateur, voir que le formulaire de suppression à une cible de la forme /galerie/image/supprimer/<pk>/, aller dans le fichier urls.py des galeries et voir que le code intéressant est dans la classe DeleteImages.

Amélioration de la lisibilité

Les URL actuelles sont peu lisibles. Il s’agit en fait d’un héritage de l’ancienne (et unique à l’époque) manière de définir des motifs avec Django. C’est le moyen le plus flexible également, mais en général, il n’y a pas besoin de cette flexibilité (et même si on en a besoin, c’est probablement que nos URL sont trop compliquées).

Il est donc possible d’utiliser la nouvelle méthode ! (Qui est la méthode recommandée, par ailleurs.)

urlpatterns = [
    # Index
    path("", ListGallery.as_view(), name="gallery-list"),
    # Gallery operation
    path("nouveau/", NewGallery.as_view(), name="gallery-new"),
    path("(<int:pk>/<slug:slug>/", GalleryDetails.as_view(), name="gallery-details"),
    path("editer/<int:pk>/<slug:slug>/", EditGallery.as_view(), name="gallery-edit"),
    path("membres/<int:pk>/", EditGalleryMembers.as_view(), name="gallery-members"),
    path("supprimer/", DeleteGalleries.as_view(), name="galleries-delete"),
    # Image operations
    path("image/ajouter/<int:pk_gallery>/", NewImage.as_view(), name="gallery-image-new"),
    path("image/supprimer/<int:pk_gallery>/", DeleteImages.as_view(), name="gallery-image-delete"),
    path("image/editer/<int:pk_gallery>/<int:pk>/", EditImage.as_view(), name="gallery-image-edit"),
    path("image/importer/<int:pk_gallery>/", ImportImages.as_view(), name="gallery-image-import"),
]

On passe sur la fonction path, toujours trois arguments. Ce coup-ci, on a des arguments entre chevrons <> au lieu de bouillie de regex.

On peut toujours leur donner un nom, mais aussi désormais un type ! Il y a des types fournis par Django, comme int et slug, mais on peut aussi définir ses propres types (ou path converters dans le jargon de Django), ce qui conserve une très grande flexibilité.

L’existence des path converters permet de décharger une partie de la vérification de la vue à la résolution d’URL, pour par exemple éliminer des pk qui n’ont aucune chance de correspondre à un pk valide parce qu’ils ne sont même pas des entiers. On se dispense alors d’un bout de logique peu intéressant (et souvent répété partout) dans le code de la vue et des tests qui vont avec.

Il y a pour l’instant deux PR non fusionnées qui s’attaquent à ce sujet :

  • PR #6284 pour les URL du module de mise en une.
  • PR #6288 pour les MP, plus ambitieuse (peut-être trop) et pas encore terminée.

Aller encore un peu plus loin

Un espace de nom, c’est plutôt bien

Vous aurez aussi sûrement remarqué que les noms sont tous (ou presque) de la forme gallery-<blabla>.

Il se trouve que faire des espaces de noms est possible dans Django et bénéfique en général à la lisibilité. Si on veut appeler un motif dans un espace de nom gallery, il suffit de faire gallery:<blabla>.


app_name = "gallery"

urlpatterns = [
    # Index
    path("", ListGallery.as_view(), name="list"),
    # Gallery operation
    path("nouveau/", NewGallery.as_view(), name="new"),
    path("(<int:pk>/<slug:slug>/", GalleryDetails.as_view(), name="details"),
    path("editer/<int:pk>/<slug:slug>/", EditGallery.as_view(), name="edit"),
    path("membres/<int:pk>/", EditGalleryMembers.as_view(), name="members"),
    path("supprimer/", DeleteGalleries.as_view(), name="delete"),
    # Image operations
    path("image/ajouter/<int:pk_gallery>/", NewImage.as_view(), name="image-new"),
    path("image/supprimer/<int:pk_gallery>/", DeleteImages.as_view(), name="image-delete"),
    path("image/editer/<int:pk_gallery>/<int:pk>/", EditImage.as_view(), name="image-edit"),
    path("image/importer/<int:pk_gallery>/", ImportImages.as_view(), name="image-import"),
]

Jusque là, il n’y a aucun changement dans les motifs, donc aucune URL ne change et le changement est véritablement invisible, même pour les sauvages qui enverraient des formulaires à la main !

Encore un peu plus ?

On pourrait encore imaginer d’autres modifications plus mineures, mais qui apportent relativement peu :

  • avoir des verbes partout plutôt que ce "nouveau" qui traîne ;
  • utiliser le verbe "modifier", un peu plus canonique en français, plutôt que "editer" ;
  • changer "details" pour "view", peut-être ?
  • enlever le slug pour les URL purement techniques (on ne fait en général jamais de lien et ils n’ont pas besoin d’être explicites).

Ce qui donnerait quelque chose de cet acabit :


app_name = "gallery"

urlpatterns = [
    # Index
    path("", ListGalleries.as_view(), name="list"),
    # Gallery operations
    path("creer/", CreateGallery.as_view(), name="create"),
    path("<int:pk>/<slug:slug>/", ViewGallery.as_view(), name="view"),
    path("modifier/<int:pk>/", EditGallery.as_view(), name="edit"),
    path("membres/<int:pk>/", EditGalleryMembers.as_view(), name="members"),
    path("supprimer/", DeleteGalleries.as_view(), name="delete"),
    # Image operations
    path("image/ajouter/<int:pk_gallery>/", AddImage.as_view(), name="image-add"),
    path("image/supprimer/<int:pk_gallery>/", DeleteImages.as_view(), name="image-delete"),
    path("image/modifier/<int:pk_gallery>/<int:pk>/", EditImage.as_view(), name="image-edit"),
    path("image/importer/<int:pk_gallery>/", ImportImages.as_view(), name="image-import"),
]

On pourrait aussi renommer intelligemment les classes au passage, dont le nom est essentiellement redondant avec le nom du module, mais ne nous emportons pas trop.

Note : Une PR a été faite après avoir rédigé ce billet, mais un peu différente encore : PR #6293.


Voilà, de la poussière en moins sous le tapis ! Améliorer la lisibilité, harmoniser et simplifier les URL n’apporte pas forcément un gros gain individuellement, mais si on additionne beaucoup de petites choses de ce type partout dans le code, le résultat pourrait être surprenant !

À bientôt, peut-être, 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

J’ignorais donc ce petit détail d’historique de Django. J’apprends ainsi que j’ai eu la chance de me mettre à Django quand la version d’alors recommandait déjà path. En fait, je pensais que c’était dans l’autre sens : path étant la version « de base » et re_path étant une version « complémentaire » pour les cas tordus (que je n’ai jamais rencontrés une seule fois, Dieu merci !).

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