Skip to content

Tuto django et uniformisation

Baptiste PECATTE edited this page Sep 21, 2016 · 11 revisions

Processus de mise en place d'un composant sur Django, et uniformisation des pratiques

Il y a trois éléments à distinguer quand on manipule Django avec REST, le modèle, le serializer, et la vue. Voici un guide qui permet de définir une utilisation basique mais souvent très très souvent largement suffisante pour notre utilisation.

Ce guide sert aussi d'harmonisation des pratiques, de sorte à ne pas créer plus de classe qu'il n'en faut, ou placer certaines fonctions inutiles, ou au mauvais endroit. A retenir de manière générale :

  • Les droits sont à placer côté modèle
  • La validation des données est à placer côté serializer

Le Modèle (Doc)

Doc : type de champs

Le modèle sert à donner a Django la structure de nos ressources. Il se charge tout seul de manipuler la base de donnée. Le modèle est la classe principale de la ressource. Sur le modèle on placera :

  • Les propriétés de la ressource (pas besoin de définir un id, il existe par défaut sur django)
  • Éventuellement Des fonctions de manipulation de la ressource, et de ses attributs
  • Les droits de la ressource (voir le paragraphe DRYPermission)

Le schéma de base d'une ressource est :

from django.db import models

class MonModel(models.Model):

    ################################################################
    # CONSTANTS                                                    #
    ################################################################
    
    # Liste de constantes à utiliser en cas de choix, par exemple ici, le champ type
    TYPE_1 = 0
    TYPE_2 = 2

    ################################################################
    # FIELDS                                                       #
    ################################################################
    
    # Liste des champs de l'objet
    name = models.CharField(max_length=254)
    group = models.ForeignKey('MonAutreModele')
    type = models.PositiveSmallIntegerField(default=TYPE_0)

    ################################################################
    # PERMISSIONS                                                  #
    ################################################################
    
    def has_object_read_permission(self, request):
        return True
    
    @staticmethod
    def has_read_permission(request):
        return True
    
    def has_object_write_permission(self, request):
        return True

    @staticmethod
    def has_write_permission(request):
        return True

Le serializer (Doc)

Il se charge d'effectuer la transformation : représentation <=> modèle django Ici, il se chargera de transformer une ressource django en json pour l'envoi, et convertir du json en ressource django pour modifier la base de donnée. Cette classe est souvent très vide puisqu'il existe une base RESTFramework qui permet de créer automatiquement le serializer à partir du modèle Django. Important, il ne peut pas automatiquement décrire une relation ! Il faut alors lui expliquer comment il est censé traduire une relation, en code JSON. Le plus souvent, il s'agit du cas OneToMany, où il suffit de lui dire de donner la clé primaire de l'objet pointé.

Exemple type d'un sérializer :

from rest_framework import serializers

from sigma_core.models.monmodel import MonModel
from sigma_core.models.monmodel import MonAutreModel

class MonSerializer(serializers.ModelSerializer):
    class Meta:
        model = MonModel
    
    # On precise que la relation MonModel.group doit se traduire par la clef primaire de l'objet pointe
    group = serializers.PrimaryKeyRelatedField(queryset=MonAutreModel.objects.all())

La vue (Doc)

Une vue est une fonction qui gère une requête, et renvoie la réponse appropriée. De manière générale, le protocole de communication étant standardisé (REST), on peut laisser le REST-framework faire les choses pour nous à l'aide du ModelViewset. A partir d'un modèle, et d'un serializer, il crée automatiquement les vues correspondantes au diverses actions.

Les diverses actions sont :

  • Actions de lecture (read)
    • La récupération d'une liste d'élement (list)
    • La récupération d'un élément en particulier (retreive)
  • Actions d'écriture (write)
    • La création (create)
    • L'édition d'un élément en particulier (update)
    • La suppression d'un élement particulier (destroy) On distingue aussi les actions globales (list, create) qui ne prennent pas de paramètre, et les actions objects (retreive, update, destroy) qui prennent un identifieur (appelé pk) de ressource.

