Accueil / Blog / Métier / 2016 / Django Rest Framework : personnalisation des routeurs (partie 5)

Django Rest Framework : personnalisation des routeurs (partie 5)

Par Yannick Chabbert — publié 14/01/2016, édité le 24/02/2016
Dernière ligne droite pour les routeurs : après avoir analysé le fonctionnement et l'intégration, c'est au tour de la personnalisation.
Django Rest Framework : personnalisation des routeurs (partie 5)

Personnaliser les routes

"Les routes dynamiques, c'est bien mais je n'arrive pas à faire exactement ce que je veux. Pourtant, c'est pas faute d'avoir essayé des trucs tordus comme jouer avec les attributs lookup_*"...

Tiens tiens, vous n'essayez pas de forcer des paramètres supplémentaires dans l'url par hasard ?

"Et sinon, je peux ajouter un chemin en plus avec des paillettes pour faire plus sexy ?"

Heureusement oui. On peut même imaginer déclarer des actions de masses type create/update/delete.

Vous l'avez compris, une des solutions possibles est la personnalisation d'un routeur. Cependant, sachez qu'il est préférable d'avoir des besoins simples. Préparez-vous également à devoir instancier plusieurs classes de routeurs. Autrement dit, on va devoir s'écarter un peu des préconisations...

En règle générale, il suffit de surcharger l'attribut routes de la classe SimpleRouter (ou DefaultRouter si vous tenez à conserver les formats de suffixes et vues d'API racine). Vous pouvez ajouter à cette liste de routes vos propres routes, voir la modifier complètement. Après bien sûr, vous pouvez aussi surcharger n'importe quelles méthodes du routeur.

Trêve de bavardages et au boulot ! Clonez ce dépôt et allez sur la branche routing_custom. N'oubliez pas de lancer la commande de migration ./manage.py migrate.

Notre exercice va porter uniquement sur l'application bar. On va commencer par déclarer un routeur personnalisé dans le fichier routers.py de l'application bar. Ce routeur va :

  • permettre de faire des opérations en masse de modifications et suppressions
  • conserver les actions de détails classiques
  • fournir pour tous les chemins de type list un argument numérique et un chemin en plus. Ceci concerne donc aussi les routes dynamiques

Voici une implémentation possible :

from rest_framework.routers import DefaultRouter, DynamicListRoute, Route


class MyCustomRouter(DefaultRouter):

    routes = list(DefaultRouter.routes)
    routes[0] = Route(
        url=r'^{prefix}/a-custom-path/(?P<custom_pk>\d+){trailing_slash}$',
        mapping={
            'get': 'list',
            'post': 'create',
            'patch': 'partial_update_multiple',
            'delete': 'delete_multiple',
        },
        name='{basename}-list',
        initkwargs={'suffix': 'List'}
    )
    routes[1] = DynamicListRoute(
        url=r'^{prefix}/a-custom-path/(?P<custom_pk>\d+)/{methodname}{trailing_slash}$',
        name='{basename}-{methodnamehyphen}',
        initkwargs={}
    )

Ce que l'on fait ici :

  • on créé une nouvelle classe routeur qui hérite de la classe DefaultRouter parce qu'on aime la diversité des formats
  • on effectue une copie de la liste des routes parentes pour modifier les routes de type list standard et dynamique. On conserve donc les autres routes, c'est-à-dire les routes de détail. Par contre, on a plus les anciennes routes de type list. C'est donc un routeur spécifique.
  • chacune des routes modifiées a désormais un nouveau chemin a-custom-path parce que c'est trop swagg (c'est comme ça qu'on dit ? Punaise ça me fait mal !).
  • un nouveau paramètre numérique custom_pk doit être fourni
  • les actions standards sont étendues pour associer les méthodes HTTP PATCH et DELETE avec de nouvelles méthodes d'instance nommées partial_update_multiple et delete_multiple. Trop classe.

Maintenant qu'on a un routeur flambant neuf, il faut modifier le Viewset en conséquence pour implémenter les deux actions de masses et l'action dynamique :

from rest_framework.decorators import list_route
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from .models import Bar


class BarViewSet(ModelViewSet):
    queryset = Bar.objects.all()

    def partial_update_multiple(self, request, custom_pk, *args, **kwargs):
        return Response(
            'Do some bulk update op for the given custom pk : {pk}'.format(pk=custom_pk)
        )

    def delete_multiple(self, request, custom_pk, *args, **kwargs):
        return Response(
            'Do some bulk delete op for the given custom pk : {pk}'.format(pk=custom_pk)
        )

    @list_route(methods=['get'], url_path='so-dope-path')
    def a_custom_callback(self, request, custom_pk, *args, **kwargs):
        return Response(
            'Like American people say : AWESOME! We now have a custom '
            'dynamic list route with a custom path and take a custom pk '
            'which is : {pk}'.format(pk=custom_pk)
        )

