-
Notifications
You must be signed in to change notification settings - Fork 1
Tuto django et uniformisation
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)
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=255)
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())
C'est aussi dans le serializer qu'on effectue la validation des données. Cela passe tout d'abord par la validation de la représentation des données (est-ce que le JSON est bien valide et lisible ?), qui est effectuée naturellement par le serializer. Il y a aussi la vérification des contraintes relationnelles, qui encore une fois, se fait naturellement. Mais on peut aussi rajouter une sur-couche de contrainte, comme par exemple vérifier qu'une date de naissance est entre 1980 et 2000, ou qu'une adresse email est bien formée. Pour cela, il existe deux type de validation :
- La validation par champ, via la définition d'une fonction
def validate_nomduchamp(self, value)
qui doit retourner value si le champ est correct, ou lancer une ValidationError :raise serializer.ValidationError("Definir l'erreur")
- La validation globale, dans certain cas, il semble plus correct de vérifier la validité d'un objet dans son ensemble. Pour cela, il faut définir la fonction
def validate(self, data)
qui fonctionne de la même manière.
Dans vos vues, vous pouvez, dans certains cas, être amenés à utiliser le serializer. Voici les grandes étapes à effectuer dans le cas où l'on reçoit des données JSON :
- Premièrement, la création du serializer avec les données
serializer = MonSerializer(data=mesdonnees)
- Ensuite, effectuer une validation des données en utilisant :
-
serializer.is_valid()
qui retourne True ou False selon la validité des données. Dans le cas où elle retourne False, on peut accéder aux erreurs à l'aide deserializer.errors
. Si la validation est gérée par champ, l'avantage est que l'on récupère alors une liste de paire (champ, erreur concernant le champ) -
serializer.is_valid(raise_exception=True)
qui va alors, au lieu de retourner False en cas d'erreur, lancer une exception HTTP qui remontera jusqu'à Django pour retourner une erreur 400 Bad Request avec le contenu de l'erreur.
-
- Traiter la requête
- Soit en faisant un
serializer.save()
qui enregistre alors les changement dans la BDD django - Soit en traitant
serlializer.validated_data
- Soit en faisant un
Dans le cas où l'on veut envoyer des données depuis Django, la démarche est plus simple :
- La création
serializer = MonSerializer(mesdonnes)
- L'utilisation des données sérializées via
serializer.data
On remarque que dans ce cas, comme les données proviennent de Django, il n'y pas besoin de les valider, et on utilise alors data et non validated_data
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
)
- La récupération d'une liste d'élement (
- 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.
- La création (
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)
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 mavue/?pk/monaction
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, elle est systématiquement appelée avant même la création de l'objet. Elle sert simplement à dire si l'utilisateur en question a accès, de manière générale, à un type de droit, sur un modèle donné. 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 sihas_XXX_permission
retourneTrue
. 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 viarequest.user
mais aussi aux données de requêtes viarequest.data
- La permission objet, elle, fonctionne de la même façon, mais elle est appelée lorsque l'on manipule un objet en particulier. 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
.
Pour les actions globales (list
, create
, ou définies par @list_route
), seule la permission globale correspondante sera appelée.
Pour les actions objets (retrieve
, destroy
, update
, ou définies par @detail_route
), la permission globale correspondante sera appelée, et si elle est validée, alors l'objet sera crée, puis la fonction de permission objet sera appelée, et une fois que celle ci valide, la requête sera traitée.
Important, il ne peut y avoir de fonction de permission objet sans la fonction de permission globale correspondante. Autrement, DRYPermission générera une erreur. Pour éviter de devoir définir une quantité énorme de méthodes de droits pouvant potentiellement avoir la même forme, il faut savoir que DRYPermission ajoute deux types d'actions :
- Les actions
read
(avec donchas_read_permission
ethas_object_read_permission
) qui peuvent remplacer les permission globales et objets des actionslist
,retrieve
, et toute route définie en méthode GET - Les actions
write
(avec donchas_write_permission
ethas_object_write_permission
) qui peuvent remplacer les permission globales et objets des actionsupdate
,create
,destroy
et toute route définie en méthode POST.
Exemple, pour définir le droit de détruire mais pas d'update, on pourrait faire :
@staticmethod
def has_write_permission(request):
return True
def has_object_destroy_permission(self, request):
return True
def has_object_update_permission(self, request):
return False
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.