Créer une API REST avec Django Rest Framework

Découvrez comment créer une API REST avec Django Rest Framework.

Publié le par Gabriel Trouvé (mis à jour le )
170 minutes

BROUILLON, ARTICLE EN COURS DE REDACTION (CORRIGER LES FAUTES ETC...)

J'ai récement publié un article : Créer une API REST en Python. Dans ce guide, nous allons nous intéressé à Django Rest Framework (on l'appelera DRF), largement utilisé pour le développement d'APIs. Et si vous connaissez déjà Django, 80% du travaille est déjà fait. 😎

Mais pourquoi utiliser Django Rest Framework plutôt que Django et ses templates ?
Vous pourriez avoir besoin de communiquer avec un front-end Vue JS ou React, ou communiquer avec d'autres applications.

Qu'est-ce que DRF ?
Il s'agit d'une surcouche de Django extrêmement complète. DRF utilise donc l'ORM de Django, l'administration, la sécurité, les modèles... tout en y ajoutant ce qu'il faut pour créer une API.

Dans ce guide nous allons développer une API pour illustrer les différents concepts.

Les fonctionnalités phares

  • DRF génère une interface web qui permet de tester son API directement dans une interface web

  • DRF fournit des classes pour la sérialisation afin de convertir les objets Python au format JSOn et vice-versa

  • DRF intègre les fonctionnalités de Django et permet aussi d'utiliser l'authentification via tokens JWT

  • DRF met à disposition des décorateurs ou des classes pour créer des vues d'API

Le projet fil conducteur
Nous allons construire une API de système de gestion de bibliothèque. Un exemple assez simple et pratique pour comprendre les différents concepts que nous allons aborder.

Les bases du projet

Tout d'abord, j'ai créé un dossier avec un environnement virtuel dans lequel j'ai installé DRF : pip install djangorestframework. Si Dango n'est pas encore installé il le sera automatiquement.

J'ai ensuite initialisé un projet avec django-admin startproject project ., puis généré une application library avec python manage.py startapp library.

Nous utiliserons ces modèles :

from django.db import models
from django.contrib.auth import get_user_model


User = get_user_model()

class Author(models.Model):
    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)

    def __str__(self):
        return self.title

class Loan(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    borrow_date = models.DateField(auto_now_add=True)
    return_date = models.DateField(null=True, blank=True)

    def __str__(self):
        return f"{self.user.username} a emprunté {self.book.title}"
PYTHON

Il faut aussi penser à enregistrer les modèles dans le fichier admin.py :

from django.contrib import admin
from .models import Author, Book, Loan


admin.site.register(Author)
admin.site.register(Book)
admin.site.register(Loan)
PYTHON

Il faut penser à ajouter notre application et DRF dans le fichier settings.py :

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # DRF et application library
    'rest_framework',
    'library',
]
PYTHON

Il ne reste plus qu'à exécuter les migrations et de créer un super utilisateur :

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
BASH

Les Serializers

Les serializers sont des classes très puissantes qui vous permettent de :

  • Désérialiser des données, elles transforment les données JSON en objets Python

  • Sérialiser les données, elles transforment les objets Python en JSON

  • Valider les données

  • Modifier ou enrichir les données avant l'envoi

La classe Serializer

C'est la classe de base qui vous oblige à redéfinir chaque champ. Créez le fichier serialiers.py dans l'application library, puis codez votre premier serializer :

from rest_framework import serializers
from .models import Author


class AuthorSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(max_length=255)

    def create(self, validated_data):
        return Author.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name = validated_data.get("name", instance.name)
        instance.save()
        return instance
PYTHON

Les champs définissent le type et les contraintes des données attendues par le serializer. Ce qui permet une validation automatique avant traitement. Les méthodes create et update permettent de définir les logiques de création et de mise à jour pour Author.

Utilisation avec une vue de liste

Pour illustrer notre serializer, nous allons l'utiliser dans une vue très simple. Nous reviendrons sur les vues plus tard. Dans le fichier views.py :

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Author
from .serializers import AuthorSerializer


@api_view(["GET", "POST"])
def author_list_and_create(request):
    if request.method == "GET":
        authors = Author.objects.all()
        serializer = AuthorSerializer(authors, many=True)

        return Response(serializer.data)

    elif request.method == "POST":
        serializer = AuthorSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
PYTHON

Nous avons créé une vue d'API qui permet d'utiliser les méthodes GET et POST : @api_view(["GET", "POST"]).

  • La méthode GET récupère tous les auteurs de la base de données, les sérialise (many=True pour une liste d'objets) et retourne les données avec un statut 200 par défaut

  • La méthode POST désérialise les données reçues, les valide avec le serializer, puis sauvegarde en base si valide avec un statut 201 ou retourne les erreurs avec un statut 400. La méthode serializer.save() appelle automatiquement la méthode create du serializer

Maintenant, dans urls.py ajoutons une URL pour utiliser notre vue. Pour rester simple, nous utilisons le urls.py du dossier project :

from django.contrib import admin
from django.urls import path
from library.views import author_list_and_create

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authors/', author_list_and_create, name='author-list-create'),
]
PYTHON

Maintenant, allons dans l'administration Django pour créer deux auteurs.

Création des auteurs dans l'administration

Création des auteurs dans l'administration

Maintenant vous pouvez lancer le serveur avec python manage.py runserver et aller sur cette URL : http://127.0.0.1:8000/authors/.

Récupération des auteurs avec la méthode GET

Récupération des auteurs avec la méthode GET

[
    {
        "id": 1,
        "name": "Patrick"
    },
    {
        "id": 2,
        "name": "Sebastien"
    }
]
JSON

Nous récupérons bien les deux auteurs avec la méthode GET. Mais comme nous avons autorisé la méthode POST, vous remarquerez qu'il est aussi possible d'envoyer des données.

Champ pour envoyer des données

Champ pour envoyer des données

Maintenant, nous pouvons créer un nouvel auteur :

Données pour créer le nouvel auteur

Données pour créer le nouvel auteur

Réponse après la création

Réponse après la création

On remarque bien une réponse 201 avec notre auteur.

