379 lines
12 KiB
Plaintext
379 lines
12 KiB
Plaintext
== Application Programming Interface
|
|
|
|
Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d'affectation.
|
|
Quelque chose comme ceci:
|
|
|
|
[source,python]
|
|
----
|
|
# models.py
|
|
|
|
from django.db import models
|
|
|
|
|
|
class People(models.Model):
|
|
CIVILITY_CHOICES = (
|
|
("M", "Monsieur"),
|
|
("Mme", "Madame"),
|
|
("Dr", "Docteur"),
|
|
("Pr", "Professeur"),
|
|
("", "")
|
|
)
|
|
|
|
last_name = models.CharField(max_length=255)
|
|
first_name = models.CharField(max_length=255)
|
|
civility = models.CharField(
|
|
max_length=3,
|
|
choices=CIVILITY_CHOICES,
|
|
default=""
|
|
)
|
|
|
|
def __str__(self):
|
|
return "{}, {}".format(self.last_name, self.first_name)
|
|
|
|
|
|
class Service(models.Model):
|
|
label = models.CharField(max_length=255)
|
|
|
|
def __str__(self):
|
|
return self.label
|
|
|
|
|
|
class ContractType(models.Model):
|
|
label = models.CharField(max_length=255)
|
|
short_label = models.CharField(max_length=50)
|
|
|
|
def __str__(self):
|
|
return self.short_label
|
|
|
|
|
|
class Contract(models.Model):
|
|
people = models.ForeignKey(People, on_delete=models.CASCADE)
|
|
date_begin = models.DateField()
|
|
date_end = models.DateField(blank=True, null=True)
|
|
contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
|
|
service = models.ForeignKey(Service, on_delete=models.CASCADE)
|
|
|
|
def __str__(self):
|
|
if self.date_end is not None:
|
|
return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
|
|
self.date_begin,
|
|
self.date_end,
|
|
self.service,
|
|
self.contract_type
|
|
)
|
|
|
|
return "A partir du {}, à durée indéterminée, dans le service {} ({})".format(
|
|
self.date_begin,
|
|
self.service,
|
|
self.contract_type
|
|
)
|
|
----
|
|
|
|
image::images/rest/models.png[]
|
|
|
|
## Configuration
|
|
|
|
La configuration des points de terminaison de notre API est relativement touffue.
|
|
Il convient de:
|
|
|
|
1. Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l'API,
|
|
2. Configurer les vues, càd le comportement de chacun des points de terminaison,
|
|
3. Configurer les points de terminaison eux-mêmes, càd les URLs permettant d'accéder aux ressources.
|
|
4. Et finalement ajouter quelques paramètres au niveau de notre application.
|
|
|
|
### Sérialiseurs
|
|
|
|
```python
|
|
# serializers.py
|
|
|
|
from django.contrib.auth.models import User, Group
|
|
from rest_framework import serializers
|
|
|
|
from .models import People, Contract, Service
|
|
|
|
|
|
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
|
|
class Meta:
|
|
model = People
|
|
fields = ("last_name", "first_name", "contract_set")
|
|
|
|
|
|
class ContractSerializer(serializers.HyperlinkedModelSerializer):
|
|
class Meta:
|
|
model = Contract
|
|
fields = ("date_begin", "date_end", "service")
|
|
|
|
|
|
class ServiceSerializer(serializers.HyperlinkedModelSerializer):
|
|
class Meta:
|
|
model = Service
|
|
fields = ("name",)
|
|
|
|
```
|
|
|
|
### Vues
|
|
|
|
```python
|
|
# views.py
|
|
|
|
from django.contrib.auth.models import User, Group
|
|
from rest_framework import viewsets
|
|
from rest_framework import permissions
|
|
|
|
from .models import People, Contract, Service
|
|
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer
|
|
|
|
|
|
class PeopleViewSet(viewsets.ModelViewSet):
|
|
queryset = People.objects.all()
|
|
serializer_class = PeopleSerializer
|
|
permission_class = [permissions.IsAuthenticated]
|
|
|
|
|
|
class ContractViewSet(viewsets.ModelViewSet):
|
|
queryset = Contract.objects.all()
|
|
serializer_class = ContractSerializer
|
|
permission_class = [permissions.IsAuthenticated]
|
|
|
|
|
|
class ServiceViewSet(viewsets.ModelViewSet):
|
|
queryset = Service.objects.all()
|
|
serializer_class = ServiceSerializer
|
|
permission_class = [permissions.IsAuthenticated]
|
|
|
|
```
|
|
|
|
### URLs
|
|
|
|
```python
|
|
# urls.py
|
|
|
|
from django.contrib import admin
|
|
from django.urls import path, include
|
|
|
|
from rest_framework import routers
|
|
|
|
from core import views
|
|
|
|
|
|
router = routers.DefaultRouter()
|
|
router.register(r"people", views.PeopleViewSet)
|
|
router.register(r"contracts", views.ContractViewSet)
|
|
router.register(r"services", views.ServiceViewSet)
|
|
|
|
urlpatterns = [
|
|
path("api/v1/", include(router.urls)),
|
|
path('admin/', admin.site.urls),
|
|
]
|
|
```
|
|
|
|
### Paramètres
|
|
|
|
```python
|
|
# settings.py
|
|
|
|
INSTALLED_APPS = [
|
|
...
|
|
"rest_framework",
|
|
...
|
|
]
|
|
|
|
...
|
|
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
'PAGE_SIZE': 10
|
|
}
|
|
|
|
```
|
|
|
|
A ce stade, en nous rendant sur l'URL `http://localhost:8000/api/v1`, nous obtiendrons ceci:
|
|
|
|
image::images/rest/api-first-example.png[]
|
|
|
|
|
|
## Modèles et relations
|
|
|
|
Plus haut, nous avons utilisé une relation de type `HyperlinkedModelSerializer`.
|
|
C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité.
|
|
Pour palier à ceci, il existe [plusieurs manières de représenter ces relations](https://www.django-rest-framework.org/api-guide/relations/): soit *via* un hyperlien, comme ci-dessus, soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource.
|
|
La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats):
|
|
|
|
```json
|
|
{
|
|
"count": 1,
|
|
"next": null,
|
|
"previous": null,
|
|
"results": [
|
|
{
|
|
"last_name": "Bond",
|
|
"first_name": "James",
|
|
"contract_set": [
|
|
"http://localhost:8000/api/v1/contracts/1/",
|
|
"http://localhost:8000/api/v1/contracts/2/"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
à ceci:
|
|
|
|
```json
|
|
{
|
|
"count": 1,
|
|
"next": null,
|
|
"previous": null,
|
|
"results": [
|
|
{
|
|
"last_name": "Bond",
|
|
"first_name": "James",
|
|
"contract_set": [
|
|
{
|
|
"date_begin": "2019-01-01",
|
|
"date_end": null,
|
|
"service": "http://localhost:8000/api/v1/services/1/"
|
|
},
|
|
{
|
|
"date_begin": "2009-01-01",
|
|
"date_end": "2021-01-01",
|
|
"service": "http://localhost:8000/api/v1/services/1/"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
La modification se limite à **surcharger** la propriété, pour indiquer qu'elle consiste en une instance d'un des sérialiseurs existants.
|
|
Nous passons ainsi de ceci
|
|
|
|
```python
|
|
class ContractSerializer(serializers.HyperlinkedModelSerializer):
|
|
class Meta:
|
|
model = Contract
|
|
fields = ("date_begin", "date_end", "service")
|
|
|
|
|
|
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
|
|
|
|
class Meta:
|
|
model = People
|
|
fields = ("last_name", "first_name", "contract_set")
|
|
```
|
|
|
|
à ceci:
|
|
|
|
```python
|
|
class ContractSerializer(serializers.HyperlinkedModelSerializer):
|
|
class Meta:
|
|
model = Contract
|
|
fields = ("date_begin", "date_end", "service")
|
|
|
|
|
|
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
|
|
contract_set = ContractSerializer(many=True, read_only=True)
|
|
|
|
class Meta:
|
|
model = People
|
|
fields = ("last_name", "first_name", "contract_set")
|
|
```
|
|
|
|
Nous ne faisons donc bien que redéfinir la propriété `contract_set` et indiquons qu'il s'agit à présent d'une instance de `ContractSerializer`, et qu'il est possible d'en avoir plusieurs.
|
|
C'est tout.
|
|
|
|
## Filtres et recherches
|
|
|
|
A ce stade, nous pouvons juste récupérer des informations présentes dans notre base de données, mais à part les parcourir, il est difficile d'en faire quelque chose.
|
|
|
|
Il est possible de jouer avec les URLs en définissant une nouvelle route ou avec les paramètres de l'URL, ce qui demanderait alors de programmer chaque cas possible - sans que le consommateur ne puisse les déduire lui-même. Une solution élégante consiste à autoriser le consommateur à filtrer les données, directement au niveau de l'API.
|
|
Ceci peut être fait. Il existe deux manières de restreindre l'ensemble des résultats retournés:
|
|
|
|
1. Soit au travers d'une recherche, qui permet d'effectuer une recherche textuelle, globale et par ensemble à un ensemble de champs,
|
|
2. Soit au travers d'un filtre, ce qui permet de spécifier une valeur précise à rechercher.
|
|
|
|
Dans notre exemple, la première possibilité sera utile pour rechercher une personne répondant à un ensemble de critères. Typiquement, `/api/v1/people/?search=raymond bond` ne nous donnera aucun résultat, alors que `/api/v1/people/?search=james bond` nous donnera le célèbre agent secret (qui a bien entendu un contrat chez nous...).
|
|
|
|
Le second cas permettra par contre de préciser que nous souhaitons disposer de toutes les personnes dont le contrat est ultérieur à une date particulière.
|
|
|
|
Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de proposer immédiatement les champs, et donc d'informer le consommateur des possibilités:
|
|
|
|
image::images/rest/drf-filters-and-searches.png[]
|
|
|
|
|
|
### Recherches
|
|
|
|
La fonction de recherche est déjà implémentée au niveau de Django-Rest-Framework, et aucune dépendance supplémentaire n'est nécessaire.
|
|
Au niveau du `viewset`, il suffit d'ajouter deux informations:
|
|
|
|
```python
|
|
...
|
|
from rest_framework import filters, viewsets
|
|
...
|
|
|
|
class PeopleViewSet(viewsets.ModelViewSet):
|
|
...
|
|
filter_backends = [filters.SearchFilter]
|
|
search_fields = ["last_name", "first_name"]
|
|
...
|
|
```
|
|
|
|
### Filtres
|
|
|
|
Nous commençons par installer [le paquet `django-filter`](https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend) et nous l'ajoutons parmi les applications installées:
|
|
|
|
```bash
|
|
λ pip install django-filter
|
|
Collecting django-filter
|
|
Downloading django_filter-2.4.0-py3-none-any.whl (73 kB)
|
|
|████████████████████████████████| 73 kB 2.6 MB/s
|
|
Requirement already satisfied: Django>=2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from django-filter) (3.1.7)
|
|
Requirement already satisfied: asgiref<4,>=3.2.10 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (3.3.1)
|
|
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (0.4.1)
|
|
Requirement already satisfied: pytz in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (2021.1)
|
|
Installing collected packages: django-filter
|
|
Successfully installed django-filter-2.4.0
|
|
```
|
|
|
|
Une fois l'installée réalisée, il reste deux choses à faire:
|
|
|
|
1. Ajouter `django_filters` parmi les applications installées:
|
|
2. Configurer la clé `DEFAULT_FILTER_BACKENDS` à la valeur `['django_filters.rest_framework.DjangoFilterBackend']`.
|
|
|
|
Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier `settings.py` de la manière suivante:
|
|
|
|
```python
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
'PAGE_SIZE': 10,
|
|
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
|
|
}
|
|
```
|
|
|
|
Au niveau du viewset, il convient d'ajouter ceci:
|
|
|
|
```python
|
|
...
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework import viewsets
|
|
...
|
|
|
|
class PeopleViewSet(viewsets.ModelViewSet):
|
|
...
|
|
filter_backends = [DjangoFilterBackend]
|
|
filterset_fields = ('last_name',)
|
|
...
|
|
```
|
|
|
|
A ce stade, nous avons deux problèmes:
|
|
|
|
1. Le champ que nous avons défini au niveau de la propriété `filterset_fields` exige une correspondance exacte. Ainsi, `/api/v1/people/?last_name=Bon` ne retourne rien, alors que `/api/v1/people/?last_name=Bond` nous donnera notre agent secret préféré.
|
|
2. Il n'est pas possible d'aller appliquer un critère de sélection sur la propriété d'une relation. Notre exemple proposant rechercher uniquement les relations dans le futur (ou dans le passé) tombe à l'eau.
|
|
|
|
Pour ces deux points, nous allons définir un nouveau filtre, en surchargeant une nouvelle classe dont la classe mère serait de type `django_filters.FilterSet`.
|
|
|
|
TO BE CONTINUED.
|
|
|
|
A noter qu'il existe un paquet [Django-Rest-Framework-filters](https://github.com/philipn/django-rest-framework-filters), mais il est déprécié depuis Django 3.0, puisqu'il se base sur `django.utils.six` qui n'existe à présent plus. Il faut donc le faire à la main (ou patcher le paquet...).
|
|
|