• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/12/19

Ajoutez un formulaire de réservation

Log in or subscribe for free to enjoy all this course has to offer!

Le site est bien avancé ! Dans les trois chapitres à venir vous apprendrez à ajouter un formulaire et à en traiter les données. Impatient ? Je comprends !

Traitement des données d'un formulaire

Envoi des données

Le formulaire de réservation est déjà présent dans la vue detail mais il est tout à fait inefficace pour le moment. Il manque, en effet, l'URL à laquelle il doit être envoyé.

D'ailleurs, allez-vous devoir créer une nouvelle vue ? Vous seriez tenté de le faire. Néanmoins, la manière la plus efficace est d'utiliser la vue detail qui existe déjà. Si le formulaire ne contient aucune erreur, l'utilisateur est redirigé vers une page de remerciement. Mais si une des données est erronée (un e-mail sans arobase, par exemple), la page de réservation est chargée de nouveau. Elle doit afficher les erreurs relatives au formulaire pour que l'utilisateur puisse les corriger et le soumettre de nouveau.

Utiliser deux vues vous obligerait à dupliquer du code ou à effectuer une redirection. Pour l'instant faites-moi simplement confiance. Vous découvrirez plus tard que d'autres considérations rendent plus pratique l'utilisation d'une seule vue.

Ajoutez également l'identifiant de l'album dans la balise input. Voici le formulaire :

detail.html

<form class="form-inline" action="{% url 'store:detail' album_id=album_id %}" method="post">

  <div class="form-group">
    <label for="name" class="control-label">Nom</label>
    <input type="text" name="name" value="" id="name">
  </div>
  <div class="form-group">
    <label for="email" class="control-label">E-mail</label>
    <input type="text" name="email" value="" id="email">
  </div>
  <!-- <input type="hidden" class="hidden" value="{{ album_id }}" name="album_id"> -->

  <button type="submit" class="btn btn-success">Réserver</button>
</form>

Essayez de soumettre le formulaire. Cela échoue, vous laissant seul·e et désemparé·e devant l'erreur suivante : CSRF token missing or incorrect..

La console affiche d'ailleurs une erreur 403 : Accès interdit.

[11/Aug/2017 16:30:27] "POST /store/49/ HTTP/1.1" 403 12181

Quel est cet acronyme inconnu ? Le Larousse ne le connaît pas : je suis doublement désemparé·e.

CSRF signifie Cross-Site Request Forgery. Cela ne vous avance pas beaucoup, j'en ai bien conscience ! Il s'agit d'une technique de piratage informatique qui permet de se faire passer pour un autre utilisateur et de réaliser des actions à sa place.

Lisez attentivement cet excellent article sur Le Blog du Hacker avant de continuer : La faille CSRF, explications et contre-mesures. Faites également un tour sur la page de mise en situation. Cela fait peur ! 😱

Bonne nouvelle : la configuration par défaut de Django inclut une protection contre cette faille.

Toute requête n'utilisant pas une méthode GET doit inclure un jeton unique analysé par le serveur. Le serveur génère ce jeton unique et l'enregistre. Il est également inclus dans le formulaire à l'intérieur d'une balise invisible par l'internaute (elle contient un attribut hidden). Lorsqu'une requête est effectuée, le serveur compare le jeton qu'il possède à celui envoyé par l'utilisateur. Si le jeton ne correspond pas, la demande est rejetée.

Ajoutez simplement la ligne suivante dans le corps du formulaire{% csrf_token %}. Par exemple :

detail.html

<form class="form-inline" action="{% url 'store:detail' album_id=album_id %}" method="post">
    {% csrf_token %}
    ...
</form>

Récupération des données

Si vous relancez la requête, la page se rechargera. C'est normal : notre vue detail intercepte toutes les requêtes. Elle ne fait pas la différence entre une requête GET ou POST. Réglons ce souci tout de suite !

views.py

def detail(request, album_id):
    album = get_object_or_404(Album, pk=album_id)
    artists = [artist.name for artist in album.artists.all()]
    artists_name = " ".join(artists)
    if request.method == 'POST':
        email = request.POST.get('email')
        name = request.POST.get('name')
    ...

