2021-04-22 21:02:55 +00:00
|
|
|
|
# DRF Initiation
|
|
|
|
|
|
|
|
|
|
par
|
|
|
|
|
|
|
|
|
|
Julien Palard <julien@palard.fr>
|
|
|
|
|
|
|
|
|
|
https://mdk.fr
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Introduce yourself!
|
|
|
|
|
|
2021-10-24 17:07:53 +00:00
|
|
|
|
Et juste pour doctest:
|
|
|
|
|
```python
|
|
|
|
|
import django.contrib.auth.models
|
|
|
|
|
```
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
# Les bonnes bases : Python
|
|
|
|
|
|
|
|
|
|
`*args, **kwargs`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les bonnes bases : Python
|
|
|
|
|
|
|
|
|
|
La MRO.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les bonnes bases : Python
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
On travaille dans un venv.
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
## Les bonnes bases : Python
|
|
|
|
|
|
|
|
|
|
La gestion des dépendances avec `pip-compile`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Pratique
|
|
|
|
|
|
|
|
|
|
Rédiger un script, en ligne de commande, permettant de tester si un
|
|
|
|
|
site internet est en bonne santé :
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
$ python checkurl.py mdk.fr
|
|
|
|
|
Redirection HTTPS: OK
|
|
|
|
|
Status: OK (200)
|
|
|
|
|
Response time: OK (0.125s < 1s)
|
|
|
|
|
Certificate: OK (expires in 68 days)
|
|
|
|
|
HSTS: OK (max-age=63072000; always)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
versionnez !
|
|
|
|
|
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
## Les bonnes bases : Django
|
|
|
|
|
|
2021-04-22 21:02:55 +00:00
|
|
|
|
```bash
|
2021-05-30 09:44:45 +00:00
|
|
|
|
python -m pip install Django
|
|
|
|
|
django-admin startproject demo
|
|
|
|
|
cd demo
|
2021-04-22 21:02:55 +00:00
|
|
|
|
./manage.py migrate
|
|
|
|
|
```
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Leur faire faire le tour du propriétaire.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Vocabulaire
|
|
|
|
|
|
|
|
|
|
Dans Django on va avoir des `models`, des `vues`, et des `urls`.
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Peut être aussi des templates, et l'admin.
|
|
|
|
|
|
|
|
|
|
|
2021-04-22 21:02:55 +00:00
|
|
|
|
## Debug toolbar
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python -m pip install django-debug-toolbar
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
L'ajouter dans `settings.py` et `urls.py`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Un compte administrateur
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
./manage.py createsuperuser
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Vous devez maintenant avoir une interface d'administration qui
|
|
|
|
|
fonctionne, avec la Debug Toolbar à droite.
|
|
|
|
|
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
## Bonnes pratiques
|
|
|
|
|
|
|
|
|
|
On versionne et on prend le temps de poser un `.gitignore`.
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
## Bonnes pratiques
|
|
|
|
|
|
|
|
|
|
On en mettra le moins possible dans le dossier du projet, on
|
|
|
|
|
utilisera des applications pour le reste du code.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Bonnes pratiques
|
|
|
|
|
|
|
|
|
|
Une bonne gestion des dépendances avec `pip-tools`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Bonnes pratiques
|
|
|
|
|
|
|
|
|
|
On surcharge l'objet `User`, même si on pense ne pas en avoir besoin :
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class User(django.contrib.auth.models.AbstractUser):
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
et :
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
AUTH_USER_MODEL = ...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Les URLs
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
urlpatterns = [
|
|
|
|
|
path("admin/", admin.site.urls),
|
|
|
|
|
path("__debug__/", include(debug_toolbar.urls)),
|
|
|
|
|
]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Les vues
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def index(request):
|
|
|
|
|
return HttpResponse("Hello world")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Pratique
|
|
|
|
|
|
|
|
|
|
Faire une page d'accueil pour votre Django.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les modèles
|
|
|
|
|
|
|
|
|
|
Désambiguons `makemigrations` et `migrate` d'abord.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les modèles
|
|
|
|
|
|
|
|
|
|
Personalisez le modèle `User` :
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class User(django.contrib.auth.models.AbstractUser):
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Les modèles
|
|
|
|
|
|
|
|
|
|
Créez un modèle `Domain` :
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class Domain(models.Model):
|
|
|
|
|
domain = models.CharField(max_length=253)
|
|
|
|
|
is_up = models.BooleanField(null=True, blank=True)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## L'admin
|
|
|
|
|
|
|
|
|
|
Indiquez l'existance du modèle domaine à l'admin :
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
admin.site.register(Domain)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Testez
|
2021-05-30 09:44:45 +00:00
|
|
|
|
|
|
|
|
|
## Les bonnes bases : DRF
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python -m pip install djangorestframework
|
2021-05-30 15:43:21 +00:00
|
|
|
|
# Ajouter l'app rest_framework
|
2021-05-30 09:44:45 +00:00
|
|
|
|
```
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
## vocabulaire
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
Dans DRF on va avoir des `serializers`, des `routers`, des `views` et des `permissions`.
|
|
|
|
|
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
## Le routage de Django
|
|
|
|
|
|
|
|
|
|
La requête parcourre les `urlpatterns` du projet, c'est donc à lui
|
|
|
|
|
d'inclure les `urlpatterns` des différentes applications.
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
## Resources
|
|
|
|
|
|
|
|
|
|
Pour Django on avait : https://ccbv.co.uk/ (Memo: « Classy Class-Based-View »).
|
|
|
|
|
|
|
|
|
|
Pour DRF on a : https://www.cdrf.co/ (Memo: « Classy DRF »).
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les media types
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
Pour représenter des données on utilise un *media type* il existe
|
|
|
|
|
plusieurs écoles :
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
- Tout est liste (`Collection+JSON`, ...).
|
|
|
|
|
- Tout est article (`atom+xml`, ...).
|
2021-05-30 15:43:21 +00:00
|
|
|
|
- Snowflakes (`application/json`).
|
|
|
|
|
|
|
|
|
|
(On ne parle donc pas de RPC, on parle de **représentation**).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les media types
|
|
|
|
|
|
|
|
|
|
Certains *media type* sont plus « tout terrain » que d'autres :
|
|
|
|
|
|
|
|
|
|
- `JSON-LD`, avec Hydra : le plus générique.
|
|
|
|
|
- `HAL` : Pour la lecture seule.
|
|
|
|
|
- `application/problem+json`
|
|
|
|
|
- `application/json-patch+json`
|
|
|
|
|
- `application/json-home`
|
2021-04-22 21:02:55 +00:00
|
|
|
|
- ...
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Pensez au web actuel : il mélange text/html, application/javascript, text/css, ...
|
|
|
|
|
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
# Mais qu'est-ce que REST ?
|
|
|
|
|
|
|
|
|
|
C'est un ensemble de contraintes :
|
|
|
|
|
|
|
|
|
|
- Client-Serveur
|
|
|
|
|
- Sans état
|
2021-05-30 15:43:21 +00:00
|
|
|
|
- Une URI identifie une resource
|
|
|
|
|
- Les resources sont manipulées via leurs représentations
|
|
|
|
|
- ...
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Pensez au web actuel pour chaque contrainte : ça marche.
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Client-Serveur
|
|
|
|
|
|
|
|
|
|
✓
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Sans état
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
Attention à l'interprétation : on ne parle pas d'un site statique pour autant.
|
|
|
|
|
|
|
|
|
|
Le serveur a un état, et cet état est amené à changer (un `PUT`, un
|
|
|
|
|
`POST`, un `DELETE` vont typiquement changer quelque chose).
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Sans état
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
Quand on dit « *stateless* » on pense au niveau d'une requête :
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
> L'interprétation d'une requête ne doit **pas** dépendre des requêtes précédentes.
|
|
|
|
|
|
|
|
|
|
C'est tout.
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Prendre l'exemple du client qui s'endort, puis qui revient 8h plus
|
|
|
|
|
tard pour terminer. Ou de plusieurs backends derrière un LB.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Sans état
|
|
|
|
|
|
|
|
|
|
Donc pas de :
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
PUT /workon/user/1
|
|
|
|
|
PUT /user -d '{"name": "Alan"}'
|
|
|
|
|
PUT /workon/user/2
|
|
|
|
|
PUT /user -d '{"name": "Ada"}'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Sans état
|
|
|
|
|
|
|
|
|
|
Mais :
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
PUT /users/1 -d '{"name": "Alan"}'
|
|
|
|
|
PUT /users/2 -d '{"name": "Ada"}'
|
|
|
|
|
```
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Si le serveur a oublié la première requête quand elle arrive, pas de souci.
|
|
|
|
|
|
|
|
|
|
Si les deux requêtes sont gérées par des serveurs différents, pas de souci.
|
|
|
|
|
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
## Coopération avec les caches intermédiaires
|
|
|
|
|
|
|
|
|
|
C'est surtout respecter la sémantique HTTP.
|
|
|
|
|
|
|
|
|
|
Avec HTTPS les problèmes causés par des proxy inconnus, éventuellement
|
|
|
|
|
ne respectant pas la sémantique HTTP ont disparu.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Attention, certains réseaux, de fait, ne respectent pas la sémantique
|
|
|
|
|
HTTP : un POST pourraît être exceptionnellement rejoué, sur un réseau
|
|
|
|
|
mobile, lors du roaming.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Une URI identifie une resource
|
|
|
|
|
|
|
|
|
|
> Cool URIs don't change.
|
|
|
|
|
|
|
|
|
|
REST ne nous impose pas des URL sémantiques / expressives.
|
|
|
|
|
|
|
|
|
|
Cependant les humains les apprécient, une URL bien choisie c'est comme
|
|
|
|
|
un nom de variable bien choisi, c'est agréable.
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
La slide n'en parle pas mais bien en parler:
|
|
|
|
|
- Une URI == une resource.
|
|
|
|
|
- Une resource == une URI.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Manipulation par la représentation
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
GET /users/1
|
|
|
|
|
{"name": "Alan", "birthdate": "1912-06-23"}
|
|
|
|
|
PUT /users/1 -d '{"name": "Alan Turing", "birthdate": "1912-06-23"}'
|
|
|
|
|
```
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Faire une parenthèse sur les etags, `If-Match`, `If-None-Match`.
|
|
|
|
|
|
|
|
|
|
|
2021-04-22 21:02:55 +00:00
|
|
|
|
## Messages auto-descriptifs
|
|
|
|
|
|
|
|
|
|
Toutes les informations nécessaires à l'interprétation du message
|
2021-05-30 09:44:45 +00:00
|
|
|
|
doivent être dans le message.
|
|
|
|
|
|
|
|
|
|
Je n'ai rien contre un lien vers la doc.
|
2021-04-22 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## HATEOAS
|
|
|
|
|
|
|
|
|
|
C'est celui qui fait peur.
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
TL;DR: data + interactions
|
|
|
|
|
|
2021-04-22 21:02:55 +00:00
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Prendre l'exemple d'une boutique avec le bouton "acheter" qui n'est
|
|
|
|
|
présent que s'il y a du stock.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## HATEOAS
|
|
|
|
|
|
|
|
|
|
Le mauvais exemple :
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"@id": "/products/123",
|
|
|
|
|
"name": "Brioche",
|
|
|
|
|
"in_stock": false,
|
|
|
|
|
"buy": {
|
2021-05-30 15:43:21 +00:00
|
|
|
|
"@id": "/cart/",
|
2021-04-22 21:02:55 +00:00
|
|
|
|
"@type": "hydra/CreateResourceOperation",
|
|
|
|
|
"method": "POST",
|
|
|
|
|
"expects": {"@id": "/products/123"}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Ce n'est pas vraiment du JSON-LD+Hydra, mais ça loge das la slide...
|
2021-05-30 09:44:45 +00:00
|
|
|
|
|
|
|
|
|
## HATEOAS
|
|
|
|
|
|
|
|
|
|
> support on building Hypermedia APIs with REST framework is planned for a future version.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## En parlant de sémantique HTTP
|
|
|
|
|
|
|
|
|
|
TL;DR
|
|
|
|
|
|
|
|
|
|
## GET / HEAD / OPTIONS
|
|
|
|
|
|
|
|
|
|
- `safe`
|
|
|
|
|
- `idempotent`
|
|
|
|
|
|
|
|
|
|
## PUT
|
|
|
|
|
|
|
|
|
|
- `idempotent`
|
|
|
|
|
|
|
|
|
|
## DELETE
|
|
|
|
|
|
|
|
|
|
- `idempotent`
|
|
|
|
|
|
|
|
|
|
## POST
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
# La configuration de Django
|
|
|
|
|
|
|
|
|
|
## Trois solutions
|
|
|
|
|
|
|
|
|
|
- `include local_settings.py`
|
|
|
|
|
- `django-configurations`
|
|
|
|
|
- `django-environ`
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
# Et si on revenait à DRF !?
|
|
|
|
|
|
|
|
|
|
## Les serialiseurs
|
|
|
|
|
|
|
|
|
|
Leur rôle est de transformer un objet Python en un objet Python
|
|
|
|
|
facilement sérialisable (en JSON typiquement).
|
|
|
|
|
|
|
|
|
|
Il va donc, par exemple transformer objet datetime en chaîne, puisque
|
|
|
|
|
JSON ne spécifie pas de représentation pour les dates.
|
|
|
|
|
|
|
|
|
|
::: notes
|
|
|
|
|
|
|
|
|
|
Et vice versa.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les URLs
|
|
|
|
|
|
2021-05-30 15:43:21 +00:00
|
|
|
|
Pour commencer : aucune différence avec Django.
|
|
|
|
|
|
2021-05-30 09:44:45 +00:00
|
|
|
|
|
|
|
|
|
## Les vues
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from rest_framework.decorators import api_view
|
|
|
|
|
from rest_framework.response import Response
|
|
|
|
|
|
|
|
|
|
@api_view(["GET"])
|
|
|
|
|
def hello(request):
|
|
|
|
|
return Response({"Hello": "world."})
|
|
|
|
|
```
|
2021-05-31 12:51:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Pratique
|
|
|
|
|
|
|
|
|
|
Avec juste un path dans `urlpatterns` et une vue, faites une API qui
|
|
|
|
|
donne l'heure :
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
$ curl 0:8000/horloge/
|
|
|
|
|
{"datetime":"2021-05-31T12:24:04.534708"}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Les autres méthodes
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
@api_view(["GET", "PUT", "DELETE"])
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Pratique
|
|
|
|
|
|
|
|
|
|
Sur une autre `url`, par exemple `/cache/`, implémentez un
|
|
|
|
|
`memcached`, testez-le avec `curl`.
|
2021-06-23 10:08:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Serializers
|
|
|
|
|
|
|
|
|
|
- serializers.BaseSerializer
|
|
|
|
|
- serializers.ModelSerializer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## BaseSerializer
|
|
|
|
|
|
|
|
|
|
`to_representation` / `to_internal_value`
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class DateSerializer(serializers.BaseSerializer):
|
|
|
|
|
def to_representation(self, instance):
|
|
|
|
|
return instance.isoformat()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Pratique
|
|
|
|
|
|
|
|
|
|
Implémentez un `FileSerializer` prenant un `Path` de `pathlib` et
|
|
|
|
|
renvoyant :
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"self": "http://127.0.0.1:8000/files/.",
|
|
|
|
|
"name": "drf-demo",
|
|
|
|
|
"path": ".",
|
|
|
|
|
"size": 4096,
|
|
|
|
|
"ctime": "2021-05-30", "mtime": "2021-05-30", "atime": "2021-04-22",
|
|
|
|
|
"mode": "0o40755",
|
|
|
|
|
"is_dir": true,
|
|
|
|
|
"files": [
|
|
|
|
|
{
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Permissions
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class IsOwner(permissions.BasePermission):
|
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
|
|
|
return request.user == obj.owner
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## HyperLinkedModelSerializer
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class DomainSerializer(HyperlinkedModelSerializer):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = Domain
|
|
|
|
|
fields = ["domain", "is_up", "checks_url", "url"]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## ViewSets
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class DomainViewSet(ModelViewSet):
|
|
|
|
|
queryset = Domain.objects.all()
|
|
|
|
|
serializer_class = DomainSerializer
|
|
|
|
|
permission_classes = [IsOwner]
|
|
|
|
|
```
|