{
    "id": 3,
    "name": "Gabriel"
}
JSON

Utilisation avec une vue de détail

Maintenant on va faire travailler la méthode update de notre serializer tout en ajoutant une vue de détail :

# views.py
@api_view(["GET", "PUT", "DELETE"])
def author_detail(request, pk):
    try:
        author = Author.objects.get(pk=pk)
    except Author.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == "GET":
        serializer = AuthorSerializer(author)
        return Response(serializer.data)

    elif request.method == "PUT":
        serializer = AuthorSerializer(author, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == "DELETE":
        author.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
PYTHON

Pensez à ajouter l'url : path('author/<int:pk>/', author_detail, name='author-detail'),.

Cette vue va gérer les opérations sur un auteur spécifique. Il est identifé par sa clé primaire. On essaye de le récupérer dans la base de données, en cas d'échec on retourne une erreur 404.

Ensuite, plusieurs actions possibles selon la méthode HTTP :

  • GET pour sérialiser l'auteur trouvé avec un statut 200 implicite

  • PUT pour mettre à jour l'auteur avec serializer.save() qui appelle la méthode update, car une instance est fournie contrairement à POST. Si les données sont invalides on retourne les erreurs avec un statut 400

  • DELETE supprime l'auteur de la base de données et retourne un statut 204

De manière générale, il préférable de spécifier le statut lorsqu'il est différent du 200.

Il est temps de se rendre sur notre URL pour récupérer l'auteur avec la clé primaire 1 : http://127.0.0.1:8000/author/1/.

Vue de détail

Vue de détail

On récupère bien les informations, et nous avons en plus les boutons DELETE et PUT. Je vais modifier le nom de Patrick en Patrick le Patoche.

Modification du nom

Modification du nom

À noter

Ne cherchez pas à modifier l'id, Django ignora cette modification.

Tant qu'à faire, autant tester la suppression :

Suppression de Patrick le Patoche

Suppression de Patrick le Patoche

Les méthodes de validation

Avant que les données ne soient sauvegardées en base de données, il est possible de les contrôler via le système de validation intégré aux serializers. Pour ce qui utilisent déjà Django et les formulaires, vous ferez vite le rapprochement avec la méthode clean_nom_du_champ.

En effet, il est possible de définir une méthode pour contrôler un champ spécifique. On voudrait par exemple s'assurer qu'un nom d'auteur commence obligatoirement par une majuscule :

class AuthorSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(max_length=255)

    def create(self, validated_data):
        return Author.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name = validated_data.get("name", instance.name)
        instance.save()
        return instance

    def validate_name(self, value):
        if not value[0].isupper():
            raise serializers.ValidationError("Le nom doit commencer par une majuscule.")
        return value
PYTHON

Si j'essaye de créer un utilisateur de cette manière via l'URL http://127.0.0.1:8000/authors/ :

    {
        "name": "robert"
    }
JSON

J'ai bien une erreur de validation avec en clé, le nom du champ et en valeur une liste avec mon erreur :

{
    "name": [
        "Le nom doit commencer par une majuscule."
    ]
}
JSON

Nous verrons qu'il est possible de valider plusieurs champs ensemble avec la méthode validate. Ce qui peut être pratique si la validité d'un champ dépend d'un autre. Nous verrons un exemple un peu plus tard avec un ModelSerializer.

La classe ModelSerializer

Quand nous travaillons avec des modèles, redéfinir chaque champ est fastidieux. DRF propose la classe ModelSerializer qui génère automatiquement les champs à partir d'un modèle Django.

À noter

Les méthodes de validation que nous avons vu précédemment fonctionnement également avec ModelSerializer.

Nous allons maintenant refactoriser notre serializer :

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = '__all__'
        # On pourrait spécifier une liste de champs au lieu de '__all__'
        # fields = ['id', 'name']
        # On peut aussi utiliser exclude pour exclure certains champs
        # exclude = ['mon_champ']
PYTHON

DRF génère automatiquement les champs avec leurs types et contraintes en fonction du modèle. Vous n'avez plus besoin de définir les méthodes create et update, c'est géré automatiquement 😎.

Toujours dans serializers.py, nous allons maintenant créer des serializers pour Book et Loan. Pour avoir un exemple très complet, nous allons définir une méthode de validation pour Loan.

class BookSerializer(serializers.ModelSerializer):

    class Meta:
        model = Book
        fields = ['id', 'title', 'author']
PYTHON
class LoanSerializer(serializers.ModelSerializer):
    class Meta:
        model = Loan
        fields = ['id', 'user', 'book', 'borrow_date', 'return_date']

    def validate(self, data):
        # Interdire de créer un emprunt avec une date de retour
        if self.instance is None and data.get("return_date"):
            raise serializers.ValidationError("Impossible de créer un emprunt déjà retourné.")

        # Vérifier qu'on ne peut pas emprunter un livre déjà emprunté (uniquement à la création)
        if self.instance is None:
            existing_loan = Loan.objects.filter(book=data.get("book"),
                                                return_date__isnull=True).exists()
            if existing_loan:
                raise serializers.ValidationError("Ce livre est déjà emprunté.")

        # Vérifier que la date de retour est après la date d'emprunt (uniquement en UPDATE)
        if self.instance and data.get("return_date"):
            if data["return_date"] < self.instance.borrow_date:
                raise serializers.ValidationError("La date de retour doit être après la date d'emprunt.")

        return data
PYTHON

Ici nous implémentons la méthode validate pour :

  • Refuser qu'un emprunt soit créé avec une return_date à la création

  • Vérifier qu'un livre n'est pas déjà emprunté (à la création)

  • Vérifier qu'en cas de modification la date de retour est cohérente avec la date d'emprunt

À noter

Le champ borrow_date est automatiquement passé en lecture seule, car c'est un champ avec auto_now_add : borrow_date = models.DateField(auto_now_add=True).

Les champs personnalisés avec SerializerMethodField

Parfois, on peut vouloir ajouter des champs calculés qui n'existent pas dans le modèle. Par exemple, en ajoutant une méthode pour afficher le statut d'un emprunt :

class LoanSerializer(serializers.ModelSerializer):
    status = serializers.SerializerMethodField()

    class Meta:
        model = Loan
        fields = ['id', 'user', 'book', 'borrow_date', 'return_date', 'status']

    def get_status(self, obj):
        return "Retourné" if obj.return_date else "En cours"

    def validate(self, data):
        # Méthode existante
PYTHON

Pour illustrer notre LoanSerializer, il va falloir nous créer une petite vue très simple pour l'exemple :

# views.py
# Ne pas oublier les imports si besoin
@api_view(["GET"])
def loan_list(request):
    loans = Loan.objects.all()
    serializer = LoanSerializer(loans, many=True)
    return Response(serializer.data)
PYTHON
# urls.py
from django.contrib import admin
from django.urls import path
from library.views import author_list_and_create, author_detail, loan_list

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authors/', author_list_and_create, name='author-list-create'),
    path('author/<int:pk>/', author_detail, name='author-detail'),
    path('loans/', loan_list, name='loan-list'),
]
PYTHON