Prenez quelques instants pour noter ce que ce formulaire doit réaliser. Voici ce que j'ai noté :

  • Création d'un nouveau contact. Attention : si le contact existe déjà, il ne faut pas le créer une seconde fois !

  • Recherche de l'album à réserver. Si l'album n'existe pas, le serveur doit renvoyer une erreur 404.

  • Création d'une nouvelle réservation associée au contact.

  • Affichage du gabaritmerci.html.

Voici le code correspondant :

views.py

def detail(request, album_id):
    # ...
    if request.method == 'POST':
        email = request.POST.get('email')
        name = request.POST.get('name')

        contact = Contact.objects.filter(email=email)
        if not contact.exists():
            # If a contact is not registered, create a new one.
            contact = Contact.objects.create(
                email=email,
                name=name
            )

        # If no album matches the id, it means the form must have been tweaked
        # so returning a 404 is the best solution.
        album = get_object_or_404(Album, id=album_id)
        booking = Booking.objects.create(
            contact=contact,
            album=album
        )

        # Make sure no one can book the album again.
        album.available = False
        album.save()
        context = {
            'album_title': album.title
        }
        return render(request, 'store/merci.html', context)
    # ...

Créez un nouveau gabarit pour la page de confirmation et ajoutez-y le contenu que vous souhaitez.

merci.html

{% extends 'store/base.html' %}

{% block content %}
    <hr>
    <h1 class="intro-text text-center">Merci !</h1>
    <hr class="detail-separator">
    <p>L'album <em>{{ album_title }}</em> vient d'être retiré des rayonnages. Une main délicate a déposé votre nom sur sa couverture et l'a rangé en vous attendant. Dépêchez-vous ! <em>{{ album_title }}</em> trépigne d'impatience et chante déjà vos louanges.</p>
    <hr>
    <div class="embed-responsive embed-responsive-16by9">
        <iframe class="embed-responsive-item" src="https://www.youtube.com/embed/ZSbuikMdgqA" frameborder="0" allowfullscreen></iframe>
    </div>
{% endblock %}

Essayez de réserver un album : la page de confirmation apparaît !

A ce stade, vous êtes certainement à deux doigts de ranger votre ordinateur en fredonnant "Let's party !" (ou pas...). Mais désolée de vous casser votre rêve : le travail est loin d'être terminé ! Reprenez votre pelle et votre pioche, nous devons valider les données.

Validation des données

Pourquoi continuer ? Le formulaire de réservation fonctionne !

Certes, mais le monde est rempli d'internautes étourdis aux doigts engourdis. Une faute de frappe est si vite arrivée ! Sans une adresse e-mail valide, comment retrouver l'utilisateur ? Lui envoyer un e-mail pour le prévenir que vous l'attendez toujours ? Impossible ! Par ailleurs, nous avons placé des champs libres. Comme c'est dangereux ! Tant d'informations peuvent y transiter... Il est donc plus prudent de vérifier que les données enregistrées correspondent à celles que vous attendez réellement.

Encore une fois, vous pouvez coder ces vérifications à la main ou utiliser Django ! Si vous voulez bien nous allons plutôt explorer la deuxième opportunité !

Django contient un module conçu spécialement pour les formulaires. Il permet à la fois de générer les formulaires dans un gabarit et de valider les données reçues. Génial, n'est-ce pas ?!

Création de la classe d'un formulaire

Créez un nouveau document appelé forms, au même niveau que les modèles, dans lequel vous importerez le module forms :

store/forms.py

from django import forms

Chaque formulaire à valider est représenté par une classe qui hérite de forms.Form. Lire le code source

from django import forms

class ContactForm(forms.Form):
    pass

Chaque attribut de classe représente un champ du formulaire à valider. Prenons le cas du champ name du formulaire de réservation. Selon son modèle, vous savez que les valeurs saisies doivent être inférieures à 100 caractères et qu'il s'agit d'une chaîne de caractères. Vous pouvez donc écrire ceci :

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)

Cette ligne validera que le nom est bien une chaîne inférieure ou égale à 100 caractères. Mais elle fait bien plus ! Si vous lui demandez gentiment, elle peut également générer une balise HTML valide. Etant donné que vous avez spécifié qu'il s'agissait d'une chaîne, le code généré sera de la forme suivante :

<input type="text" name="name" maxlength="100" id="id_name">

En paramètre, vous pouvez spécifier des informations importantes telles que le label, la classe ou sa présence.

Complétez le formulaire en conséquence :

