Gabrielle Barboteau

Développeuse Full-stack

Créer une API avec Django REST Framework, partie 1 : les bases en une heure

by Gabrielle


Posté le Aug. 27, 2020, 10 a.m.



Pourquoi créer sa propre API ?

Si l'utilisation d'API externes est une base du métier de développeur web (l'une des premières choses que j'ai appris fut d'intégrer une carte Google Maps à une page statique), créer sa propre API est moins courant. Et pourtant ! Cela peut-être un vrai plus en fonction du projet sur lequel vous travaillez, et ce n'est pas forcémment très compliqué à faire grâce aux outils modernes.

Après avoir fini le gros du développement de Trans-sport, j'ai réalisé que doter le site de son API pouvait être très utile pour certains utilisateurs : en leur donnant la possibilité de récupérer facilement la liste des lieux en les triant à leur façon (par département, en fonction des notes...), ils pourront peut-être trouver de nouveaux usages auxquels je n'ai pas pensé. Et si cela peut permettre au site d'accomplir encore mieux sa mission, ça me va !

Mais je n'avais pas beaucoup de temps devant moi pour mettre commencer le développement de cette fonctionnalité : qu'à cela ne tienne, j'ai décidé de prendre une heure pour comprendre et utiliser Django REST Framework, et mettre au point une API très (TRES) basique !

 

Mise en place

Première étape : installer Django REST Framework dans votre projet !  

pip install djangorestframework

Faites ensuite attention à bien l'intégrer dans votre projet. Pour cela, ouvrez votre fichier settings.py, et ajoutez l'app à ce niveau : 

INSTALLED_APPS = [
    ...
    'rest_framework',
]

 

Notre base

Ce que je souhaite faire me semble relativement simple : faire un sorte que les développeurs puissent récolter les informations concernant les lieux. Pour cela, passons en revue le modèle Place 

class Place(models.Model):
    """Model for the places on the App"""
    class Meta:
        constraints = [models.UniqueConstraint(fields=['category', 'adress'], name='saved_place')]

    name = models.TextField()
    picture = models.TextField(blank=True)
    description = models.TextField(blank=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    adress = models.ForeignKey(Adress, on_delete=models.CASCADE)
    website = models.TextField(blank=True)
    contact_mail = models.TextField(blank=True)
    contact_phone = models.TextField(blank=True)
    can_be_seen = models.BooleanField(default=False)

    def __str__(self):
        return self.name

Quelques explications pour y voir plus clair :

  • Chaque lieu a une adresse et une catégorie, tous deux des modèles. Nous reviendrons très rapidement sur le modèle Adress.
  • Les noms de chaque attributs parlent d'eux-mêmes, avec une subtilité : can_be_seen n'est jamais utilisé par les utilisateurs du site, mais uniquement par l'équipe administrative et modératrice. Cela permet aux gens d'ajouter des lieux sans que ceux-ci n'apparaissent directement. Du coup, cette variable sera absente des données transmises par notre API (mais nous l'utiliserons pour filtrer quels lieux envoyer).
  • Les notes (globales, liées à des critères précis...) de chaque lieu n'apparaissent pas dans le modèle du lieu, car ils sont liés aux commentaires. La note apparaissant est tout simplement la moyenne des notes de chaque commentaire : cela aura une petite importance lorsque nous renverrons cette information.

Nous aurons également besoin de renvoyer les éléments contenus dans le modèle Adress:

class Adress(models.Model):
    region = models.TextField()
    departement = models.TextField()
    postal_code = models.TextField(max_length=5)
    city = models.TextField()
    street_adress = models.TextField()

Pas grand-chose à signaler de côté. 

Afin de transmettre les informations des éléments liés à ces modèles, nous allons devoir les serializer.

 

Serialization des données

Au même niveau que votre fichier models.py, créez un fichier serializers.py. Il ressemblera à ça une fois terminé : 

from rest_framework import serializers
from rest_framework.response import Response
from .models import Place, Adress, Category, Comment
from .utils import GetNote

class AdressSerializer(serializers.ModelSerializer):
    class Meta:
        model = Adress
        fields = ('street_adress', 'postal_code', 'city', 'departement', 'region')


class PlaceSerializer(serializers.ModelSerializer):
    adress = AdressSerializer(many=False, read_only=True)
    comment = CommentSerializer(many=True, read_only=True)

    class Meta:
        model = Place
        fields = ('name', 'picture', 'description', 'website', 'contact_mail', 'contact_phone', 'comment', 'adress')

    def to_representation(self, instance):
        ret = super().to_representation(instance)
        placeName = ret['name']
        myPlace = Place.objects.get(name=placeName)
        allComments = Comment.objects.all().filter(place_id=myPlace)
        ret['global_note'] = GetNote(allComments.filter(score_global='P').count(), allComments.filter(score_global='N').count())
        ret['note_can_you_enter'] = GetNote(allComments.filter(can_you_enter=True).count(), allComments.filter(can_you_enter =False).count())
        ret['note_are_you_safe_enough'] = GetNote(allComments.filter(are_you_safe_enough=True).count(), allComments.filter(are_you_safe_enough =False).count())
        ret['note_is_mixed_lockers'] = GetNote(allComments.filter(is_mixed_lockers=True).count(), allComments.filter(is_mixed_lockers =False).count())
        ret['note_is_inclusive_lockers'] = GetNote(allComments.filter(is_inclusive_lockers=True).count(), allComments.filter(is_inclusive_lockers =False).count())
        ret['note_has_respectful_staff'] = GetNote(allComments.filter(has_respectful_staff=True).count(), allComments.filter(has_respectful_staff =False).count())
        return ret

Intimidé ? Pas de panique, reprenons depuis le début : 

from rest_framework import serializers
from rest_framework.response import Response
from .models import Place, Adress, Category, Comment
from .utils import GetNote

La première chose à faire est d'importer les éléments dont nous aurons besoin : les fonctionnalités de Django REST Framework, nos modèles, ainsi qu'une fonction utilitaire que j'ai mise au point pour calculer la moyenne des avis. Elle se trouve dans le fichier utils.py situé au même niveau que models.py et serializers.py, et ressemble à ça (les utilisateurs ne pouvant laisser que des avis positifs, négatifs, ou neutres, la note est calculé en calculant le ration entre notes positives et notes positives + négatives, les neutres étant exclues de l'opération) :