Avant de continuer nous allons créer un livre et un emprunt dans l'administration Django :

Création d'un livre dans l'administration Django

Création d'un livre dans l'administration Django

Création d'un emprunt dans l'administration Django

Création d'un emprunt dans l'administration Django

Maintenant, si je me rends sur http://127.0.0.1:8000/loans/, je peux voir le statut de l'emprunt grâce à notre SerializerMethodField.

Affichage de l'emprunt via l'API

Affichage de l'emprunt via l'API

Une propriété dans le modèle

Dans ce cas, plutôt que d'avoir un SerializerMethodField, il serait préférable de définir ce type de logique dans le modèle avec une @property :

class Loan(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    borrow_date = models.DateField(auto_now_add=True)
    return_date = models.DateField(null=True, blank=True)

    @property
    def status(self):
        return "Retourné" if self.return_date else "En cours"

    def __str__(self):
        return f"{self.user.username} a emprunté {self.book.title}"
PYTHON

On peut maintenant simplifier le serializer en spécifiant juste 'status' dans fields :

class LoanSerializer(serializers.ModelSerializer):

    class Meta:
        model = Loan
        fields = ['id', 'user', 'book', 'borrow_date', 'return_date', 'status']

    def validate(self, data):
        # Méthode existante
PYTHON

En retournant sur http://127.0.0.1:8000/loans/ on aura exactement le même résultat. DRF détecte les @property et les sérialise, pratique non ?!

À noter

Quand la logique est liée à la logique métier du modèle ou que l'on veut réutiliser la méthode ailleurs, une @property est plus appropriée.
Par contre, pour un usage lus temporaire, ou que l'on a besoin du contexte de la requête on préferera un SerializerMethodField.

À vous de jouer

Voici comment récupérer la requête dans les serializers (avec une méthode fictive) :

class AuthorSerializer(serializers.ModelSerializer):

    foo= serializers.SerializerMethodField()

    class Meta:
        model = Author
        fields = '__all__'

    def get_foo(self, obj):
        request = self.context.get('request')
        if request.user.is_staff:
            # logique pour les utilisateurs staff
        # logique pour les autres utilisateurs
PYTHON

Il faudrait bien penser à passer la requête depuis la vue :

@api_view(["GET", "POST"])
def author_list_and_create(request):
    if request.method == "GET":
        authors = Author.objects.all()
        serializer = AuthorSerializer(authors, many=True, context={'request': request})

        return Response(serializer.data)
PYTHON

Avant de passer à la suite, essayer d'implémenter vous-même un SerializerMethodField qui permet de savoir si j'ai emprunté un livre. Sinon, passer directement à la suite, nous allons le faire ensemble.

Commençons par implémenter une méthode au BookSerializer :

class BookSerializer(serializers.ModelSerializer):

    is_borrowed_by_me = serializers.SerializerMethodField()

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'is_borrowed_by_me']

    def get_is_borrowed_by_me(self, obj):
        request = self.context.get("request")
        if request and request.user.is_authenticated:
            return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists()
        return False
PYTHON

Puis ajouter une vue simple pour afficher les livres, tout en passant la requête au serializer :

# views.py
# Penser aux imports manquants si besoin
@api_view(["GET"])
def book_list(request):
    books = Book.objects.all()
    serializer = BookSerializer(books, many=True, context={'request': request})
    return Response(serializer.data)
PYTHON

On ajoute la vue à urls.py :

# urls.py
from django.contrib import admin
from django.urls import path
from library.views import author_list_and_create, author_detail, loan_list, book_list

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authors/', author_list_and_create, name='author-list-create'),
    path('author/<int:pk>/', author_detail, name='author-detail'),
    path('loans/', loan_list, name='loan-list'),
    path('books/', book_list, name='book-list'),
]
PYTHON

J'avais déjà un emprunt en cours, et j'ai ajouté un livre dans l'administration. Quand je me rends sur l'URL http://127.0.0.1:8000/books/ :

Vue d'API pour les livres

Vue d'API pour les livres

Gérer les relations entre modèles

Le modèle Book a une relation ForeignKey vers Author, et DRF permet de représenter cette relation dans l'API de différentes manières.

Le comportement par défaut

Actuellement, avec le BookSerializer nous affichons l'ID de l'auteur :

Représentation d'un livre par défaut

Représentation d'un livre par défaut

Léger et rapide, facile à utiliser pour créer ou modifier un livre.

PrimaryKeyRelatedField pour plus de contrôle

Comme vu précédemment, le Serializer génère automatiquement le clé primaire de l'auteur. Mais il est possible de le définir explicitement pour avoir plus de contrôle :

class BookSerializer(serializers.ModelSerializer):

    author = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'is_borrowed_by_me']
PYTHON

read_only permet de rendre le champ en lecture seule.

class BookSerializer(serializers.ModelSerializer):

    author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.filter(is_active=True))
    is_borrowed_by_me = serializers.SerializerMethodField()

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'is_borrowed_by_me']
PYTHON