Il est possible de créer une route supplémentaire en utilisant un décorateur. On en utilisant @detail_route on créer une route qui prend un paramètre, et en utilisant @list_route, une route qui ne prend aucun paramètre. Ces deux décorateurs prennent entre autre un argument, la méthode HTTP correspondant à la route, soit get, soit post. (ne pas confondre la méthode HTTP qui définie comment passer les données, et l'action qui définie qu'en faire) Le nom de la vue est alors le nom de la fonction. En pratique, dans une telle fonction, on récupère un ou des objets selon un critère, on sérialise le/les objets, puis on les retourne dans une Response.

Important, la gestion des droits se fait de manière automatisée en utilisant DRYPermission. Il faut pour cela préciser à la vue d'utiliser DRYPermission. Je parlerais de la gestion des droits dans une autre partie.

Exemple type d'une vue :

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from dry_rest_permissions.generics import DRYPermissions

from sigma_core.models.monmodel import MonModel
from sigma_core.serializers.monserializer import MonSerializer

class MaViewset(viewsets.ModelViewSet):
    queryset = MonModel.objects.all()
    serializer_class = MonSerializer
    permission_classes = [IsAuthenticated, DRYPermissions]

    @detail_route(methods=['get'])
    def objet_special(self, request, pk=None):
        # [...]
        # truc type :
            data = [...]
            serializer = UnSerializer(data)
            return Response(serializer.data)
        # s'il manque des droits
            return Response("Pas possible", status=status.HTTP_403_FORBIDDEN)

Les URLs

Une fois la vue créée, il faut dire a Django de faire correspondre une url avec une vue. Encore une fois, grace à REST qui standardise le tout, le processus est simplifié. Il suffit de lui donner une Viewset et il va générer automatiquement les url correspondantes. Dans le cas d'une action monaction() particulière créée avec @detail_route ou @list_route dans un viewset "mavue", l'url correspondant est alors mavue/monaction ou /?pk/mavue

Ligne à insérer dans le fichier sigma/url.py :

router = routers.DefaultRouter() # deja present au debut du fichier

from sigma_core.views.maviewset import MaViewset
router.register(r'mavue', MaViewset) # genere l'url /mavue/... avec toutes les routes définies dans la viewset

Les droits (Doc)

La gestion des droits peut se faire de moult manières. De base, django le permet avec des fonctions à définir dans le modèle. Mais elles sont souvent assez limitée et inadaptée, on préfère donc un système de droit basée sur la vue, qui inclue donc la notion d'utilisateur connecté. Cela reste cependant souvent incomplet. D'où l'utilisation de DRYPermission !

Ce framework permet de définir très facilement les permissions. Mode d'emploi : on dit à la vue d'utiliser le système de droit de DRYPermission (comme fait dans la partie vue) Puis on définit dans le modèle, un certain nombre de fonctions de permissions.

On distingue deux type de fonction de permission :

  • La permission globale, qui correspond à une action globale (type create, list, mais aussi toute @list_route). On définit alors une fonction statique @staticmethod def has_XXX_permission(request) dans le modèle. XXX est le nom de l'action pour laquelle on veut définir la permission. Cette méthode sera automatiquement appelée par la vue lorsque la route demande l'action XXX, et la fonction d'action de la Viewset ne sera exécutée que si has_XXX_permission retourne True. La fonction est statique, puis que l'action est globale, elle ne dépend d'aucun object en particulier. On a bien sur accès à l'utilisateur via request.user mais aussi aux données de requêtes via request.data
  • La permission objet, qui correspond à une action objet (type retreive, update, destroy, mais aussi toute @detail_route). On définit alors une méthode def has_object_XXX_permission(self, request)dans le modèle, où XXX est le nom de l'action pour laquelle on veut définir la permission. Même fonctionnement que pour la permission globale, la différence est que la fonction est cette fois ci une méthode. On a donc accès à l'objet concerné par la requête, viaself`.

En plus des actions de base, DRY définit deux types de permission qui permette d'englober les autres pour réduire la quantité de code. Il y a les actions de type read : list, retreive, et toute route définie comme utilisant la méthode GET ; et les actions de type write : create, update, destroy et toute route définie comme utilisant la méthode POST.

Clone this wiki locally