Django & DRF : DRF tips & tricks
Tags: django, drf, djangorestframework, tips
Published: 2020-11-23 19:14:01

Un article sur DRF, [Django Rest Framework](https://www.django-rest-framework.org/) (module pour créer des API), sur des aspects plus avancés de la librairie.

DRF présente un certain nombres de méthodes / fonctions qui peuvent être surchargées à des fins d'utilité, d'optimisations, etc, voyons ce que l'on peut en faire.

## Sommaire
  * ViewSet
    * [get_queryset](#get_queryset)
    * [filter_queryset](#filter_queryset)
    * [paginate_queryset / get_paginated_response](#paginate)
    * [perform_create / perform_update / perform_destroy](#perform)
    * [get_serializer_class](#get_serializer_class)
    * [get_serializer_context](#get_serializer_context)
    * [action](#action)
    * [lookup_field](#lookup_field)
  * Serializer
    * [create / update](#create_update)
  * [Conclusion et liens](#conclusion)

# get_queryset <a name="get_queryset"></a>

Dans le `ModelViewSet`, parmi les champs requis, il y a **queryset** qui précise la "requête" à exécuter pour les différentes opérations (GET, POST, ...) afin de retrouver les entités du modèle souhaité.

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

Il peut être intéressant de la construire selon le **contexte d'utilisation de l'API** ou lorsque la queryset commence à être longue. Ainsi, selon le connecté, les entités ramenées peuvent variées, par exemple : les livres de l'auteur connecté ou tous les livres lors d'une gestion d'un administrateur. 

Il suffit alors de surcharger la méthode **get_queryset** qu'appelle DRF, dans sa version par défaut, il renvoie la propriété habituelle définie dans le viewset, à savoir **queyset**

Par exemple, même si l'exemple est simple, on pourrait, si on a un rôle de gestionnaire, on renvoie tous les livres, et si, c'est un client, on ne renvoie que les livres disponibles, les règles métiers pourraient être plus complexes.

```python
class BookViewSet(viewsets.ModelViewSet):
   # ...
   def get_queryset(self):
     if self.isAdmin():
       return Book.objects.all()
     return Book.objects.filter(enabled=True)
```

ou si un auteur ne doit voir que ses livres, pour sa gestion par exemple, on peut réduire la liste de cette façon

```python
class BookViewSet(viewsets.ModelViewSet):
   # ...
   def get_queryset(self):
     qs = Book.objects.select_related('author')

     if self.isAdmin():
       return qs.all()
     if self.isAuthor():
       return qs.filter(author__user=self.request.user)
     return qs.filter(enabled=True)
```

# filter_queryset <a name="filter_queryset"></a>

**filter_queryset** est utilisé par défaut sur les listes ou l'obtention d'un objet (GET) ou lors des autres opérations (DELETE / PUT), pour détecter les paramètres de la querystring "?filtre1=valeur1&filtre2=valeur2", filtre1 / filtre2 peuvent être des champs du modèle ou des filtres personnalisés, lors de l'usage des filtres FilterSet ou filterset_fields de la viewset. 

En gros, lorsque vous faites un **/authors/?last_name=duval**, DRF appelle filter_queryset() (méthode contenue dans **GenericAPIView**, classe mère utilitaire, on a cet héritage de défini pour la ModelViewSet : GenericViewSet : GenericAPIView) qui se charge de filtrer sur le **last_name** les auteurs.

En revanche, lors de l'utilisation des **[@action](https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing)**, ce filtrage n'est pas appelé explicitement, vous perdez alors tout ce bénéfice, ce qui est dommage.

Il suffit alors de **l'appeler explicitement**, s'il y a des filtres dans la querystring, ils seront appliqués. 

Par exemple, imaginons une action qui renvoie
la liste des dossiers du connecté : application d'un filtre sur le connecté sur  la queryset par défaut de la viewset puis on applique filter_queryset sur cette dernière, s'il y a d'autres critères, ils seront appliqués :

```python
    @action(detail=False, methods=['get'])
    def mes_dossiers(self, request):
        """
        Liste des dossiers du le connecté
        """
        connecte = self.request.user
        qs = self.get_queryset().filter(user=connecte)
        dossiers = self.filter_queryset(qs)
   
        serializer = self.get_serializer(dossiers, many=True)
        return Response(serializer.data)
```
# paginate_queryset / get_paginated_response <a name="paginate"></a>

Comme pour [filter_queryset](#filter_queryset), on peut appeler explicitement la mise en place de la pagination, avec **self.paginate_queryset(qs)** sur la queryset et la création du JSON de retour avec **self.get_paginated_response(data)**, dans une @action.

Par exemple, si la pagination est demandée (le paramètre **limit** est dans la querystring, de la forme /api/books/?limit=5&offset=0) alors la pagination est constituée sinon la liste simple d'objets sera ramenée au client navigateur.

```python
    @action(detail=False)
    def get_stats(self, request):
        '''
        Obtient les stats ....
        '''
        qs = self.filter_queryset(qs)
        r = Modele.objects.by_days(qs)

        page = self.paginate_queryset(r)
        if page is not None: # on remonte la pagination
            serializer = ModeleSerializer(page, many=True)
            r = self.get_paginated_response(serializer.data)
            return r
  
        # pas de pagination demandée, on remonte une liste d'objets
        s = ModeleSerializer(r, many=True)
        return Response(s.data)
```
le self.get_paginated_response(data) forme un JSON de type

```json
{ 'count': total_lignes, 
  'next': url_next, 'previous': url_previous, 
  'results': [objets] }
```

**self.paginate_queryset()** et **self.get_paginated_response()** sont contenu dans la pagination utilisée dans la [2ère partie Django](https://dev.to/zorky/django-drf-101-partie-2-1gp5#pagination), à savoir la classe [LimitOffsetPagination](https://www.django-rest-framework.org/api-guide/pagination/#limitoffsetpagination), elles peuvent être surchargées à leur tour pour [personnaliser la pagination](https://www.django-rest-framework.org/api-guide/pagination/#custom-pagination-styles).

# perform_create / perform_update / perform_destroy <a name="perform"></a>

Ces 3 fonctions appartiennent aux "[Mixins](https://riptutorial.com/python/example/15228/mixin)" DRF (incluses dans les viewsets proposées ModelViewSet ou CreateAPIView ou ...), elles sont chargées de la création / mise à jour ou suppression effective d'un objet.

DRF appelle ces fonctions dans les **create()** / **update()** / **destroy()** des mixins, à l'intérieur de celles-ci, par exemple pour le create() (dans CreateModelMixin), perform_create() est chargée du save() de l'object et donc de sa création.

```python
class CreateModelMixin:
    # ...
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer) # ** appel **
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()
    # ...
``` 

On peut **surcharger** ses fonctions appelées à l'intérieur afin d'y apporter des comportements supplémentaires au save() ou effectuer des vérifications avant le save(). 

Ce pattern se nomme le **pattern "Hollywood"** : "Ne m'appelez pas, je vous appelerai" :) notamment utilisé pour l'**IoC** mais aussi pour créer des **hooks** ou autre usage (notamment en partie dans le design pattern Strategy), comme dans DRF, de modifier un comportement déjà implémenter, pattern que j'aime à utiliser car il donne la possibilité d'**étendre un comportement** sans modification de code.

Dans le cadre d'une API, cela peut être utile pour compléter le modèle à sauvegarder ou exécuter des actions de traitement, par exemple ajouter automatiquement l'utilisateur ayant fait l'action, ici dans le **save()** du modèle 

```python
    def perform_create(self, serializer):
        instance = serializer.save(updated_by=self.request.user.individu)
```

ou après une mise à jour, appeler une fonction pour un traitement spécial, ici **_create_or_update_destinataires(instance)**

```python
   def perform_update(self, serializer):
       """
       Hook à la modification d'une instance : creation ou modification des 
       destinataires
      """
      instance = serializer.save()
      self._create_or_update_destinataires(instance)
```

# get_serializer_class <a name="get_serializer_class"></a>

La fonction **get_serializer_class()** (également dans **GenericAPIView**) surchargée permet de préciser un autre Serializer dynamiquement selon le contexte.

En effet, l'on souhaite parfois des versions simplifiées des données ramenées, par exemple lorsque l'on liste ou qu'on édite un objet, toutes les informations ne sont pas tout le temps nécessaires, surtout s'il commence à y avoir beaucoup de données imbriquées (*nested serializers*), selon son besoin.

Le serializer détermine les données à ramener (en JSON), cela induit aussi les requêtes à exécuter pour les chercher, cela peut être une forme aussi d'optimisation.

Par exemple, dans la version 3.2 de notre tutoriel, le serializer livre BookSerializer par défaut ressemble à ceci, avec l'auteur attaché

```python
class AuthorSerializer(serializers.ModelSerializer):
    books_obj = BookSimpleSerializer(source='books', many=True, read_only=True)

    class Meta:
        model = Author
        fields = '__all__'

class BookSerializer(serializers.ModelSerializer):
    author_obj = AuthorSerializer(source='author', read_only=True)

    class Meta:
        model = Book
        fields = '__all__'
```

ce qui pour la liste des livres (/books/) génère le JSON suivant, la liste books_objs est de trop pour author_obj, pas de nécessité ici

```json
[
    {
        "id": 1,
        "author_obj": {
            "id": 14,
            "books_obj": [
                {
                    "id": 1,
                    "dt_created": "2020-02-22T15:57:26.934127",
                    "dt_updated": "2020-04-11T18:55:09.352309",
                    "name": "Django primer : ultimate guide",
                    "nb_pages": 150,
                    "enabled": true,
                    "author": 14
                }
            ],
            "dt_created": "2020-04-11T18:55:06.977351",
            "dt_updated": "2020-04-18T17:33:29.349090",
            "first_name": "Aurélie",
            "last_name": "Dudu 2"
        },
        "dt_created": "2020-02-22T15:57:26.934127",
        "dt_updated": "2020-04-11T18:55:09.352309",
        "name": "Django primer : ultimate guide",
        "nb_pages": 150,
        "enabled": true,
        "author": 14
    },
]
```

A la rigueur, ce serializer peut être le bienvenu pour afficher un livre précis, avec son auteur pour ensuite afficher liste des livres de cet auteur, pour une liste de livres, on ferait certainement autrement.

Limitons le livre à plus simple : juste son auteur attaché, via les serializers suivant

```python
class AuthorSimpleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = '__all__'

class BookAuthorSimpleSerializer(serializers.ModelSerializer):
    author_obj = AuthorSimpleSerializer(source='author', read_only=True)

    class Meta:
        model = Book
        fields = '__all__'
```

qui sera utilisé uniquement lorsque l'action de listage est appelée. 

DRF permet de détecter quelle action est appelée (pour faire court, dans le CRUD) via le self.action qui propose pour le CRUD les valeurs suivantes (des "string") (NB : si une **@action** est utilisée, le self.action contiendra le nom de cette action) : 

* GET /authors/1/ : 'retrieve' 
* PUT /authors/1/ : 'update'
* POST /authors/ : 'create'
* PATCH /authors/1/ : 'partial_update'
* GET /books/ : 'list'

on va utiliser cette possibilité dans **get_serializer_class()** et selon le type d'action renvoyer BookAuthorSimpleSerializer (list) ou la classe par défaut définie dans la viewset (serializer_class = BookSerializer) pour les autres actions, ce qui donnera :

```python
class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer
    permission_classes = [AllowAny]
    filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter,)
    filterset_class = BookFilter

    def get_serializer_class(self):
        if self.action == 'list':
            return BookAuthorSimpleSerializer

        return self.serializer_class
```

Lors d'un appel à /books/, on aura maintenant ce JSON retourné, un peu plus léger

```json
[
    {
        "id": 1,
        "author_obj": {
            "id": 14,
            "dt_created": "2020-04-11T18:55:06.977351",
            "dt_updated": "2020-04-18T17:33:29.349090",
            "first_name": "Aurélie",
            "last_name": "Dudu 2"
        },
        "dt_created": "2020-02-22T15:57:26.934127",
        "dt_updated": "2020-04-11T18:55:09.352309",
        "name": "Django primer : ultimate guide",
        "nb_pages": 150,
        "enabled": true,
        "author": 14
    },
]
``` 

# get_serializer_context <a name="get_serializer_context"></a>

**get_serializer_context()** est utilisé pour passer des valeurs au serializer de la viewset, par défaut, il est appelé dans le get_serializer (GenericAPIView) et envoie la **request**, le **format** et la vue / **view**

```python
class GenericAPIView(views.APIView):
    # ...
    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)

    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self
        }   
   # ...
```

Cela peut être utile de lui envoyer d'autres valeurs dont on aurait besoin dans le serializer lors d'un calcul

Par exemple, dans notre vue / viewset, on surcharge la fonction **get_serializer_context()**, et on lui passe une année passée en paramètre dans l'API ou celle par défaut

```python
class MaViewSet(ModelViewSet):
    # ...
    def get_serializer_context(self):
        annee = self.request.data['annee'] if 'annee' in self.request.data else get_annee()
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self,
            'annee': annee
        }
```

et dans le serializer, on peut lire cette valeur via **self.context**

```python
class MonSerializer(AnnuaireBaseSerializer):    
    propriete_obj = serializers.SerializerMethodField(read_only=True)

    def get_propriete_obj (self, obj):
        """
        on peut dès lors obtenir l'attribut "annee" avec sa valeur et potentiellement faire une requête liée à cette année, ici, le nombre de membres pour cette année
        """
        annee = '2020'
        if 'annee' in self.context:
           annee = self.context['annee']

        membres = Membres.objects.filter(annee=annee)
        return membres.count()
```

Pratique !

# action <a name="action"></a>

Les ModelViewSet permettent un CRUD (post, get, put, delete) sur un modèle, il peut intéressant de spécialisé une API à l'intérieur d'un *endpoint*, par exemple get_books_online() qui permettrait d'obtenir uniquement les *books* en ligne, c'est pour l'exemple.

Il existe 2 type d'actions qui seront représentées par l'annotation @action : celle pour renvoyer une liste ou un traitement, celle qui se base sur un objet particulier pour lequel l'id sera mis en querystring et trouvé grâce à **self.get_object()**, tout se joue au niveau du mot clé **detail** à False ou True pour le second cas.

```python
  @action(detail=False)
  def get_books_online(self, request):
     pass
```

```python
  @action(detail=True)
  def get_book(self, request, request, *args, **kwargs):
    instance = self.get_object()
    pass
```

# lookup_field <a name="lookup_field"></a>

Le **lookup_field** est la clé par défaut de recherche sur un modèle, elle est fixée habituellement à "**pk**" (l'identifiant du modèle qui est défini automatiquement, la plupart du temps cela représente l'**id**, clé primaire auto incrémentée de la table). 

Le lookup_field va être utilisé par DRF lors de l'appel de la fonction **get_object()** (GenericAPIView) pour retrouver l'entité précise.

Code DRF du **get_object()** réduit au code utile, le **filter_kwargs** sera utilisé pour un get, de la façon suivante Model.objects.get(pk=valeur)

```python
class GenericAPIView(views.APIView):
    # ...
    def get_object(self):
        """
        Returns the object the view is displaying.        
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        
        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj
``` 

**lookup_field** peut être surchargé pour changer la clé de recherche d'un objet.

Par exemple, imaginons un modèle de Dossier, sa clé primaire sera par défaut "id" et la recherche d'un dossier de type /dossiers/2/ s'effectuera avec un Dossier.objects.get(pk=2)

```python
class Dossier(TimeStampedModel):    
    reference = models.CharField(max_length=150, null=True)    
```

Imaginons maintenant que nous voulons utiliser plutôt des [UUID](https://fr.wikipedia.org/wiki/Universal_Unique_Identifier), pour sécuriser l'URL (à la place de /dossiers/2/ on souhaite /dossiers/7778c552-73fc-4bc4-8bf9-5a2f6f7b7f47/, le connecté ne peut ainsi pas itérer sur les ID), tout en conservant l'auto-incrément id pour faciliter les requêtes manuelles. 

Le modèle sera enrichi d'un uuid

```python
class Dossier(TimeStampedModel):    
    reference = models.CharField(max_length=150, null=True)    
    uuid = models.UUIDField(default=uuid.uuid4)
```

Dans notre viewset, il suffira de dire que la clé est maintenant "uuid" 

```python
class DossiersViewSet(viewsets.ModelViewSet):
    # surcharge de la clé de recherche pour le get ou get_object()
    lookup_field = 'uuid'
```

DRF prendra alors l'attribut uuid du modèle pour filtrer 

```python
Dossier.objects.get(uuid=valeur)
```

_Nota Bene :_ Django permet d'utiliser directement des uuid en clé primaire, il suffit de [surcharger **id**](https://docs.djangoproject.com/fr/2.0/ref/models/fields/#uuidfield) dans la définition du modèle en lui précisant **primary_key=True**

```python
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
```
# Serializers : create / update <a name="create_update"></a>

Le `ModelSerializer` qui est utilisé à 80 % contient 2 méthodes qui peuvent être surchargées afin d'apporter un changement de comportement sur l'enregistrement d'un modèle.

En effet, nous avons la hiérarchie suivante 

BookSerializer <-- ModelSerializer **create()** / **update()** <-- Serializer <-- BaseSerializer **save()** (appel de create() ou update()) <-- Field

Le save() de BaseSerializer appelle create() ou update() de ModelSerialier selon le mode d'écriture du modèle en cours (en création ou en modification), bon pattern ! Hollywood ;)

Pseudo code du BaseSerializer et du ModelSerializer qui implémente de create() et l'update() que demande BaseSerializer

```python
class BaseSerializer(Field):
    def update(self, instance, validated_data):
        raise NotImplementedError('`update()` must be implemented.')

    def create(self, validated_data):
        raise NotImplementedError('`create()` must be implemented.')

    def save(self, **kwargs):
        # ...
        if self.instance is not None:
            self.instance = self.update(self.instance, validated_data)
        else:
            self.instance = self.create(validated_data)

        return self.instance
```

```python
class ModelSerializer(Serializer):
    # ... instance = model
    def create(self, validated_data):
        instance = ModelClass._default_manager.create(**validated_data)
        return instance
    def update(self, instance, validated_data):
        instance.save() # instance = model
        return instance
    # ...
```

Le schéma de la hiérarchie complète (générée avec Pycharm)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/o7u15vp7v6mmkg1lxkqh.png)

Il faut savoir que DRF **ne sait pas manipuler** en création ou mise à jour les **relations Many-to-Many via les serializers** voire les **[nested serializers](https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers)**  qui ont un **through** dans la définition du modèle ManyToManyField, autrement dit, si vous avez un many to many à modifier ou à créer, il ne le fera pas et lèvera une exception, il va falloir les effectuer autrement et c'est ça que la surcharge du create() ou update() intervient.

Par exemple, imaginons une salle ayant plusieurs équipements possibles (wifi, écran, ...), en UML, cela se représentera de la façon suivante : `Room * ---- * Equipment`, le serializer Room mettra en `read_only` (obligatoire) le serializer embarqué pour les équipements. 

On devra alors **surcharger** create() et update() pour prendre en compte les équipements pour les traiter comme un champ particulier, et en les sortant des _data_, comme fait dans le code suivant qui a pour but d'avoir des fonctions update sur les équipements (ajout ou suppression) d'une salle, une sorte de __hook__.

```python
class Room(models.Model):
  equipments = models.ManyToManyField('Equipment', through='EquipmentToRoom', related_name='rooms')

class RoomSerializer(serializers.ModelSerializer):
      # input equipements pour création ou màj
    equipments_ids = serializers.PrimaryKeyRelatedField(queryset=Equipment.objects.all(), write_only=True, many=True)
    # output des equipements
    equipments_obj = EquipmentSerializer(source='equipments', many=True, read_only=True)

    def update(self, instance, validated_data):
       """ prise en compte équipements salle pour màj """
        raise_errors_on_nested_writes('create', self, validated_data)

        equipements_ids = validated_data.pop('equipments_ids', None)
        instance = super().update(instance, validated_data)
        instance.update_equipements(instance, equipements_ids)
    
        return instance
 
    def create(self, validated_data):
        """ prise en compte équipements pour création salle """
        equipements_ids = validated_data.pop('equipments_ids', None)
        instance = super().create(validated_data)
        instance.update_equipements(instance, equipements_ids)

        return instance


      class Meta:
        model = Room
        fields = '__all__'
```

## Conclusion

Les attributs / fonctions de GenericView qui peuvent être surchargés : [Basics settings](https://www.django-rest-framework.org/api-guide/generic-views/#attributes) et d'autres que vous retrouverez un peu plus loin dans la page ("Other methods")

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gt2vr4slzhid5lca14kp.png)