Bien que réellement nous n'avons pas de champ is_active dans le modèle Author, si ce champ était présent, on pourrait définir quels auteurs sont autorisés lors de la création/modification du livre. Le queryset ne filtre pas les livres, mais bien les auteurs.

Afficher le nom de l'auteur

Il est possible d'utiliser ce que l'on appelle une StringRelatedField, qui utilise la représentation en chaîne de caractères via la méthode __str__ de Author :

class BookSerializer(serializers.ModelSerializer):

    author = serializers.StringRelatedField()
    is_borrowed_by_me = serializers.SerializerMethodField()

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'is_borrowed_by_me']

    def get_is_borrowed_by_me(self, obj):
        # Méthode existante
PYTHON
Affichage du nom des auteurs

Affichage du nom des auteurs

Lisible mais en lecture seule uniquement, vous ne pourrez pas créer des livres de cette manière.

Objet complet, le Nested Serializer

Certainement l'approche la plus pratique : utiliser un serializer pour la lecture, et un autre pour l'écriture.

On utilisera un Serializer pour la lecture :

class BookListSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)
    is_borrowed_by_me = serializers.SerializerMethodField()

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'is_borrowed_by_me']

    def get_is_borrowed_by_me(self, obj):
        request = self.context.get("request")
        if request and request.user.is_authenticated:
            return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists()
        return False
PYTHON

Ici nous utilisons le serializer AuthorSerializer dans le BookListSerializer. Modifions la vue :

@api_view(["GET"])
def book_list(request):
    books = Book.objects.all()
    serializer = BookListSerializer(books, many=True, context={'request': request})
    return Response(serializer.data)
PYTHON
Affichage de l'auteur de manière imbriquée

Affichage de l'auteur de manière imbriquée

À côté de ça on aurait un serializer pour la création/modification :

class BookCreateUpdateSerializer(serializers.ModelSerializer):

    class Meta:
        model = Book
        fields = ['id', 'title', 'author']
PYTHON

Et dans la vue on utiliserait selon les deux serializers en fonction de la méthode utilisée :

# views.py
@api_view(["GET", "POST"])
def book_list(request):
    if request.method == "GET":
        books = Book.objects.all()
        serializer = BookListSerializer(books, many=True, context={'request': request})
        return Response(serializer.data)

    elif request.method == "POST":
        serializer = BookCreateUpdateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
PYTHON

Les vues

