WIP django
This commit is contained in:
parent
ebae45132e
commit
f9b7688be0
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
@ -22,6 +22,8 @@ jobs:
|
||||||
export deploy_key=""
|
export deploy_key=""
|
||||||
python3 -m pip install --user --upgrade pip
|
python3 -m pip install --user --upgrade pip
|
||||||
python3 -m pip install --user mdtoreveal
|
python3 -m pip install --user mdtoreveal
|
||||||
PATH="$HOME/.local/bin/:$PATH" make -C initiation rsync
|
export PATH="$HOME/.local/bin/:$PATH"
|
||||||
PATH="$HOME/.local/bin/:$PATH" make -C avancé rsync
|
make -C initiation rsync
|
||||||
PATH="$HOME/.local/bin/:$PATH" make -C drf-initiation rsync
|
make -C avancé rsync
|
||||||
|
make -C drf-initiation rsync
|
||||||
|
make -C django-initiation rsync
|
||||||
|
|
33
django-initiation/Makefile
Normal file
33
django-initiation/Makefile
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "Usage:"
|
||||||
|
@echo " make static # to build static version."
|
||||||
|
@echo " make test # to run tests."
|
||||||
|
@echo " make rsync # rsync to prod"
|
||||||
|
@echo " make clean"
|
||||||
|
|
||||||
|
.PHONY: static
|
||||||
|
static: output/index.html
|
||||||
|
|
||||||
|
.PHONY: check
|
||||||
|
test:
|
||||||
|
python test.py *.md
|
||||||
|
|
||||||
|
%.html: %.md
|
||||||
|
mdtoreveal $< --output $@
|
||||||
|
|
||||||
|
output/index.md: django.md
|
||||||
|
mkdir -p output
|
||||||
|
cat $< > $@
|
||||||
|
|
||||||
|
.PHONY: rsync
|
||||||
|
rsync: static
|
||||||
|
rsync -vah --delete output/ mdk_fr@mdk.fr:/var/www/mdk.fr/django-initiation/
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -fr output/
|
||||||
|
|
||||||
|
.PHONY: serve
|
||||||
|
serve:
|
||||||
|
python -m http.server -d output/
|
27
django-initiation/README.md
Normal file
27
django-initiation/README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Contenu
|
||||||
|
|
||||||
|
Side project : https://github.com/JulienPalard/drf-demos
|
||||||
|
|
||||||
|
- Revue des bases et consolidation Python et Django
|
||||||
|
- Présentation des types d'API, approfondissement de REST/HATEOAS
|
||||||
|
- Rappels autour de la sémantique HTTP
|
||||||
|
- django-rest-framework: La sérialisation
|
||||||
|
- django-rest-framework: Les différents types de vues
|
||||||
|
- django-rest-framework: Les permissions
|
||||||
|
- django-rest-framework: Les relations
|
||||||
|
- Les tests et la maintenabilité
|
||||||
|
|
||||||
|
|
||||||
|
# Objectifs pédagogiques
|
||||||
|
|
||||||
|
- Pouvoir démarrer rapidement un projet DRF.
|
||||||
|
- Savoir designer une API.
|
||||||
|
- Implémenter un service headless en utilisant DRF.
|
||||||
|
|
||||||
|
|
||||||
|
# TP
|
||||||
|
|
||||||
|
- Projet 1 : API « Horloge parlante ».
|
||||||
|
- Projet 2 : API « memcached ».
|
||||||
|
- Projet 3 : API « file system ».
|
||||||
|
- Projet 4 : API « uptime manager ».
|
945
django-initiation/django.md
Normal file
945
django-initiation/django.md
Normal file
|
@ -0,0 +1,945 @@
|
||||||
|
# Django Initiation
|
||||||
|
|
||||||
|
par
|
||||||
|
|
||||||
|
Julien Palard <julien@palard.fr>
|
||||||
|
|
||||||
|
https://mdk.fr
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Introduce yourself!
|
||||||
|
|
||||||
|
|
||||||
|
# Django
|
||||||
|
|
||||||
|
Django est une infrastructure d'applications web populaire et robuste.
|
||||||
|
|
||||||
|
> The web framework for perfectionists with deadlines.
|
||||||
|
|
||||||
|
|
||||||
|
## Django : qui l'utilise ?
|
||||||
|
|
||||||
|
- Instagram, Pineterest, Mozilla, Disqus, BitBucket, …
|
||||||
|
|
||||||
|
![](static/stats.png)
|
||||||
|
|
||||||
|
|
||||||
|
# Les bonnes bases : Python
|
||||||
|
|
||||||
|
On travaillera toujours dans un `venv` :
|
||||||
|
|
||||||
|
```python
|
||||||
|
python -m pip install django
|
||||||
|
```
|
||||||
|
|
||||||
|
# La théorie — Projet
|
||||||
|
|
||||||
|
Pour démarrer un projet, une commande :
|
||||||
|
|
||||||
|
```python
|
||||||
|
django-admin startproject project
|
||||||
|
```
|
||||||
|
|
||||||
|
## La théorie — App
|
||||||
|
|
||||||
|
Une fois dans le projet, pour créer une application, une commande :
|
||||||
|
|
||||||
|
```python
|
||||||
|
python manage.py startapp watch
|
||||||
|
```
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
(et ajout dans `settings.py`)
|
||||||
|
|
||||||
|
|
||||||
|
## La théorie — Modèle
|
||||||
|
|
||||||
|
Un « modèle » est la description d'une table.
|
||||||
|
|
||||||
|
Ça rappelle un ORM, mais ça permet beaucoup plus de choses en Django.
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
- admin
|
||||||
|
- forms
|
||||||
|
- serializers (API)
|
||||||
|
- class based views
|
||||||
|
|
||||||
|
|
||||||
|
## La théorie — Modèle
|
||||||
|
|
||||||
|
Par exemple :
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Website(models.Model):
|
||||||
|
host = models.CharField(max_length=512)
|
||||||
|
is_up = models.BooleanField(null=True, blank=True)
|
||||||
|
last_check = models.DateTimeField(auto_now_add=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Prendre le temps d'expliquer les fields (leur relation avec la DB), et
|
||||||
|
les differents endroits ou Django peut reutiliser cette information
|
||||||
|
(widgets, validation, ...).
|
||||||
|
|
||||||
|
|
||||||
|
## La théorie — Première interface d'admin
|
||||||
|
|
||||||
|
En une ligne, pourquoi pas :
|
||||||
|
|
||||||
|
```python
|
||||||
|
admin.site.register(Website)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## La théorie — la DB
|
||||||
|
|
||||||
|
- PostgreSQL
|
||||||
|
- MySQL
|
||||||
|
- sqlite
|
||||||
|
- ...
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Leur faire croire 2 secondes qu'on va devoir s'installer et se
|
||||||
|
configurer un serveur de base de donnée :D
|
||||||
|
|
||||||
|
|
||||||
|
## La théorie — La DB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
On expliquera plus tard, leur dire que ça crée la DB et que le but
|
||||||
|
maintenant c'est surtout d'aller tester ça :)
|
||||||
|
|
||||||
|
|
||||||
|
## La théorie — L'interface d'administration
|
||||||
|
|
||||||
|
On a une DB, mais pas encore d'utilisateur admin dedans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
## La théorie — L'interface d'administration
|
||||||
|
|
||||||
|
On a terminé, on peut démarrer le serveur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
# La pratique
|
||||||
|
|
||||||
|
```bash
|
||||||
|
django-admin startproject project
|
||||||
|
cd project
|
||||||
|
python manage.py startapp watch
|
||||||
|
```
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Biiien prendre le temps d'expliquer l'arborescence, de se promener,
|
||||||
|
d'y lire les commentaires.
|
||||||
|
|
||||||
|
|
||||||
|
## La pratique
|
||||||
|
|
||||||
|
Ajout de l'app `watch` dans `project/settings.py` :
|
||||||
|
|
||||||
|
```python
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"watch",
|
||||||
|
...,
|
||||||
|
...,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Les modèles
|
||||||
|
|
||||||
|
On va mettre celui-ci dans `watch/models.py`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Website(models.Model):
|
||||||
|
host = models.CharField(max_length=512)
|
||||||
|
is_up = models.BooleanField(null=True, blank=True)
|
||||||
|
last_check = models.DateTimeField(auto_now_add=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Première interface d'admin
|
||||||
|
|
||||||
|
Et ça dans `watch/admin.py`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from watch.models import Website
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Website)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Création de la DB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Expliquer les deux étapes.
|
||||||
|
|
||||||
|
|
||||||
|
## L'interface d'administration
|
||||||
|
|
||||||
|
On a une DB, mais pas encore d'utilisateur admin dedans :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
## L'interface d'administration
|
||||||
|
|
||||||
|
On a terminé, on peut essayer maintenant ?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Leur faire faire ça dans un **autre** shell.
|
||||||
|
|
||||||
|
puis les laisser jouer avec l'interface d'admin, créer quelques sites...
|
||||||
|
|
||||||
|
|
||||||
|
## L'interface d'administration
|
||||||
|
|
||||||
|
Les modèles, leurs `fields` ne servent donc pas qu'a l'ORM, cette
|
||||||
|
interface d'admin nous à demandé une ligne de code.
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Si ce n'est pas déjà fait, leur faire ajouter des `__str__`.
|
||||||
|
|
||||||
|
|
||||||
|
## Astuce
|
||||||
|
|
||||||
|
On peut passer beaucoup de temps à peaufiner l'interface d'admin,
|
||||||
|
repoussez ça après avoir livré une première version.
|
||||||
|
|
||||||
|
|
||||||
|
# Manipulation des modèles
|
||||||
|
|
||||||
|
## Mise en pratique
|
||||||
|
|
||||||
|
Créez le modèle `Check` avec les champs `is_up`, `date`, `website`, et
|
||||||
|
`message`.
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Pour le champ `website` vous aurez besoin d'un `models.ForeignKey`, RTFM.
|
||||||
|
|
||||||
|
|
||||||
|
## L'admin
|
||||||
|
|
||||||
|
Ajoutez une interface d'admin pour ce modèle, et ajoutez à la main quelques « *checks* ».
|
||||||
|
|
||||||
|
|
||||||
|
## L'ORM
|
||||||
|
|
||||||
|
C'est l'occasion de sortir un `python manage.py shell`.
|
||||||
|
|
||||||
|
```pycon
|
||||||
|
>>> from watch.models import Website
|
||||||
|
>>> Website.objects.all()
|
||||||
|
```
|
||||||
|
|
||||||
|
Essayer `.all`, `.filter`, `.get`, `.order_by`, et les slices.
|
||||||
|
|
||||||
|
|
||||||
|
# Les URLs
|
||||||
|
|
||||||
|
Changons de sujet et rajoutons des URLs et des vues.
|
||||||
|
|
||||||
|
|
||||||
|
## Les URLs
|
||||||
|
|
||||||
|
Dans `urls.py` on va se rajouter une URL pour la page d'accueil :
|
||||||
|
|
||||||
|
```python
|
||||||
|
from votre_app import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
...
|
||||||
|
...
|
||||||
|
path("", views.index, name=index),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Les vues
|
||||||
|
|
||||||
|
Dans `views.py` de l'app :
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
html = "<html><body><h1>Website Watcher</h1></body></html>"
|
||||||
|
return HttpResponse(html)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
C'est bien mais écrire du HTML dans du Python c'est pas élégant.
|
||||||
|
|
||||||
|
|
||||||
|
## Les vues
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
return render(request, "watch/index.html")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Les templates
|
||||||
|
|
||||||
|
Dans `watch/templates/watch/index.html` ...
|
||||||
|
|
||||||
|
Mais pourquoi `watch` deux fois ??
|
||||||
|
|
||||||
|
|
||||||
|
## Les templates
|
||||||
|
|
||||||
|
Dans `watch/templates/watch/index.html` :
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Website Watch</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Website Watch</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Les vues
|
||||||
|
|
||||||
|
Et si on ajoutais de la données provenant de la DB dans le template ?
|
||||||
|
|
||||||
|
|
||||||
|
## Les vues
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
return render(request, "watch/index.html", {"websites": Website.objects.all()})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Les templates
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body>
|
||||||
|
<h1>Website Watch</h1>
|
||||||
|
<ul>
|
||||||
|
{% for website in websites %}
|
||||||
|
<li>{{ website.host }} {% if website.is_up %}✓{% else %}✗{% endif %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## La Debug Toolbar
|
||||||
|
|
||||||
|
# Les tests
|
||||||
|
|
||||||
|
## Les fixtures
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# L'ORM
|
||||||
|
|
||||||
|
## Les *Managers*
|
||||||
|
|
||||||
|
Exposent les opérations de requête de base de donnée, c'est le `.objects`.
|
||||||
|
|
||||||
|
Ces opérations (des méthodes) renvoient des `queryset`s.
|
||||||
|
|
||||||
|
## Les *Queryset*
|
||||||
|
|
||||||
|
Introduction aux concepts Manager et Queryset, RelatedManager
|
||||||
|
Méthodes simples de requêtage en base de données
|
||||||
|
|
||||||
|
|
||||||
|
# Les URLS
|
||||||
|
|
||||||
|
## path
|
||||||
|
## include
|
||||||
|
## namespaces
|
||||||
|
|
||||||
|
# Les vues
|
||||||
|
## Function based views
|
||||||
|
## Class based views
|
||||||
|
|
||||||
|
# Templates (Jour 2)
|
||||||
|
## filters, tags
|
||||||
|
## extends
|
||||||
|
|
||||||
|
# Forms
|
||||||
|
## Forms fields
|
||||||
|
## Widgets
|
||||||
|
## ModelForm
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
# Modèles
|
||||||
|
## Relations
|
||||||
|
## ForeignKey
|
||||||
|
## ManyToManyField
|
||||||
|
## OneToOneField
|
||||||
|
## InlineModeladmin
|
||||||
|
|
||||||
|
# Users
|
||||||
|
## Groups
|
||||||
|
## Permissions
|
||||||
|
## Authentication
|
||||||
|
How to protect views
|
||||||
|
|
||||||
|
# Static assets (day 3)
|
||||||
|
# Deployment
|
||||||
|
## gunicorn
|
||||||
|
|
||||||
|
# Les bonnes bases : Python
|
||||||
|
|
||||||
|
`*args, **kwargs`
|
||||||
|
|
||||||
|
|
||||||
|
## Les bonnes bases : Python
|
||||||
|
|
||||||
|
La MRO.
|
||||||
|
|
||||||
|
|
||||||
|
## 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 !
|
||||||
|
|
||||||
|
|
||||||
|
## Les bonnes bases : Django
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install Django
|
||||||
|
django-admin startproject demo
|
||||||
|
cd demo
|
||||||
|
./manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
::: 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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
On versionne et on prend le temps de poser un `.gitignore`.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## Les bonnes bases : DRF
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install djangorestframework
|
||||||
|
# Ajouter l'app rest_framework
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## vocabulaire
|
||||||
|
|
||||||
|
Dans DRF on va avoir des `serializers`, des `routers`, des `views` et des `permissions`.
|
||||||
|
|
||||||
|
|
||||||
|
## Le routage de Django
|
||||||
|
|
||||||
|
La requête parcourre les `urlpatterns` du projet, c'est donc à lui
|
||||||
|
d'inclure les `urlpatterns` des différentes applications.
|
||||||
|
|
||||||
|
|
||||||
|
## 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 »).
|
||||||
|
|
||||||
|
|
||||||
|
## Les media types
|
||||||
|
|
||||||
|
Pour représenter des données on utilise un *media type* il existe
|
||||||
|
plusieurs écoles :
|
||||||
|
|
||||||
|
- Tout est liste (`Collection+JSON`, ...).
|
||||||
|
- Tout est article (`atom+xml`, ...).
|
||||||
|
- 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`
|
||||||
|
- ...
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Pensez au web actuel : il mélange text/html, application/javascript, text/css, ...
|
||||||
|
|
||||||
|
|
||||||
|
# Mais qu'est-ce que REST ?
|
||||||
|
|
||||||
|
C'est un ensemble de contraintes :
|
||||||
|
|
||||||
|
- Client-Serveur
|
||||||
|
- Sans état
|
||||||
|
- Une URI identifie une resource
|
||||||
|
- Les resources sont manipulées via leurs représentations
|
||||||
|
- ...
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Pensez au web actuel pour chaque contrainte : ça marche.
|
||||||
|
|
||||||
|
|
||||||
|
## Client-Serveur
|
||||||
|
|
||||||
|
✓
|
||||||
|
|
||||||
|
|
||||||
|
## Sans état
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
|
||||||
|
## Sans état
|
||||||
|
|
||||||
|
Quand on dit « *stateless* » on pense au niveau d'une requête :
|
||||||
|
|
||||||
|
> 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"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
::: 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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
::: notes
|
||||||
|
|
||||||
|
Faire une parenthèse sur les etags, `If-Match`, `If-None-Match`.
|
||||||
|
|
||||||
|
|
||||||
|
## Messages auto-descriptifs
|
||||||
|
|
||||||
|
Toutes les informations nécessaires à l'interprétation du message
|
||||||
|
doivent être dans le message.
|
||||||
|
|
||||||
|
Je n'ai rien contre un lien vers la doc.
|
||||||
|
|
||||||
|
|
||||||
|
## HATEOAS
|
||||||
|
|
||||||
|
C'est celui qui fait peur.
|
||||||
|
|
||||||
|
TL;DR: data + interactions
|
||||||
|
|
||||||
|
::: 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": {
|
||||||
|
"@id": "/cart/",
|
||||||
|
"@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...
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
# La configuration de Django
|
||||||
|
|
||||||
|
## Trois solutions
|
||||||
|
|
||||||
|
- `include local_settings.py`
|
||||||
|
- `django-configurations`
|
||||||
|
- `django-environ`
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
Pour commencer : aucune différence avec Django.
|
||||||
|
|
||||||
|
|
||||||
|
## 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."})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
```
|
BIN
django-initiation/static/stats.png
Normal file
BIN
django-initiation/static/stats.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
40
django-initiation/test.py
Executable file
40
django-initiation/test.py
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from string import ascii_letters, digits
|
||||||
|
import re
|
||||||
|
import doctest
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("file", nargs="+")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true")
|
||||||
|
parser.add_argument("-d", "--debug", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
for file_to_test in args.file:
|
||||||
|
with open(file_to_test) as f:
|
||||||
|
source = f.read()
|
||||||
|
|
||||||
|
with NamedTemporaryFile(mode="w", suffix=".py") as f:
|
||||||
|
for example_match in re.finditer("```python.*?```", source, re.S):
|
||||||
|
example = example_match.group()
|
||||||
|
example = "\n".join(example.split("\n")[1:-1])
|
||||||
|
lineno = source[:example_match.start()].count("\n") + 1
|
||||||
|
function_name = ''.join(letter if letter in ascii_letters + digits else '_' for letter in file_to_test[:-3]) + "_line_" + str(lineno)
|
||||||
|
if example.startswith(">>> "):
|
||||||
|
if '"""' in example:
|
||||||
|
f.write(f"""def _{function_name}():\n r'''""" + example + """\n'''\n\n""")
|
||||||
|
else:
|
||||||
|
f.write(f'''def _{function_name}():\n r"""''' + example + '''\n"""\n\n''')
|
||||||
|
else:
|
||||||
|
f.write(example + "\n\n")
|
||||||
|
f.flush()
|
||||||
|
if args.debug:
|
||||||
|
with open(f.name) as py_source:
|
||||||
|
print(py_source.read())
|
||||||
|
spec = importlib.util.spec_from_file_location("to_test", f.name)
|
||||||
|
to_test = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(to_test)
|
||||||
|
doctest.testmod(to_test, verbose=args.verbose)
|
Loading…
Reference in New Issue
Block a user