312 lines
12 KiB
Markdown
312 lines
12 KiB
Markdown
---
|
||
title: Formulaires Django
|
||
author: Steve Kossouho
|
||
---
|
||
|
||
# Saisir des données avec les formulaires
|
||
|
||
----
|
||
|
||
## Les formulaires HTML
|
||
|
||
Le web n'a pas toujours eu de formulaires, mais c'est arrivé très tôt; Le navigateur [Mosaic du NCSA](https://en.wikipedia.org/wiki/NCSA_Mosaic) a introduit le
|
||
concept dans sa version 2.0, publiée en novembre 1993. Ils sont rentrés de facto dans la spécification HTML, qui a subi quelques améliorations depuis.
|
||
|
||
En HTML, les formulaires permettent à l'utilisateur d'interagir et d'envoyer des données.
|
||
Django fournit une classe `Form`{.python} qui permet de facilement construire des formulaires et d'abstraire leur validation du contenu.
|
||
|
||
----
|
||
|
||
## Les formulaires avec Django
|
||
|
||
Pour pouvoir afficher des formulaires dans un site Django, il faut écrire des classes Python qui héritent d'une classe `Form`{.python} se trouvant dans le module `django.forms`{.python}. Les classes de formulaire décrivent les champs du formulaire à afficher, ainsi que leur type (texte, fichier, date, etc.) et leur contenu.
|
||
|
||
```python {.numberLines}
|
||
from datetime import date
|
||
from django import forms
|
||
|
||
class PersonForm(forms.Form):
|
||
"""Exemple de formulaire avec des champs de types divers."""
|
||
|
||
first_name = forms.CharField(max_length=32, label="first name")
|
||
last_name = forms.CharField(max_length=32, label="last name")
|
||
birth_date = forms.DateField(initial=date(1990, 1, 1), label="birthday")
|
||
number = forms.IntegerField(label="phone number")
|
||
password = forms.CharField(widget=forms.PasswordInput, max_length=50, label="password")
|
||
```
|
||
|
||
Les classes de formulaire déclarent des attributs dont les types sont des instances de classes héritant de `Field`{.python}.
|
||
|
||
[Documentation des champs de formulaires](https://docs.djangoproject.com/en/dev/ref/forms/fields/)
|
||
|
||
----
|
||
|
||
## Utilisation de formulaires
|
||
|
||
Une fois un formulaire défini, on peut l'utiliser dans une vue.
|
||
|
||
```python {.numberLines}
|
||
from django.shortcuts import render
|
||
from demonstration.forms import PersonForm
|
||
|
||
def view_person_form(request):
|
||
"""Utilisation du formulaire PersonForm."""
|
||
form = PersonForm(data=request.POST or None)
|
||
return render(request, "person-form.html", {"form": form})
|
||
```
|
||
|
||
Lorsqu'une vue affiche un formulaire, elle doit l'initialiser. Un objet de formulaire peut être initialisé de deux manieres :
|
||
|
||
- `PersonForm(data=request.POST)`{.python} si la requête HTTP est de type `POST`;
|
||
- `PersonForm()`{.python} si la requête HTTP est de type `GET`.
|
||
|
||
Dans le premier cas, les données ``POST: dict``{.python} seront utilisées pour les associer à l'objet formulaire.
|
||
Dans le second cas, le formulaire sera vide et s'affichera sans contenu dans le template.
|
||
|
||
----
|
||
|
||
## Utilisation des formulaires dans les templates
|
||
|
||
Lorsque l'on a instancié un objet `Form`{.python}, nous l'envoyons au contexte du template.
|
||
Interpoler l'objet suffit à en afficher les champs. Les données liées à l'objet seront visibles dans les champs.
|
||
|
||
```djangotemplate {.numberLines}
|
||
...
|
||
<body>
|
||
<h1>Person form</h1>
|
||
{# Envoyer le formulaire à l'URL courante avec la methode POST #}
|
||
<form method="post">
|
||
{% csrf_token %}
|
||
{{ form }}
|
||
<input type="submit" value="OK">
|
||
</form>
|
||
</body>
|
||
```
|
||
|
||
Notez que vous avez la responsabilité de fournir la balise `<form>`{.html} et la classique balise `<input type="submit">`{.html} pour faire fonctionner le formulaire.
|
||
|
||
----
|
||
|
||
La représentation de l'objet `Form`{.python} dans un template peut prendre plusieurs formes.
|
||
|
||
| Syntaxe | Description |
|
||
|-----------------------------------|---------------------------------------------------------------------------------------------|
|
||
| `{{ form }}`{.djangotemplate} | Affiche les champs dans des balises `<div>`{.html}. |
|
||
| `{{ form.as_p }}`{.djangotemplate} | Affiche les champs dans des balises `<p>`{.html}. |
|
||
| `{{ form.as_table }}`{.djangotemplate} | Affiche les champs dans des balises `<th>`{.html} et `<td>`{.html}. |
|
||
|
||
Les options sont limitées mais suffisantes. Pour pouvoir utiliser un rendu compatible avec des bibliothèques CSS telles que Bootstrap, Bulma ou Foundation, on conseillera une application externe telle que [django-crispy-forms](https://django-crispy-forms.readthedocs.io/en/latest/index.html).
|
||
|
||
----
|
||
|
||
### Attention lors du rendu
|
||
|
||
Notez que l'interpolation d'un objet formulaire dans un template ne génère jamais les balises `<form></form>`{.html} nécessaires,
|
||
ni même la classique balise `<input type="submit">`{.html} pour faire fonctionner le formulaire.
|
||
|
||
**La raison est simple** : les classes de formulaires existent notamment pour simpler
|
||
l'affichage et la validation des données.
|
||
Définir la destination de l'envoi des données et les boutons de validation est de la responsabilité du programmeur.
|
||
Techniquement, cela permet par exemple de cumuler plusieurs objets formulaire dans la même balise `<form>`{.html}, si besoin.
|
||
|
||
----
|
||
|
||
## Validation des formulaires
|
||
|
||
Un objet `Form`{.python} peut aussi gérer la validation des données. Par défaut, il est capable de s'assurer que tous les champs obligatoires
|
||
sont remplis et que les données sont valides. Nous verrons par la suite comment on peut personnaliser la validation.
|
||
|
||
Pour confirmer dans la vue que des données expédiées par un formulaire sont correctes, nous devons utiliser la méthode `is_valid()`{.python}.
|
||
|
||
```python {.numberLines}
|
||
from django.shortcuts import render, redirect
|
||
from demonstration.forms import PersonForm
|
||
|
||
def view_person_form(request):
|
||
"""Utilisation du formulaire PersonForm."""
|
||
form = PersonForm(data=request.POST or None)
|
||
if form.is_valid():
|
||
... # Traitement des données
|
||
return redirect("accueil")
|
||
return render(request, "person-form.html", {"form": form})
|
||
```
|
||
|
||
----
|
||
|
||
### La méthode `is_valid()`{.python}
|
||
|
||
La méthode `is_valid()`{.python} d'un formulaire renvoie `True`{.python} si toutes les données associées au formulaire sont correctes et `False`{.python} sinon.
|
||
Cette méthode exécute plusieurs méthodes de validation de l'objet qui s'occupent entre autres de confronter les données entrantes et le type des champs.
|
||
|
||
Si le formulaire contient des données considérées incorrectes, l'objet contiendra des attributs `.field_errors`{.python} et `.non_field_errors`{.python} contenant des messages d'erreur,
|
||
qui seront automatiquement affichés dans le template pour alerter l'utilisateur.
|
||
|
||
----
|
||
|
||
```djangotemplate {.numberLines}
|
||
<form action="" method="post" name="person-form">
|
||
{% csrf_token %}
|
||
<table class="form-table">
|
||
{{ form.as_table }}
|
||
<tr>
|
||
<th></th>
|
||
<td><input type="submit" name="submit-form" value="Validate"></td>
|
||
</tr>
|
||
</table>
|
||
</form>
|
||
```
|
||
|
||
**Note** : La balise `{% csrf_token %}`{.djangotemplate} est une protection obligatoire lorsqu'on gère la méthode HTTP `POST`. Django est configuré par défaut pour qu'une page demandée via la méthode `POST` et qui ne possède pas une donnée de formulaire valide pour l'entrée `csrf_middleware_token` provoque une erreur HTTP 403.
|
||
|
||
----
|
||
|
||
## Validation de la saisie utilisateur
|
||
|
||
Django propose un système simple dans les formulaires permettant de contrôler la saisie de données des utilisateurs.
|
||
Le système consiste principalement à écrire des méthodes des types suivants :
|
||
|
||
- `def clean_<field_name>() -> Any`{.python} : vérifie l'entrée du champ `field_name`
|
||
- `def clean() -> dict`{.python} : vérifier l'entrée de tous les champs
|
||
|
||
----
|
||
|
||
Les méthodes `clean_<field_name>()`{.python} renvoient une donnée Python correspondant au type du champ,
|
||
ou peuvent lever une exception de type `ValidationError` si la donnée du champ est incorrecte ou non autorisée.
|
||
|
||
La méthode `clean()`{.python}, elle, renvoie un dictionnaire de données, mais peut aussi vérifier
|
||
plusieurs champs à la fois. Elle peut également lever une `ValidationError`{.python}.
|
||
Dans ce cas, le formulaire contient des données dans `.non_field_errors`{.python}.
|
||
|
||
:::notes
|
||
Jeter un œil à la documentation en ligne pour en voir un exemple.
|
||
:::
|
||
|
||
----
|
||
|
||
```python {.numberLines}
|
||
from django import forms
|
||
class MyForm(forms.Form):
|
||
text = forms.CharField(max_length=10, label="Texte")
|
||
|
||
def clean_text(self):
|
||
return self.data["text"] # Va aller dans le dictionnaire `self.cleaned_data`
|
||
|
||
def clean(self):
|
||
return super().clean() # Renvoie un dictionnaire des données de champs "nettoyées"
|
||
```
|
||
|
||
----
|
||
|
||
## Gestion des téléchargements et fichiers média
|
||
|
||
La gestion des téléchargements est toujours un cas un peu délicat, que Django permet de gérer.
|
||
|
||
----
|
||
|
||
Pour gérer le téléchargement, il faut remplir quelques conditions :
|
||
|
||
- Créer un répertoire
|
||
- Définir `settings.MEDIA_URL`{.python} et `settings.MEDIA_ROOT`{.python}
|
||
- Avoir un formulaire HTML avec la méthode `POST` et le type d'encodage
|
||
`<form enctype="multipart/form-data">`{.html}.
|
||
|
||
Pour accéder aux fichiers uploadés :
|
||
|
||
- Ajouter `+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)`{.python} aux URLs du
|
||
projet pour servir les fichiers (ne fonctionne pas si `settings.DEBUG = False`{.python})
|
||
|
||
----
|
||
|
||
```{.python .numberLines}
|
||
MEDIA_ROOT = BASE_DIR / "media" # par exemple
|
||
MEDIA_URL = "/media/"
|
||
```
|
||
|
||
```{.python .numberLines}
|
||
from django.conf import settings
|
||
from django.conf.urls.static import static
|
||
from django.urls import path, include
|
||
|
||
urlpatterns = [
|
||
path("", view_index, name="index"),
|
||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||
```
|
||
|
||
```{.djangotemplate .numberLines}
|
||
<form method="post" action="" enctype="multipart/form-data">
|
||
{% csrf_token %} {{ form }} <input type="submit" value="OK">
|
||
</form>
|
||
```
|
||
|
||
Le code (approximatif) à ajouter pour prendre en compte les fichiers média et pour pouvoir les
|
||
afficher.
|
||
|
||
----
|
||
|
||
```{.python .numberLines}
|
||
class UploadForm(forms.Form):
|
||
"""Example of file upload form."""
|
||
image = forms.ImageField(max_length=128, required=True, label="image")
|
||
|
||
def save_uploaded_file(request: HttpRequest) -> str:
|
||
"""Custom method to process the upload of an image."""
|
||
storage = FileSystemStorage()
|
||
image = request.FILES["image"] # l'attribut FILES est un dictionnaire de champs fichier
|
||
name = storage.save(None, image)
|
||
return storage.url(name)
|
||
```
|
||
|
||
Exemple de formulaire et d'une fonction à appeler pour sauver le fichier et récupérer son URL.
|
||
|
||
----
|
||
|
||
```{.python .numberLines}
|
||
def view_upload_form(request: HttpRequest): # noqa
|
||
"""View for the upload form."""
|
||
form = UploadForm(request.POST, request.FILES) if request.method == "POST" else UploadForm()
|
||
if form.is_valid():
|
||
image_url = save_uploaded_file(request)
|
||
return render(request, "template.html", {"form": form, "image_url": image_url})
|
||
```
|
||
|
||
Exemple de vue utilisant notre formulaire
|
||
|
||
----
|
||
|
||
## Divers : Formulaire de recherche
|
||
|
||
L'écriture d'un formulaire de recherche est régie par les étapes suivantes :
|
||
|
||
- Afficher (méthode GET) la page contenant uniquement le formulaire.
|
||
- Valider le formulaire vers la même page (POST ou GET).
|
||
- Si le formulaire est valide, ajouter les résultats au contexte du template.
|
||
- Dans le template, s'il y a des résultats, alors les afficher sous le formulaire. Sinon, n'afficher que le formulaire.
|
||
|
||
----
|
||
|
||
## Divers : Applications tierces pour gérer les formulaires
|
||
|
||
Il existe plusieurs applications dédiées à un affichage avancé des formulaires. La plus aboutie et réputée est `django-crispy-forms`.
|
||
|
||
```bash {.numberLines}
|
||
pip install django-crispy-forms
|
||
```
|
||
|
||
[Page officielle de la documentation du package](https://django-crispy-forms.readthedocs.io/en/latest/)
|
||
|
||
----
|
||
|
||
## Divers : Applications tierces pour déboguer vos pages
|
||
|
||
Une application célèbre et utile pour déboguer vos pages en live est `django-debug-toolbar`.
|
||
|
||
```bash {.numberLines}
|
||
pip install django-debug-toolbar
|
||
```
|
||
|
||
[Page officielle de la documentation du package](https://django-debug-toolbar.readthedocs.io/en/latest/)
|
||
|
||
----
|
||
|
||
## Atelier
|