DRF propose plusieurs niveaux d'abstraction concernant les vues. Nous avons déjà évoqué les vues avec le décorateur @api_view pour illustrer les serializers. C'est simple, mais il y a pas mal de code répétitif. Nous ne reviendrons pas dessus (n'hésitez pas à revenir sur les @api_view que nous avons codé précédemment).

La classe APIView

Ce n'est que mon avis personnel, mais tant qu'à redéfinir mes méthodes HTTP, autant utiliser l'APIView. Très semblable à ce que l'on a fait différemment mais avec une classe. Ce qui donne tout de suite un côté plus organisé à votre code.

Commençons par remplacer notre ancienne vue author_list_and_create.

# views.py
# Nouvel import
from rest_framework.views import APIView

class AuthorListCreateView(APIView):

    def get(self, request):
        authors = Author.objects.all()
        serializer = AuthorSerializer(authors, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = AuthorSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
PYTHON

Mettons à jour le fichier urls.py :

from django.contrib import admin
from django.urls import path
from library.views import author_detail, loan_list, book_list, AuthorListCreateView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authors/', AuthorListCreateView.as_view(), name='author-list-create'),
    path('author/<int:pk>/', author_detail, name='author-detail'),
    path('loans/', loan_list, name='loan-list'),
    path('books/', book_list, name='book-list'),
]
PYTHON

Et pour la vue de détail :

# Nouvel import
from django.shortcuts import get_object_or_404

class AuthorDetailView(APIView):

    def get(self, request, pk):
        author = get_object_or_404(Author, pk=pk)
        serializer = AuthorSerializer(author)
        return Response(serializer.data)

    def put(self, request, pk):
        author = get_object_or_404(Author, pk=pk)
        serializer = AuthorSerializer(author, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        author = get_object_or_404(Author, pk=pk)
        author.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
PYTHON

Puis mettons à jour urls.py :

from django.contrib import admin
from django.urls import path
from library.views import loan_list, book_list, AuthorListCreateView, AuthorDetailView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('authors/', AuthorListCreateView.as_view(), name='author-list-create'),
    path('author/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'),
    path('loans/', loan_list, name='loan-list'),
    path('books/', book_list, name='book-list'),
]
PYTHON

Le code est déjà un peu mieux organisé, mais il reste encore pas mal de code répétitif. Passons à la suite.

Les vues génériques

DRF propose des vues qui permettent de réduire le code. Commençons par importer from rest_framework import generics dans views.py.

ListCreateAPIView

On remplace notre AuthorListCreateView par :

class AuthorListCreateView(generics.ListCreateAPIView):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
PYTHON

DRF gère automatiquement :

  • La récupération de la liste

  • La sérialisation

  • La création avec validation

  • Les codes de status

De plus, lorsque vous accéder à l'endpoint via le navigateur, DRF affiche un formulaire avec le nom des champs du modèle !

Formulaire pré-rempli

Formulaire pré-rempli

RetrieveUpdateDestroyAPIView

Vous vous doutez que nous allons parler de la vue de détail :

class AuthorDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
PYTHON

La classe permet de gérer GET, PUT, PATCH et DELETE sur un objet spécifique.

Je vous invite à regarder cette partie de la documentation de DRF.

Page de la documentation

Page de la documentation

Personnalisation des vues génériques

Il est très simple de personnaliser ces vues. On pourrait par exemple utiliser différents serializers selon l'action :

class BookListCreateView(generics.ListCreateAPIView):
    queryset = Book.objects.all()

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return BookCreateUpdateSerializer
        return BookListSerializer
PYTHON

Ou encore filtrer les résultats pour n'afficher que des umprunts d'un utilisateur en question :

class UserLoansView(generics.ListAPIView):
    serializer_class = LoanSerializer

    def get_queryset(self):
        # Retourne uniquement les emprunts de l'utilisateur connecté
        return Loan.objects.filter(user=self.request.user)
PYTHON

Avant de passer à la suite, amusez-vous à implémenter les vues pour Book et Loan. Sachant que pour Book nous avons deux serializers. Petite astuce, plus besoin de passer la reuqête en contexte au serializer les livres, les vues génériques le font automatiquement.

class BookListCreateView(generics.ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookListSerializer

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return BookCreateUpdateSerializer
        return BookListSerializer
PYTHON
class BookDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookCreateUpdateSerializer
PYTHON
class LoanListCreateView(generics.ListCreateAPIView):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
PYTHON
class LoanDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
PYTHON

Pensez aux URLs :

# Exemple pour les livres
path('books/', BookListCreateView.as_view(), name='book-list-create'),
path('book/<int:pk>/', BookDetailView.as_view(), name='book-detail'),
PYTHON

Les ModelViewSets

Et si je vous disais que l'on pouvait regrouper les opérations CRUD (Create, Read, Update, Delete) dans une seule classe ?

Implémentons des ModelViewSet pour le nos modèles :

from rest_framework import viewsets


class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
PYTHON
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()

    def get_serializer_class(self):
        if self.action in ["create", "update", "partial_update"]:
            return BookCreateUpdateSerializer
        return BookListSerializer
PYTHON
class LoanViewSet(viewsets.ModelViewSet):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
PYTHON

Maintenant nous allons utiliser un router dans urls.py :

# urls.py
# Nettoyons notre fichier pour n'utiliser que les routers
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from library.views import AuthorViewSet, BookViewSet, LoanViewSet

router = routers.DefaultRouter()
router.register(r'authors', AuthorViewSet)
router.register(r'books', BookViewSet)
router.register(r'loans', LoanViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
]
PYTHON

Si je me rends à l'adresse http://127.0.0.1:8000/api/ :

Liste des endpoints API

Liste des endpoints API

Par exemple, pour l'auteur, vous pouvez vous rendre sur http://127.0.0.1:8000/api/authors/ pour la liste et la création d'une instance, ou pour le détail http://127.0.0.1:8000/api/authors/2/.

Il est aussi possible d'ajouter des endpoints personnalisés dans les ModelViewSet avec le décorateur @action :

from rest_framework.decorators import action


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()

    def get_serializer_class(self):
        if self.action in ["create", "update", "partial_update"]:
            return BookCreateUpdateSerializer
        return BookListSerializer

    @action(detail=True, methods=["get"])
    def loans(self, request, pk=None):
        # detail=True car on accède à un objet spécifique
        # methods pour les méthodes autorisées
        """
        Récupérer tous les emprunts associés à un livre spécifique.
        """
        book = self.get_object()
        loans = Loan.objects.filter(book=book)
        serializer = LoanSerializer(loans, many=True)
        return Response(serializer.data)
PYTHON

Maintenant, comme j'ai un emprunt, si je me rends sur http://127.0.0.1:8000/api/books/1/loans/ :

action personnalisée

action personnalisée

Filtrer et rechercher

Pour filtrer, chercher et trier des résultats, je vous conseille d'installer django-filter.

Configuration

Commençons par installer la librairie :

pip install django-filter
BASH

Ensuite nous allons dans le fichier settings.py pour ajouter django_filters dans les applications et la constante REST_FRAMEWORK :

# settings.py

# Code existant
...

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # DRF et application library
    'rest_framework',
    'django_filters',
    'library',
]

# Code existant
....

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        # Filtrage via django-filters
        'django_filters.rest_framework.DjangoFilterBackend',
        # Recherche textuelle
        'rest_framework.filters.SearchFilter',
        # Tri des champs
        'rest_framework.filters.OrderingFilter',
    ],
}
PYTHON
  • DjangoFilterBackend : Permet de filtrer par valeurs exacts (ex: ?author=1)

  • SearchFilter : Recherche textuelle partielle dans plusieurs champs (ex: ?search=django)

  • OrderingFilter : Permet de trier les résultats (ex: ?ordering=title ou ?ordering=-id pour ordre décroissant)

Il s'agit ici d'une configuration globale, mais il est aussi possible de redéfinir la configuration pour chaque vue.

Utilisation dans les vues

Si nous reprenons la vue BookViewSet :

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()

    filterset_fields = ['author']  # Filtrage par auteur
    search_fields = ['title', 'author__name']    # Recherche textuelle sur le titre et le nom de l'auteur
    ordering_fields = ['title', 'author__name']  # Tri par titre et nom d'auteur

    def get_serializer_class(self):
        if self.action in ["create", "update", "partial_update"]:
            return BookCreateUpdateSerializer
        return BookListSerializer

    @action(detail=True, methods=["get"])
    def loans(self, request, pk=None):
        """
        Récupérer tous les emprunts associés à un livre spécifique.
        """
        book = self.get_object()
        loans = Loan.objects.filter(book=book)
        serializer = LoanSerializer(loans, many=True)
        return Response(serializer.data)
PYTHON

Utilisation de l'attribut filterset_fields : http://127.0.0.1:8000/api/books/?author=2.

Exemple filterset_fields

Exemple filterset_fields

Un exemple pour l'attribut search_fields : http://127.0.0.1:8000/api/books/?search=django.

Exemple de recherche avec le titre d'un livre

Exemple de recherche avec le titre d'un livre

Ordonné par titre

Ordonné par titre

Et un exemple avec le nom de l'auteur : http://127.0.0.1:8000/api/books/?ordering=author__name.

Ordonné via le nom de l'auteur

Ordonné via le nom de l'auteur

Utilisation avancée des filtres

Pour permettre les comparaisons de dates, des plages de valeur, ou des filtres booléens, il faut créer des FilterSet personnalisés. En effet, nos précédents filtres permettaient des correspondances exacts.

Commençons par créers le fichier filters.py et notre premier filtre personnalisé :

# filters.py
from django_filters import rest_framework as django_filters
from .models import Loan


