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