def GetNote(positive_reviews, negative_reviews):
    total_notes = positive_reviews + negative_reviews
    if positive_reviews == 0:
        ratio = 0
    else:
        ratio = positive_reviews / total_notes
    note = ratio * 5
    return round(note, 1) 

Une fois les dépendances nécessaires importées, occupons-nous de notre modèle Adress, dont la serialization est assez simple : 

class AdressSerializer(serializers.ModelSerializer):
    class Meta:
        model = Adress
        fields = ('street_adress', 'postal_code', 'city', 'departement', 'region')

Ici, nous indiquons simplement que le modèle à utiliser est Adress, et nous choisissons quels champs seront renvoyés (j'aurais pu ommettre complètement la dernière ligne, mais j'ai préféré l'écrire par soucis de clarté du code). Vous voyez, c'est simple ! 

Par contre, la serialization du modèle Place est un peu plus... Touffue : 

class PlaceSerializer(serializers.ModelSerializer):
    adress = AdressSerializer(many=False, read_only=True)

    class Meta:
        model = Place
        fields = ('name', 'picture', 'description', 'website', 'contact_mail', 'contact_phone', 'adress')

    def to_representation(self, instance):
        ret = super().to_representation(instance)
        placeName = ret['name']
        myPlace = Place.objects.get(name=placeName)
        allComments = Comment.objects.all().filter(place_id=myPlace)
        ret['global_note'] = GetNote(allComments.filter(score_global='P').count(), allComments.filter(score_global='N').count())
        ret['note_can_you_enter'] = GetNote(allComments.filter(can_you_enter=True).count(), allComments.filter(can_you_enter =False).count())
        ret['note_are_you_safe_enough'] = GetNote(allComments.filter(are_you_safe_enough=True).count(), allComments.filter(are_you_safe_enough =False).count())
        ret['note_is_mixed_lockers'] = GetNote(allComments.filter(is_mixed_lockers=True).count(), allComments.filter(is_mixed_lockers =False).count())
        ret['note_is_inclusive_lockers'] = GetNote(allComments.filter(is_inclusive_lockers=True).count(), allComments.filter(is_inclusive_lockers =False).count())
        ret['note_has_respectful_staff'] = GetNote(allComments.filter(has_respectful_staff=True).count(), allComments.filter(has_respectful_staff =False).count())
        return ret

Comme tout à l'heure, allons-y point par point : 

class PlaceSerializer(serializers.ModelSerializer):
    adress = AdressSerializer(many=False, read_only=True)

Nous voulons que les informations de l'adresse d'un lieu soient également renvoyé, nous indiquons donc que le champ adress fait référence à la version sérializée du modèle Adress.

class PlaceSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = Place
        fields = ('name', 'picture', 'description', 'website', 'contact_mail', 'contact_phone', 'adress')

Comme pour AdressSerializer, nous indiquons le modèle à utiliser, et les champs à montrer. Notez que cette fois, nous n'incluons pas tous les champs du modèle : can_be_seen est absent !

class PlaceSerializer(serializers.ModelSerializer):
    ...
    def to_representation(self, instance):
        ret = super().to_representation(instance)
        placeName = ret['name']
        myPlace = Place.objects.get(name=placeName)
        allComments = Comment.objects.all().filter(place_id=myPlace)
        ret['global_note'] = GetNote(allComments.filter(score_global='P').count(), allComments.filter(score_global='N').count())
        ret['note_can_you_enter'] = GetNote(allComments.filter(can_you_enter=True).count(), allComments.filter(can_you_enter =False).count())
        ret['note_are_you_safe_enough'] = GetNote(allComments.filter(are_you_safe_enough=True).count(), allComments.filter(are_you_safe_enough =False).count())
        ret['note_is_mixed_lockers'] = GetNote(allComments.filter(is_mixed_lockers=True).count(), allComments.filter(is_mixed_lockers =False).count())
        ret['note_is_inclusive_lockers'] = GetNote(allComments.filter(is_inclusive_lockers=True).count(), allComments.filter(is_inclusive_lockers =False).count())
        ret['note_has_respectful_staff'] = GetNote(allComments.filter(has_respectful_staff=True).count(), allComments.filter(has_respectful_staff =False).count())
        return ret

La fonction to_representation permet d'ajouter des données à des modèles sérialisés afin de transmettre des informations supplémentaires. Ce que nous voulons faire ici est assez simple : rajouter la moyenne des notes à chaque lieu ! 

Pour cela, nous avons récupéré tous les commentaires du lieu, avant de créer de nouveaux champs pour chaque moyenne à renvoyer. Par exemple : 

ret['global_note'] = GetNote(allComments.filter(score_global='P').count(), allComments.filter(score_global='N').count())

ret[nom_du_champ] permet d'ajouter nom_du_champ à notre modèle sérialisé. Ici, global_note est la moyenne des notes globales d'un lieu, que l'on obtient avec la fonction GetNote() (en passant comme premier argument le nombre de commentaires avec une note positive, et en deuxième deux avec une note négative).

On rince, et on répète pour chaque attribut avant de retourner ret !

 

Les vues et les URL

Fatigués ? J'espère que non, nous n'avons pas encore terminé !

Afin de pouvoir accéder à ces informations, il va nous falloir créer de nouvelles vues, et ensuite les ajouter à nos URL. Attaquons-nous d'abord à notre fichier views.py :

from rest_framework import viewsets
from .models import Place
from .serializers import PlaceSerializer

class PlaceViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Place.objects.all().filter(can_be_seen=True).order_by("id")
    serializer_class = PlaceSerializer

Nous importons le modèle Place et sa version sérialisée (nous n'avons pas besoin d'Adress, vu qu'il est intégré à Place) ainsi que ce dont nous avons besoin dans Django-REST-Framework, puis nous créons une classe héritant du viewsets ReadOnlyModelViewSet, car nous ne voulons pas que nos utilisateurs puissent ajouter ou modifier des informations. Nous indiquons que seuls les lieux visibles dans l'application (je vous avais dit que nous utiliserions can_be_seen !) seront renvoyés, nous précisons que le serializer que nous utilisons est PlaceSerializer, et... C'est tout de ce côté ! Passons à urls.py !

from django.urls import include, path
from rest_framework import routers
...
from . import views


router = routers.DefaultRouter()
router.register(r'place', views.PlaceViewSet)

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

Comme d'habitude, nous importons les éléments dont nous avons besoin. Petite subtilité pour gagner du temps : créer une instance d'un router afin de générer automatiquement les url de notre API ! Et enfin, ajoutons une url pour accéder au point de départ de notre API (sobremment nommé... api/).

 

Et maintenant, on teste !

Cette version de l'API est d'hors et déjà en ligne, vous pouvez donc voir par vous même qu'elle fonctionne soit en effectuant une requête (via Postman par exempl), soit en allant directement à cette adresse, notre API étant directement accessible depuis un navigateur !

Avec https://trans-sport.herokuapp.com/api/place/, vous obtiendrez l'ensemble des lieux visibles sur Trans-Sport, et pour en obtenir un en particulier, rajoutez son id (mon petit préféré étant celui-ci) !

 

Du coup, c'est fini ?

Absolument pas Timmy ! 

On a bien travaillé en une heure, mais il nous reste beaucoup de choses à faire. Par exemple : il est impossible d'obtenir l'id d'un lieu via l'API, rendant la navigation vers un lieu particulier bien compliqué.

Il y a tout un tas d'autres choses à faire et à revoir, mais nous avons déjà une base fonctionnelle ! La suite... Nous verrons cela une prochaine fois.

En attendant, vous pouvez consulter le repo github du projet, voir même visiter le site de Trans-Sport !