class LoanFilterSet(django_filters.FilterSet):
    borrow_date_after = django_filters.DateFilter(
        field_name="borrow_date", lookup_expr="gte"
    )

    borrow_date_before = django_filters.DateFilter(
        field_name="borrow_date", lookup_expr="lte"
    )

    is_active = django_filters.BooleanFilter(
        field_name="return_date", lookup_expr="isnull"
    )

    class Meta:
        model = Loan
        fields = ["user", "book"]
PYTHON

Les attributs déclarés dans la classe n'ont pas besoin d'être listés dans fields. En spécifiant user et book, on génère automatiquement deux filtres supplémentaires basés sur les champs du modèle Loan.
On utilisera donc fields pour la génération de filtres simples.

Retournons dans views.py pour utiliser notre FilterSet :

# views.py
# Autres imports
from .filters import LoanFilterSet


class LoanViewSet(viewsets.ModelViewSet):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
    filterset_class = LoanFilterSet
PYTHON

Si je me rends sur l'url http://127.0.0.1:8000/api/loans/ :

Utilisation de la vue LoanViewSet

Utilisation de la vue LoanViewSet

Filtre is_active

Filtre is_active

J'ai rajouté un emprunt dans ma base de donnée avec un date d'emprunt au 1er janvier 2024, donc si je vais sur l'URL http://127.0.0.1:8000/api/loans/ :

Liste des emprunts

Liste des emprunts

Si j'applique le filtre borrow_date_after du FilterSet http://127.0.0.1:8000/api/loans/?borrow_date_after=2025-01-01 :

Filtre borrow_date_after

Filtre borrow_date_after

On pourrait aussi combiner plusieurs filtres pour récupérer les emprunts actifs après une certaine date : http://127.0.0.1:8000/api/loans//loans/?user=1&is_active=true&borrow_date_after=2025-01-01.

Vous pouvez aussi utiliser les filtres depuis l'interface de l'API :

Bouton pour les filtres

Bouton pour les filtres

Utilisation des filtres depuis l'interface

Utilisation des filtres depuis l'interface

Pagination

La pagination est très importante, car sans elle, votre API peut retourner des milliers d'objets en une requête. La pagination permet alors de diviser les résultats en pages plus petites et plus rapides à charger.

Configuration globale

Dans le fichier de settings.py ajoutons la pagination à la constante REST_FRAMEWORK :

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        # Filtrage via django-filters
        'django_filters.rest_framework.DjangoFilterBackend',
        # Recherche textuelle
        'rest_framework.filters.SearchFilter',
        # Tri des champs
        'rest_framework.filters.OrderingFilter',
    ],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}
PYTHON

Toutes les listes sont paginées avec 10 éléments par page. Je vais aller créer plusieurs auteurs dans l'administration. Et quand j'affiche les auteurs j'ai bien une pagination :

Pagination

Pagination

Personnaliser la pagination

Il est aussi possible de créer une classe pour personnaliser la pagination. Commençons par créer un fichier pagination.py dans l'application library :

# pagination.py
from rest_framework.pagination import PageNumberPagination


class CustomPagination(PageNumberPagination):
    page_size = 10                      # Nombre d'éléments par défaut
    page_size_query_param = 'page_size' # Permet au client de changer la taille
    max_page_size = 100                 # Limite maximale
PYTHON

Maintenant utilisons cette classe avec la vue AuthorViewSet :

# views.py
# autres imports
from .pagination import CustomPagination


class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    pagination_class = CustomPagination
PYTHON

En accédant à l'URL http://127.0.0.1:8000/api/authors/?page=2&page_size=20, on affiche la page 2 avec 20 éléments :

Page 2, on affiche 20 éléments

Page 2, on affiche 20 éléments

À noter

À savoir qu'il est possible d'utiliser un autre style de pagination. Je vous invite à vous rendre sur cette partie de la documentation : https://www.django-rest-framework.org/api-guide/pagination/.

Authentification

Par défaut, l'API est accessible à tout le monde. L'authentification permet d'identifier qui fait la requête. DRF propose plusieurs systèmes d'authentification :

  • SessionAuthentification : réutilise le même système de session Django mais l'adapte pour fonctionner les vues DRF. Pratique si on ajoute des endpoints pour HTMX avec une application Django MVT existante

  • TokenAuthentication : Token simple intégré à DRF (basique, sans expiration)

  • JWT (JSON Web Token) : LE standard moderne avec expiration et refresh

Pour cet article, nous allons nous concentrer sur JWT (Simple JWT) car :

  • C'est le standard pour les APIs modernes

  • Gère l'expiration automatiquement

  • Pas de stockage en base

  • Indispensable pour le mobile et des frontend colmme Vue JS ou React

Session Authentication

L'authentification par session utilise les cookies Django traditionnels. C'est utile si votre API est consommée par un frontend Django classique (templates) sur le même domaine.

Il faudrait utiliser cette configuration dans le fichier settings.py :

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
    ],
}
PYTHON

Le cas d'usage typique serait d'appeler une vue DRF dans un projet Django MVT pour la combiner à HTMX. Mais pour de simples endpoints HTMX, dans ce cas JsonResponse Django natif suffit. SessionAuthentication devient vraiment utile si vous souhaitez bénéficier de l'écosystème DRF ou si vous prévoyez d'ajouter d'autres méthodes d'authentification plus tard.

Token Authentification DRF

DRF propose un système de tokens intégré. Chaque utilisateur reçoit un token unique stocké en base.

# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework.authtoken',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}
PYTHON

Il faut ensuite exécuter les migrations : python manage.py migrate.

Il suffit ensuite d'ajouter cette vue au fichier d'URLs : from rest_framework.authtoken.views import obtain_auth_token. Je passe assez vite sur ce système d'authentification pour me concentrer sur JWT par la suite, mais voici à quoi les tokens ressemblent :

Création d'un token directement depuis l'administration

Création d'un token directement depuis l'administration

Les tokens sont sans expiration et les tokens sont stockés en base de données sans mécanisme de refresh. Ce qui n'est pas idéal pour un environnement de production.

Tokens JWT avec Simple JWT