Rien de particulier hormis un détail qui vous a peut-être échappé : regardez la classe héritée du Viewset. Comme c'est la classe ModelViewSet, les actions standard sont déjà implémentées. Donc ça n'est pas visible, mais les routes de détail qu'on avait copiées sans modification dans le routeur sont bien implémentées par le Viewset.

Il ne reste plus qu'à utiliser ce nouveau routeur et enfreindre une de nos propres règles :

from django.conf.urls import include, url

from rest_framework.routers import DefaultRouter

from bar.routers import MyCustomRouter
from bar.viewsets import BarViewSet
from baz.viewsets import BazViewSet
from foo.viewsets import FooViewSet


router = DefaultRouter()
router.register('api/baz', BazViewSet)
router.register('api/foo', FooViewSet)

custom_router = MyCustomRouter()
custom_router.register('api/bar', BarViewSet)

urlpatterns = [
    # Some Web views
    url(r'^foo/', include('foo.urls', namespace='foo')),
    url(r'^bar/', include('bar.urls', namespace='bar')),
    url(r'^baz/', include('baz.urls', namespace='baz')),
]
# Append our API views
urlpatterns += router.urls
urlpatterns += custom_router.urls

Voilà, on a deux routeurs, beurk. Mais désormais, les motifs d'urls générés sont :

/   rest_framework.routers.APIRoot  api-root
/   rest_framework.routers.APIRoot  api-root
/.<format>/ rest_framework.routers.APIRoot  api-root
/.<format>/ rest_framework.routers.APIRoot  api-root
/api/bar/<pk>.<format>/     bar.viewsets.BarViewSet bar-detail
/api/bar/<pk>/      bar.viewsets.BarViewSet bar-detail
/api/bar/a-custom-path/<custom_pk>.<format>/        bar.viewsets.BarViewSet bar-list
/api/bar/a-custom-path/<custom_pk>/ bar.viewsets.BarViewSet bar-list
/api/bar/a-custom-path/<custom_pk>/so-dope-path.<format>/   bar.viewsets.BarViewSet bar-so-dope-path
/api/bar/a-custom-path/<custom_pk>/so-dope-path/    bar.viewsets.BarViewSet bar-so-dope-path
/api/baz.<format>/  baz.viewsets.BazViewSet baz-list
/api/baz/   baz.viewsets.BazViewSet baz-list
/api/baz/<pk>.<format>/     baz.viewsets.BazViewSet baz-detail
/api/baz/<pk>/      baz.viewsets.BazViewSet baz-detail
/api/foo.<format>/  foo.viewsets.FooViewSet foo-list
/api/foo/   foo.viewsets.FooViewSet foo-list
/api/foo/<pk>.<format>/     foo.viewsets.FooViewSet foo-detail
/api/foo/<pk>/      foo.viewsets.FooViewSet foo-detail
/bar/mywebview/     bar.views.MyWebView     bar:mywebview
/baz/mywebview/     baz.views.MyWebView     baz:mywebview
/foo/mywebview/     foo.views.MyWebView     foo:mywebview

Si vous lancez un serveur avec la commande ./manage.py runserver, vous pouvez vérifier le bon fonctionnement en exécutant les requêtes suivantes :

  • curl -X PATCH 127.0.0.1:8000/api/bar/a-custom-path/15/
  • curl -X DELETE 127.0.0.1:8000/api/bar/a-custom-path/15/
  • curl -X GET 127.0.0.1:8000/api/bar/a-custom-path/15/so-dope-path/

Remarquez que les actions standard de détails sont bien implémentées et retournent un joli code 404 car aucune ressource avec l'id 1 n'existe :

  • curl -v -X GET 127.0.0.1:8000/api/bar/1/
  • curl -v -X PUT 127.0.0.1:8000/api/bar/1/
  • curl -v -X PATCH 127.0.0.1:8000/api/bar/1/
  • curl -v -X DELETE 127.0.0.1:8000/api/bar/1/

Éviter les routeurstein

Dans l'exemple précédent, on a adopté une approche minimaliste en surchargeant uniquement l'attribut routes. Notre fainéantise n'est pas anodine car elle nous permet de rester simple. Et ce qui est simple est facile à maintenir.

Si vous êtes amené à trop surcharger le routeur, il est possible d'obtenir un routeurstein difficile à maintenir (ouais, ça m'est venu comme ça. J'ai osé). Rappellez-vous que les routeurs sont là pour vous faire gagner du temps en produisant très peu de code. Si vous perdez du temps à surcharger (bidouiller) les mécanismes par défaut des routeurs car vous avez des besoins particuliers, c'est probablement que le routeur n'est pas la solution à retenir. N'oubliez pas, vous pouvez tout-à-fait générer manuellement les motifs d'urls d'un Viewset comme le fait un routeur. En reprenant notre projet d'exemple précédent (allez sur la branche no_router), on pourrait générer les mêmes motifs d'url comme ceci :

