formations/django-initiation/django.md

952 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Django Initiation
par
Julien Palard <julien@palard.fr>
https://mdk.fr
notes:
Introduce yourself!
# Django
Django est une infrastructure dapplications web populaire et robuste.
> The web framework for perfectionists with deadlines.
## Django : qui lutilise ?
- Instagram
- Pineterest
- Mozilla
- National Geographic
- [Washington Post](https://www.djangoproject.com/weblog/2005/dec/08/congvotes/)
- ...
## Vocabulaire
Dans Django on va avoir principalement des `models`, des `vues`, des
`templates`, et des `urls`.
# Les bonnes bases
On travaillera toujours dans un `venv` :
```bash
python -m pip install django
```
# La théorie — Projet
Pour démarrer un projet, une commande :
```bash
django-admin startproject project
```
## La théorie — App
Une fois dans le projet, pour créer une application, une commande :
```bash
python manage.py startapp watch
```
notes:
(et ajout dans `settings.py`)
## La théorie — Modèle
Un « modèle » est la description dune 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 dexpliquer les fields (leur relation avec la DB), et
les differents endroits ou Django peut reutiliser cette information
(widgets, validation, ...).
## Première interface dadmin
En une ligne, pourquoi pas :
```python
admin.site.register(Website)
```
## La théorie — la DB
- PostgreSQL
- MySQL
- sqlite
- ...
notes:
Leur faire croire 2 secondes quon va devoir sinstaller et se
configurer un serveur de base de donnée :D
Leur expliquer que sqlite est utilisé dans les applications : pas
besoin dinstaller un postgresql pour utiliser Firefox, pourtant
Firefox a besoin dune base de donnée.
## 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 cest surtout daller tester ça :)
## Linterface dadministration
On a une DB, mais pas encore dutilisateur admin dedans :
```bash
python manage.py createsuperuser
```
## Terminé
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 dexpliquer larborescence, de se promener,
dy lire les commentaires.
## La pratique
Ajout de lapp `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 dadmin
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.
## Les modèles
Désambiguons `makemigrations` et `migrate` dabord.
## Linterface dadministration
On a une DB, mais pas encore dutilisateur admin dedans :
```bash
python manage.py createsuperuser
```
## Terminé
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 linterface dadmin, créer quelques sites...
## Linterface dadministration
Les modèles, leurs `fields` ne servent donc pas qua lORM, cette
interface dadmin nous a demandé une ligne de code.
notes:
Si ce nest pas déjà fait, leur faire ajouter des `__str__`.
## Astuce
On peut passer beaucoup de temps à peaufiner linterface dadmin,
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 dun `models.ForeignKey`, RTFM.
## Ladmin
Ajoutez une interface dadmin pour ce modèle, et ajoutez à la main quelques « *checks* ».
notes:
Les faire tester ça.
## Personalisons
Dans chaque modèle, un `__str__` aide ladmin à être lisible.
## Personalisons
Dans `admin.py` on peut préciser les colonnes quon veut afficher :
```python
@admin.register(Website)
class WebsiteAdmin(admin.ModelAdmin):
list_display = ("host", "is_up", "last_check")
@admin.register(Check)
class CheckAdmin(admin.ModelAdmin):
list_display = ("website", "date", "is_up", "message")
```
# Les URLs & les vues
Changons complètement de sujet : les URLs et des vues.
## Les URLs
Dans `project/urls.py` on va se rajouter une URL pour la page daccueil :
```python
from watch import views
urlpatterns = [
...,
...,
path("", views.index, name="index"),
]
```
notes:
Cest un `path`, un chemin, cest le chemin vide.
## Les URLs
On aurait pu rajouter :
```python
path("about", views.about, name="about"),
path("help", views.help, name="help"),
...
```
## include
Petite parenthèse, on aurait pu mettre un `urlpatterns` dans
`watch/urls.py`, et les inclure dans `project/urls.py` en utilisant :
```python
from django.urls import include
[...]
path("", include("watch.urls")),
```
notes:
Cest pratique pour « ancrer » un ensemble de chemin sous un autre
chemin : pour se faire une hierarchie.
## namespaces
Les espaces de nommage permettent de désambiguer les urls nommées :
`index` est le nom de la page daccueil de linterface dadmin ou de la
page daccueil de votre application ?
## namespaces
Avec les espaces de nommage, on a donc :
- `admin:index`
- `watch:index`
sans ambiguité.
notes:
Utiliez-en, cest bien.
## Les vues
Dans `watch/views.py` :
```python
from django.http import HttpResponse
def index(request):
html = "<html><body><h1>Website Watcher</h1></body></html>"
return HttpResponse(html)
```
notes:
Cest bien mais écrire du HTML dans du Python cest pas élégant.
## Les vues
Mieux :
```python
from django.http import HttpResponse
from django.shortcuts import render
def index(request):
return render(request, "watch/index.html")
```
## Les templates
Django va chercher `watch/index.html` dans tous les dossiers de
templates, dont `watch/templates/`.
## Les templates
Donc 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>
```
notes:
La création du dossier `templates/` est typiquement quelque chose que
`runserver` ne voit pas, il faut le redémarrer.
## Les vues
Et si on ajoutait de la donnée provenant de la DB dans le template ?
## Les vues
```python
...
from watch.models import Website
def index(request):
return render(request, "watch/index.html",
{"websites": Website.objects.all()})
```
notes:
Ne pas oublier les imports…
Premier apperçu de lORM en passant.
## Les vues
Digression :
Il existe aussi des vues basées sur des classes, pouvant sappuyer sur
des modèles.
## Les vues
```python
class WebsiteListView(ListView):
model = Website
```
## 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>
```
## Les templates
Ça fonctionne, mais on ne veut pas répéter lentête HTML à chaque
page…
## Les templates
En utilisant `extends`, on peut réutiliser des templates.
## La Debug Toolbar
![](https://raw.githubusercontent.com/django-debug-toolbar/django-debug-toolbar/master/example/django-debug-toolbar.png)
## La Debug Toolbar
```bash
python -m pip install django-debug-toolbar
```
Lajouter dans `settings.py` et `urls.py`.
# LORM
## LORM
Cest loccasion 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 *Managers*
Les *managers* représentent une table, ils sont accessibles via
lattribut `objects` dun modèle.
Ses opérations (des méthodes) renvoient des `queryset`s.
```pycon
In [2]: Website.objects
Out[2]: <django.db.models.manager.Manager at 0x7fa77a9a1500>
```
## Les instances de modèles
Les instances de modèles représentent une ligne de la table.
## Les *Queryset*
Représentent un ensemble de lignes de la base de donnée. Ils ont les
mêmes méthodes que les *managers* :
- filter
- get
- order_by
- ...
## Les *Queryset*
```pycon
In [3]: Website.objects.all()
Out[3]: <QuerySet [<Website: mdk.fr>]>
```
## Les *Queryset*
Pour ceux qui ont fait du SQL cest un "lazy select" : cest un
`SELECT` qui ne séxécutera que si nécessaire.
## Les *RelatedManager*
Sont des managers, mais sur les relations :
```
In [4]: Website.objects.get(host="mdk.fr").check_set.all()
Out[4]: <QuerySet [<Check: mdk.fr is up>, <Check: mdk.fr is up>]>
```
# Forms
Comme pour ladmin, Django peut utiliser les informations des modèles
pour vous aider à générer des formulaires.
## Forms fields
Créer un formulaire ressemble à créer un modèle, on décrit les champs :
```python
class WebsiteForm(forms.Form):
host = forms.CharField(label="Website hostname",
max_length=512)
```
## Forms
On le donne au template :
```python
return render(
request,
"watch/index.html",
{
"websites": Website.objects.all(),
"form": WebsiteForm,
},
)
```
## Forms
On laffiche dans le template :
```html
<form action="/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Add">
</form>
```
## Forms
Mais là on a pas utilisé les informations données par les modèles…
## ModelForm
Avec un `ModelForm` on ne répète pas les attributs fields :
```python
class WebsiteForm(forms.ModelForm):
class Meta:
model = Website
fields = ("host",)
```
## Widgets
Le rendu HTML dun champ de formulaire est appelé un `Widget`, il est
possible de le changer, par exemple si vous préférez un `<input` à un
`<textarea` ou vice-versa.
## Validation
Une instance de formulaire a une méthode `is_valid` :
```python
if request.method == "POST":
form = WebsiteForm(request.POST)
if form.is_valid():
Website.objects.create(host=form.cleaned_data["host"])
```
notes:
Django peut donc vérifier que les champs non-empty sont bien pas
vides, que les longuers sont respectées, etc…
# Les tests
Tester cest douter.
notes:
Ou pas. Avoir les tests qui passent avant de pousser, avant de merger
une PR, avant de mettre en prod cest un véritable confort.
## Les fixtures
Pour tester on va avoir besoin de données de test.
On appelera ça des « fixtures ».
## Les fixtures
Le moyen simple de créer des fixtures est dutiliser les données que
vous avez crée via ladmin :
```bash
./manage.py dumpdata -o watch/fixtures/initial.json
```
notes:
Créez le dossier dabord ;)
## Les fixtures
Profitez-en pour indiquer aux collègues dans le README quils peuvent
les charger aussi :
```bash
git clone …
cd project
./manage.py migrate
./manage.py loaddata initial
```
## Les tests
Par défaut Django utilise la bibliothèque native `unittest`, on
placera les tests de nos applications dans le dossier `tests` ou le
fichier `tests.py` des applications.
notes:
Habituellement on dit que le nom du fichier na aucune importance, que
seul le succès de limport compte. Ici cest le contraire, le nom du
fichier doit commencer par `test` pour être trouvé.
## Les tests
Les tests sexécutent via :
```bash
python manage.py test
```
## Les tests
Tous nos tests seront des instances de `TestCase` :
```python
from django.test import TestCase
class WatchTestCase(TestCase):
fixtures = ["initial"]
def test_can_access_home(self):
...
```
## Les tests
Pour vérifier que tout se passe bien `unittest` et `Django` nous
fournissent une collection dassertions :
- assertTemplateUsed
- assertRedirects
- assertHTMLEqual
- assertInHTML
- ...
## Les tests
Personnellement habitué à pytest jabuse de `assert`. Attention, bien
que ça fonctionne, les messages ne sont pas aussi lisibles.
# Les relations entres modèles
On a déjà fait une `ForeignKey`, mais il existe aussi :
- OneToOneField
- ManyToManyField
notes:
Parler des cas dusage, et parler de `thrue`.
# Users
- Groups
- Permissions
- Authentication
# Static assets
Chaque application peut déposer des fichiers dans son dossier `/static/`.
## Static assets
En dev ils seront tous accessibles via lURL `/static/`.
## Static assets
En prod cependant il y a beaucoup plus efficace pour les servir :
```bash
./manage.py collectstatic
```
Puis configurez votre serveur HTTP (`nginx`/`apache2`) pour servir le
dossier généré sans passer par Python.
# Deployment
En parlant de prod, comment mettre un Django en prod ?
## Deployment
Django utilise les protocoles `wsgi` (synchrone) et `asgi`
(asynchrone), des classiques en Python.
## Deployment
Attention, `runserver` cest bien en dev, mais ça nest pas fait pour la prod.
notes:
Sérieusement.
## Deployment
`runserver` cest un peu comme un groupe éléctrogène :
- Cest vrai que ça fonctionne.
- Ça dépanne quand on est seul dessus, chez soi.
- Mais on alimente pas un quartier ou une ville avec.
notes:
Trouver mieux :D
## Deployment
Nginx et Apache2 gèrent `wsgi`, dautres serveurs aussi, probablement.
## Deployment
Mais en production on ne veut pas juste une instance de Django, on en
veut plusieurs, pour traiter plus de requêtes, pour ça on peut
utiliser `gunicorn`, `uwsgi`, …
## Deployment
`gunicorn` est un bon point de départ :
```bash
pip install gunicorn
gunicorn -w 16 project.wsgi
```
## Deployment
`gunicorn` est bien derrière un `nginx` qui va soccuper, entre autres,
de la décapsulation HTTPS, ou de délivrer vos fichiers statiques sans
passer par Python.
## Deployment
```nginx
server {
listen 443 http2 ssl;
server_name example.com;
include snippets/letsencrypt-example.com.conf;
location /static {
alias /opt/example.com/static/;
}
location / {
proxy_pass http://unix:/run/example.com/wsgi.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Protocol $scheme;
}
}
```
# Bonnes pratiques
Pas de `order_by` / `fiter` / … dans les vues, rangez ça dans les
modèles (dans des *managers* personalisés).
Ça permet de nommer et de réutiliser.
## Bonnes pratiques
La gestion des dépendances avec `pip-compile`.
## Bonnes pratiques
Versionnez !!
## Bonnes pratiques
Prennez 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
On surcharge lobjet `User`, même si on pense ne pas en avoir besoin :
```python
class User(django.contrib.auth.models.AbstractUser):
...
```
et :
```python
AUTH_USER_MODEL = ...
```
notes:
Enfin, si on est **100% sûrs** que notre application ne sera pas
réutilisée dans un contexte où une autre app redéfinit l'object
`User`
## La configuration de Django
- `include local_settings.py`
- `django-configurations`
- `django-environ`
## Resources
- La doc officielle.
- Pour Django : https://ccbv.co.uk/ (Memo : « Classy Class-Based-View »).
- Pour Django Rest Framework : https://www.cdrf.co (Memo : « Classy Django REST Framework »).