by Gabrielle
Posté le Aug. 27, 2020, 10 a.m.
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 !
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',
]
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 :
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.
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 !
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/).
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) !
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 !