from django.conf.urls import include, url

from bar.viewsets import BarViewSet
from baz.viewsets import BazViewSet
from foo.viewsets import FooViewSet

urlpatterns = [
    url(r'^api/bar/$', BarViewSet.as_view({
        'get': 'list',
        'post': 'create',
    }), name='bar-list'),
    url(r'^api/bar/(?P<pk>[^/.]+)/$', BarViewSet.as_view({
        'get': 'retrieve',
        'put': 'update',
        'patch': 'partial_update',
        'delete': 'destroy',
    }), name='bar-detail'),
    url(r'^api/baz/$', BazViewSet.as_view({
        'get': 'list',
        'post': 'create',
    }), name='baz-list'),
    url(r'^api/baz/(?P<pk>[^/.]+)/$', BazViewSet.as_view({
        'get': 'retrieve',
        'put': 'update',
        'patch': 'partial_update',
        'delete': 'destroy',
    }), name='baz-detail'),
    url(r'^api/foo/$', FooViewSet.as_view({
        'get': 'list',
        'post': 'create',
    }), name='foo-list'),
    url(r'^api/foo/(?P<pk>[^/.]+)/$', FooViewSet.as_view({
        'get': 'retrieve',
        'put': 'update',
        'patch': 'partial_update',
        'delete': 'destroy',
    }), name='foo-detail'),
    # Some Web views
    url(r'^foo/', include('foo.urls', namespace='foo')),
    url(r'^bar/', include('bar.urls', namespace='bar')),
    url(r'^baz/', include('baz.urls', namespace='baz')),
]

C'est moche hein ? Bon, ceci n'est qu'un exemple pour vous montrer que les routeurs ne sont pas indispensables. Néanmoins, on réalise aussi que finalement les routeurs, c'est de la boulette !

Bonnes pratiques des url REST

Même si on sort du cadre de cet article dédié exclusivement à DRF, j'aimerai terminer sur quelques conseils pour vos urls finales :

  • le préfix de chemin, c'est le nom de votre ressource au pluriel. Cependant, il arrive parfois qu'on ne manipule qu'une seule ressource. C'est le cas avec l'utilisateur courant. Par exemple, avoir /user/ ou /account/ a du sens. Mais pour les autres situations, on manipule en générale plusieurs ressources. Admettons que le modèle soit Book, le préfix de chemin est donc /books/ et non /book/. Sinon, ca veut dire que votre bibliothèque tient sur la commode des W.C.
  • elles sont courtes. Par conséquent, vous avez de nombreux points d'entrées principales et peu de profondeur relationnelle. Par exemple, ne faites pas GET /books/authors/<author_pk>/ mais plutôt GET /authors/<pk>/. Néanmoins, on s'accorde en général la possibilité d'avoir le premier point d'entrée enfant qui est la vue de liste. Par exemple GET /books/authors/. Pas plus ! Stop ! Fini ! Sinon on risque d'avoir des urls mega/longue/et/super/galere/a/maintenir avec un seul point d'entrée principal qui règnera en maître absolu dans votre documentation d'API et qui fera ramer votre Swagger.

Non pas que toutes ces bonnes pratiques soient innées, ce document m'a grandement aidé dans mon apprentissage.

Conclusion

Personnaliser les routeurs est parfois nécessaire pour répondre à des besoins spécifiques. Toutefois, il est préférable que ces besoins ne soient pas trop complexes. Cette personnalisation n'est pas parfaite. Elle peut notamment nous obliger à multiplier le nombre de routeurs. Poussée à l'extrême, elle peut même devenir inmaintenable. Il convient donc d'en limiter son usage. Toutefois, si la personnalisation des routeurs est utilisée convenablement, les routeurs peuvent nous dévoiler tous leurs potentiels.

À venir

Voilà, notre périple s'achève (enfin !) ici pour les routeurs. Le prochain article sera dédié au versioning.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
La Haute-Garonne met à l’honneur l’application Geotrek dans son magazine 08/06/2021

Le magazine Haute-Garonne du mois de mai-juin 2021 présente le site marando.haute-garonne.fr

Comment migrer vers une version récente de Django ? Comment migrer vers une version récente de Django ? 15/04/2021

Que ce soit pour avoir les dernières fonctionnalités ou les correctifs de sécurité, rester sur ...

Créer un tag d'inclusion avec paramètres dans Django 22/12/2020

La bibliothèque de tags interne permet d'enregistrer des tags avec paramètres ou des tags ...

Présentation de django-admin-watchdog Présentation de django-admin-watchdog 12/11/2020

Comment garder une trace des erreurs Django en toute simplicité.

Présentation de django-tracking-fields Présentation de django-tracking-fields 03/11/2020

Suivi de modification d'objets Django