forms.py

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(
        label='Nom',
        max_length=100,
        widget=forms.TextInput(attrs={'class': 'form-control'}),
        required=True
        )
    email = forms.EmailField(
        label='Email',
        widget=forms.EmailInput(attrs={'class': 'form-control'}),
        required=True)

Utilisation d'un formulaire dans un gabarit

A présent, utilisez la classe du formulaire pour générer les champs automatiquement dans le gabarit. Commencez par créer une instance de ContactForm() dans la vue puis utilisez-la dans le gabarit :

views.py

from .forms import ContactForm
# ...

def detail(request, album_id):
    if request.method == 'POST':
        # ...
    else:
        # GET method. Create a new form to be used in the template.
        form = ContactForm()
    context = {
        ...
        'form': form
    }

Attaquez-vous maintenant au gabarit ! Utilisez l'objet form et spécifiez le champ que vous désirez afficher à la place des balises HTML.

detail.html

<label for="{{ form.name.id_for_label }}">
<!-- A SUPPRIMER <input type="text" name="name" value="" id="name"> -->
{{ form.name }}

...

<label for="{{ form.email.id_for_label }}">
<!-- A SUPPRIMER <input type="text" name="email" value="" id="email"> -->
{{ form.email }}

Affichez de nouveau la page et vérifiez que tout est là ! Voici le HTML généré :

<!-- A SUPPRIMER <input type="text" name="name" value="" id="name"> -->
<input type="text" name="name" class="form-control" maxlength="100" required="" id="id_name">

<!-- A SUPPRIMER <input type="text" name="email" value="" id="email"> -->
<input type="email" name="email" class="form-control" required="" id="id_email">

Comme vous pouvez le constater, Django a ajouté plusieurs attributs dans le but de valider la forme des valeurs présentes avant qu'elles ne soient envoyées vers le serveur.

Validation des valeurs d'un formulaire dans une vue

Vous y êtes presque ! Une première validation est désormais effectuée côté client. Vous pouvez effectuer une seconde vérification du côté du serveur.

Prenons l'exemple de l'adresse e-mail. Si l'utilisateur oublie l'arobase, il ne pourra soumettre le formulaire car une erreur apparaîtra directement dans le champ. Si l'erreur est plus subtile, elle peut toujours passer entre les mailles du filet. Par exemple, si l'utilisateur entre l'adresse "hey@you".

Ajoutez donc une seconde validation dans la vue ! La méthode is_valid() renvoie un booléen indiquant si les champs du formulaire sont corrects.

views.py

def detail(request, album_id):
    # ...
    context = {
        'album_title': album.title,
        'artists_name': artists_name,
        'album_id': album.id,
        'thumbnail': album.picture
    }
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # Form is correct.
            # We can proceed to booking.
            email = request.POST.get('email')
            name = request.POST.get('name')
            # ...
    context['form'] = form
    return render(request, 'store/detail.html', context)

Si le formulaire n'est pas valide, cela signifie qu'il contient des erreurs ! Ajoutez-les au contexte du gabarit :

views.py

def detail(request, album_id):
    # ...
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # ...
        else:
            # Form data doesn't match the expected format.
            # Add errors to the template.
            context['errors'] = form.errors.items()

Enfin, ajoutez la gestion des erreurs dans le gabarit :

detail.html

<form>
  <div class="form-group  {% if form.name.errors %}has-warning has-feedback{% endif %}">
    ...
  </div>

  <div class="form-group  {% if form.email.errors %}has-warning has-feedback{% endif %}">
    ...
  </div>
...
</form>

{% if errors %}
<div>
  {% for key, error in errors %}
  {% autoescape off %}
    {{ error }}
  {% endautoescape %}
  {% endfor %}
</div>
{% endif %}

Et ça fonctionne !

Quelles que soient les données envoyées avec un formulaire, si is_valid() renvoie True Django les transfère dans un dictionnaire facilement utilisable : form.cleaned_data.

Utilisez-le dans la vue comme suit :

views.py

def detail(request, album_id):
    # ...
    if form.is_valid():
        email = form.cleaned_data['email']
        name = form.cleaned_data['name']

Bravo, c'est terminé !

Dans le prochain chapitre nous irons un peu plus loin dans l'univers des formulaires ! A tout de suite, moussaillon !

Code de ce chapitre

Retrouvez l'intégralité du code sur ce dépôt GitHub.

Example of certificate of achievement
Example of certificate of achievement