Le standard moderne pour les APIs, le token JWT offre :

  • Tokens non stockés en base de données : donc pas besoin d'exécuter une requête pour récupérer le token

  • Expiration automatique des tokens : sécurité renforcée

  • Refresh tokens : les nouveaux tokens sont récupérés de manière transparente sans redemander le mot de passe

  • Compatibilité universelle : les tokens fonctionnent avec React, Vue, Angulat, Flutter...

La librairie Simple JWT est devenue la référence pour implémenter JWT avec Django REST Framework.

Installation et configuration

Commençons par installer la librairie dans l'environnement : pip install djangorestframework-simplejwt. Puis dans le fichier settings.py :

# settings.py
INSTALLED_APPS = [
    # autres applications
    # ...
    'rest_framework_simplejwt',
]

# Et la constante REST_FRAMEWORK
# Je met ma configuration globale arrivée à ce stade
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_FILTER_BACKENDS': [
        # Filtrage via django-filters
        'django_filters.rest_framework.DjangoFilterBackend',
        # Recherche textuelle
        'rest_framework.filters.SearchFilter',
        # Tri des champs
        'rest_framework.filters.OrderingFilter',
    ],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}
PYTHON

Maintenant, nous pouvons ajouter les urls :

# urls.py
# Autres imports...
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)


