formations/django-initiation/django.md

16 KiB

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, …

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 :

python -m pip install django

La théorie — Projet

Pour démarrer un projet, une commande :

django-admin startproject project

La théorie — App

Une fois dans le projet, pour créer une application, une commande :

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 :

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, ...).

Première interface d'admin

En une ligne, pourquoi pas :

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

Leur expliquer que sqlite est utilisé dans les applications : pas besoin d'installer un postgresql pour utiliser Firefox, pourtant Firefox a besoin d'une base de donnée.

La théorie — La DB

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 :)

L'interface d'administration

On a une DB, mais pas encore d'utilisateur admin dedans :

python manage.py createsuperuser

Terminé

On a terminé, on peut démarrer le serveur :

python manage.py runserver

La pratique

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 :

INSTALLED_APPS = [
    "watch",
    ...,
    ...,
]

Les modèles

On va mettre celui-ci dans watch/models.py.

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.

from watch.models import Website


admin.site.register(Website)

Création de la DB

python manage.py makemigrations
python manage.py migrate

::: notes

Expliquer les deux étapes.

Les modèles

Désambiguons makemigrations et migrate d'abord.

L'interface d'administration

On a une DB, mais pas encore d'utilisateur admin dedans :

python manage.py createsuperuser

Terminé

On a terminé, on peut essayer maintenant ?

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 ».

::: notes

Les faire tester ça.

Personalisons

Dans chaque modèle, un __str__ aide l'admin à être lisible.

Personalisons

Dans admin.py on peut préciser les colonnes qu'on veut afficher :

@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 d'accueil :

from watch import views

urlpatterns = [
   ...,
   ...,
   path("", views.index, name="index"),
]

::: notes

C'est un path, un chemin, c'est le chemin vide.

Les URLs

On aurait pu rajouter :

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 :

from django.urls import include

[...]

path("", include("watch.urls")),

::: notes

C'est 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 d'accueil de l'interface d'admin de la page d'accueil de votre application...

namespaces

Avec les espaces de nommage, on a donc :

  • admin:index
  • watch:index

sans ambiguité.

::: notes

Utiliez-en, c'est bien.

Les vues

Dans watch/views.py :

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

Mieux :

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 :

<!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 ajoutais de la données provenant de la DB dans le template ?

Les vues

...
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 l'ORM en passant.

Les vues

Digression :

Il existe aussi des vues basées sur des classes, pouvant s'appuyer sur des modèles.

Les vues

class WebsiteListView(ListView):
    model = Website

Les templates

<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 veux pas répéter l'entête HTML à chaque page…

Les templates

En utilisant extends, on peut réutiliser des templates.

La Debug Toolbar

La Debug Toolbar

python -m pip install django-debug-toolbar

L'ajouter dans settings.py et urls.py.

L'ORM

L'ORM

C'est l'occasion de sortir un python manage.py shell.

>>> 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, il sont accessible via l'attribut objects d'un modèle.

Ses opérations (des méthodes) renvoient des querysets.

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

In [3]: Website.objects.all()
Out[3]: <QuerySet [<Website: mdk.fr>]>

Les Queryset

Pour ceux qui ont fait du SQL c'est un "lazy select" : c'est 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 l'admin, 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 :

class WebsiteForm(forms.Form):
    host = forms.CharField(label='Website hostname',
                           max_length=512)

Forms

On le donne au template :

    return render(
        request,
        "watch/index.html",
        {
            "websites": Website.objects.all(),
            "form": WebsiteForm,
        },
    )

Forms

On l'affiche dans le template :

<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 :

class WebsiteForm(forms.ModelForm):
    class Meta:
        model = Website
        fields = ("host",)

Widgets

Le rendu HTML d'un 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 :

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 c'est douter.

::: notes

Ou pas. Avoir les tests qui passent avant de pousser, avant de merger une PR, avant de mettre en prod c'est 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 d'utiliser les données que vous avez crées via l'admin :

./manage.py dumpdata -o watch/fixtures/initial.json

::: notes

Créez le dossier d'abord ;)

Les fixtures

Profitez-en pour indiquer aux collègues dans le README qu'ils peuvent les charger aussi :

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 n'a aucune importance, que seul le succès de l'import compte. Ici c'est le contraire, le nom du fichier doit commencer par test pour être trouvé.

Les tests

Les tests s'exécutent via :

python manage.py test

Les tests

Tous nos tests seront des instances de TestCase :

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 d'assertions :

  • assertTemplateUsed
  • assertRedirects
  • assertHTMLEqual
  • assertInHTML
  • ...

Les tests

Personnellement habitué à pytest j'abuse 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 d'usage, 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 l'URL /static/.

Static assets

En prod cependant il y a beaucoup plus efficace pour les servir :

./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 le protocole wsgi, un standard en Python.

Deployment

Attention, runserver c'est bien en dev, mais ça n'est pas voué à partir en prod.

::: notes

Sérieusement.

Deployment

runserver c'est un peu comme un groupe éléctrogène :

  • C'est 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, d'autres 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 :

pip install gunicorn
gunicorn -w 16 project.wsgi

Deployment

gunicorn est bien derrière un nginx qui va s'occuper, entre autre, de la décapsulation HTTPS, ou de délivrer vos fichiers statiques sans passer par Python.

Deployment

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 l'objet User, même si on pense ne pas en avoir besoin :

class User(django.contrib.auth.models.AbstractUser):
    ...

et :

AUTH_USER_MODEL = ...

La configuration de Django

  • include local_settings.py
  • django-configurations
  • django-environ

Resources

Pour Django on avait : https://ccbv.co.uk/ (Memo: « Classy Class-Based-View »).