urlpatterns = [
    # Autres URLs
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
PYTHON
  • TokenObtainPairView permet d'obtenir les tokens d'accès et de rafraichissement en renseignant l'username et le mot de passe, c'est maintenant votre vue de login

  • TokenRefreshView permet de renouveller le token d'accès afin de rester connecté

  • TokenVerifyView permet de vérifier la validité d'un token. Cette vue est en générale plus rare car c'est optionnel

Obtention des tokens

Rendez-vous sur http://127.0.0.1:8000/api/token/ :

Vue d'obtention des tokens

Vue d'obtention des tokens

Réponse avec les tokens

Réponse avec les tokens

Le client peut ensuite stocker ces deux tokens :

  • Access token à envoyer dans chaque requête API

  • Refesh token à utiliser pour obtenir un nouveau access token

À noter

Pour tester un endpoint protégé, en n'autorisant que les utilisateurs connectés par exemple, on peut légèrement modifier BookViewSet.
Nous reviendrons sur le concept de permissions plus tard dans cet article.

# views.py
# Autres imports
from rest_framework.permissions import IsAuthenticated


class BookViewSet(viewsets.ModelViewSet):
    # Autres attributs existants
    permission_classes = [IsAuthenticated]

    # Méthodes existantes
PYTHON

Imaginez que votre vue soit protégée par une permission (ce que l'on verra à la fin du guide), pour y accéder il faudra renseigner le token d'accès :

# Requête avec curl vers la vue pour récupérer les livres (j'ai protégé la vue pour cet exemple)
# Nous reviendrons plus tard sur les permissions
# Ici je fais une requête via mon terminal avec curl
curl http://127.0.0.1:8000/api/books/ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...suite_du_token"
BASH

À noter

L'interface web browsable de DRF ne supporte pas l'authentification JWT par défaut. Pour tester les endpoints qui nécessitent un token JWT il faut utiliser des outils comme cURL (le plus simple, ce que je montre dans l'exemple juste au-dessus).

Pour ma part j'aurais tendance à utiliser la librairie DRF-Spectacular qui permet la génération d'une interface OpenAPI.

Accès à la vue avec un Token JWT

Accès à la vue avec un Token JWT

Tentative d'accès à la vue sans Token

Tentative d'accès à la vue sans Token

Le client peut facilement récupérer un nouveau token d'accès si celui si n'est plus valide via la vue TokenRefreshView.

Permissions

Nous venons de voir l'authentification qui permet de savoir qui vous êtes. Nous allons maintenant voir les permissions qui permettent de savoir qui peut faire quoi.

Configuration globale

Dans le fichier settings.py il est possible de définir une permission par défaut :

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}
PYTHON

Essayez d'accéder à n'importe quelle vue, l'accès y sera refusé. Par exemple, si je me rends sur le router http://127.0.0.1:8000/api/ :

Tentative d'accès au router

Tentative d'accès au router

À noter

Pour la suite de ce guide, je vais retirer la permission par défaut pour utiliser les permissions directement au niveau des vues.

Permissions intégrées

Voici les permissions intégrées qu'il est possible d'importer pour vos vues (on considerera qu'ils sont importés pour les exemples suivants) :

from rest_framework.permissions import AllowAny, IsAuthenticated, \
IsAdminUser, IsAuthenticatedOrReadOnly
PYTHON

AllowAny

Sans configuration explicite de permissions dans settings.py, DRF utilise par défaut AllowAny. Tout le monde peut accéder.
Si on avait une configuration par défaut IsAuthenticated (comme au début de la section sur les permissions), on pourrait spécifier au niveau de la vue AllowAny pour permettre à tout le monde d'y accéder :

# views.py
class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    pagination_class = CustomPagination
    permission_classes = [AllowAny]
PYTHON

IsAuthenticated

Seuls les utilisateurs peuvent accéder :

# views.py
class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    pagination_class = CustomPagination
    permission_classes = [IsAuthenticated]
PYTHON

IsAdminUser

Seul les administrateurs peuvent accéder à la vue :

# views.py
class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    pagination_class = CustomPagination
    permission_classes = [IsAdminUser]
PYTHON

IsAuthenticatedOrReadOnly

L'accès aux vues est publique, mais les créations/modifications/suppressions nécessitent d'être authentifié :

# views.py
class AuthorViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    pagination_class = CustomPagination
    permission_classes = [IsAuthenticatedOrReadOnly]
PYTHON

Permissions personnalisées

Bien que les permissions standards de DRF couvrent les principaux cas d'usage, il est possible de créer ses propres permissions en héritant de BasePermission.

Les permissions personnalisées permettent d'encapsuler des règles métiers spécifiques, plutôt que d'utiliser des conditions dans chaque vue.

Les méthodes de permission

Une permission personnalisée peut implémenter deux méthodes qui sont appelées à différents moments. Créons une permission qui permet de de vérifier l'accès général à la vue : autoriser les utilisateurs authentifiés. Ensuite, pour les opérations sur un objet spécifique, nous allons restreindre les modifications uniquement au propriétaire de l'objet.

Créons un fichier permissions.py :

# permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS


class IsOwnerOrReadOnly(BasePermission):

    def has_permission(self, request, view):
        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True

        return obj.user == request.user
PYTHON

has_permission contrôle l'accès général à la vue en autorisant que les utilisateurs authentifiés. Puishas_object_permission permet de contrôler les actions sur un objet spécifique. Elle authorise la lecture pour tous les utilisateurs authentifiés (via SAFE_METHODS), mais restreint la modification uniquement au propriétaire.

Un utilisateur connecté peut consulter les emprunts, mais ne peut modifier ou supprimer que des propres emprunts.

À noter

SAFE_METHODS est une constante (voir le code source) qui contient les méthodes HTTP en lecture seule ('GET', 'HEAD', 'OPTIONS'). On peut les mettre en oppostion aux méthodes "unsafe' qui modifient les données : POST, PUT, PATCH, DELETE.

Attention

has_object_permission n'est appelée QUE pour les vues de détail. Nous verrons plus tard comment il est possible de filtrer une liste selon l'utilisateur.

Ajoutons cette permissions à notre vue :

# views.py
# importer la permissions
class LoanViewSet(viewsets.ModelViewSet):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
    filterset_class = LoanFilterSet
    permission_classes = [IsOwnerOrReadOnly]
PYTHON

Si j'essaye de supprimer un emprunt qui n'est pas à moi :

Impossible de supprimer l'emprunt

Impossible de supprimer l'emprunt

Nous avons un message d'erreur en rapport avec la permission.

Si j'essaye de supprimer mon emprunt :

Emprunt supprimé

Emprunt supprimé

Pas de message d'erreur, l'emprunt a été supprimé.

Attention

J'avais au préalable deux utilisateurs dans la base avec chacun des emprunts. Dans une application en production on est d'accord que de donner la permission à un utilisateur de supprimer un emprunt c'est dangereux (je dirais même inconscient), mais nous sommes ici dans la démonstration technique 😊.

Filtrage des listes

Il est possible de surcharger la méthode get_queryset de ModelViewSet :

# views.py
# imports

class LoanViewSet(viewsets.ModelViewSet):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
    filterset_class = LoanFilterSet
    permission_classes = [IsOwnerOrReadOnly]

    def get_queryset(self):
        user = self.request.user

        if user.is_staff:
            return Loan.objects.all()
        return Loan.objects.filter(user=user)
PYTHON

Un administrateur a accès à tous les emprunts, et un utilisateur n'a accès qu'à la liste de ses emprunts. Sachant que l'on combine ce filtre avec la permission IsOwnerOrReadOnly.

Dans ma base j'ai créé un emprunt pour l'utilisateur qui n'est pas staff gabigab118 :

Liste des emprunts

Liste des emprunts

Et quand j'essaye de récupérer la liste des emprunts avec cet utilisateur :

Emprunt de gabigab118

Emprunt de gabigab118

Je n'ai bien que son emprunt.

Voyez comment il est possible de combiner get_queryset et les permissions 😊.

Permission par actions

Mais ce n'est pas terminé... Il faut savoir qu'il est possible d'avoir des permissions différentes selon l'action :

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()

    def get_permissions(self):
        if self.action in ['list', 'retrieve']:
            # Lecture publique
            permission_classes = [AllowAny]
        elif self.action in ['create', 'update', 'partial_update']:
            # Modification nécessite authentification
            permission_classes = [IsAuthenticated]
        else:
            # Suppression réservée aux admins
            permission_classes = [IsAdminUser]

        return [permission() for permission in permission_classes]
PYTHON

Permission sur une action custom

Nous avons vu précédemment qu'il était possible d'implémenter des actions customisées dans nos ModelViewSet :

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    permission_classes = [IsAuthenticatedOrReadOnly]

    @action(detail=False, methods=['get'], permission_classes=[AllowAny])
    def available(self, request):
        # Cette action est publique même si le ViewSet nécessite l'authentification
        borrowed_books = Loan.objects.filter(
            return_date__isnull=True
        ).values_list('book_id', flat=True)
        # values_list renvoie une liste des IDs des livres empruntés

        available_books = Book.objects.exclude(id__in=borrowed_books)
        # Exclut les livres empruntés actuellement
        serializer = self.get_serializer(available_books, many=True)
        return Response(serializer.data)
PYTHON

Dans cette action, nous avons une permission publique pour l'action, alors que l'accès aux endpoints générés par le ModelViewSet ont la permission IsAuthenticatedOrReadOnly.

**Nous arrivons à la fin de ce guide qui vous donne déjà une bonne base pour vous lancer avec DRF. **

Mais avant de se quitter je vais vous donner deux conseils :

  • Optimiser les performances (ce n'est pas propore à DRF mais à Django)

  • Documenter son API

Optimisation des performances

Revenons sur le serializer le modèle Book et le serializer BookListSerializer :

class Book(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)

    def __str__(self):
        return self.title
PYTHON
class BookListSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)
    is_borrowed_by_me = serializers.SerializerMethodField()

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'is_borrowed_by_me']

    def get_is_borrowed_by_me(self, obj):
        request = self.context.get("request")
        if request and request.user.is_authenticated:
            return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists()
        return False
PYTHON

Lorsque vous retournerez la liste des livres dans vos vues, par exemple :

# views.py
# Reprenons un exemple basique
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookListSerializer
PYTHON

Dans le modèle Book, l'auteur est une clé étrangère. Donc Django va devoir faire une requête pour récupérer les livres, puis une requête PAR livre pour récupérer l'auteur. Ce qui est très coûteux en terme de performance.

Pour les relations Many-to-Many ou les reverse ForeignKey on utilisera prefetch_related. On peut voir dans le modèle Book une relation inverse avec le champ author : related_name='books'.

Ajoutons le champ books créé par la relation inverse dans le serializer :

class AuthorSerializer(serializers.ModelSerializer):

    class Meta:
        model = Author
        fields = ['id', 'name' ,'books']
PYTHON

On peut

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.