Gestion de formulaires avec les vues fondées sur les classes

Le traitement de formulaires utilise généralement trois parcours :

  • Affichage initial GET (vierge ou contenu pré-rempli)

  • Envoi POST avec données non valides (réaffiche normalement le formulaire avec indication des erreurs)

  • Envoi POST avec données valides (traitement des données normalement suivi par une redirection)

Lorsqu’on implémente soi-même ces étapes, il en résulte souvent beaucoup de code répétitif (voir Utilisation d’un formulaire dans une vue). Pour l’éviter, Django fournit un ensemble de vues génériques fondées sur les classes dédiées au traitement des vues.

Formulaires élémentaires

Étant donné un formulaire de contact :

forms.py
from django import forms


class ContactForm(forms.Form):
    name = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

    def send_email(self):
        # send email using the self.cleaned_data dictionary
        pass

La vue peut être construite en utilisant une classe FormView:

views.py
from myapp.forms import ContactForm
from django.views.generic.edit import FormView


class ContactFormView(FormView):
    template_name = "contact.html"
    form_class = ContactForm
    success_url = "/thanks/"

    def form_valid(self, form):
        # This method is called when valid form data has been POSTed.
        # It should return an HttpResponse.
        form.send_email()
        return super().form_valid(form)

Notes :

Formulaires de modèles

Generic views really shine when working with models. These generic views will automatically create a ModelForm, so long as they can work out which model class to use:

  • Si l’attribut model est présent, c’est cette classe de modèle qui sera utilisée.

  • If get_object() returns an object, the class of that object will be used.

  • Si un attribut queryset est présent, c’est le modèle correspondant à ce jeu de requête qui sera utilisé.

Model form views provide a form_valid() implementation that saves the model automatically. You can override this if you have any special requirements; see below for examples.

You don’t even need to provide a success_url for CreateView or UpdateView - they will use get_absolute_url() on the model object if available.

Si vous souhaitez utiliser un formulaire ModelForm personnalisé (par exemple pour ajouter de la validation en plus), définissez form_class dans la vue.

Note

Lorsqu’une classe de formulaire personnalisée est présente, il est toujours nécessaire de définir le modèle, même si la classe form_class est une classe ModelForm.

First we need to add get_absolute_url() to our Author class:

models.py
from django.db import models
from django.urls import reverse


class Author(models.Model):
    name = models.CharField(max_length=200)

    def get_absolute_url(self):
        return reverse("author-detail", kwargs={"pk": self.pk})

Puis nous pouvons utiliser CreateView et compagnie pour faire le travail. Remarquez que nous avons ici juste à configurer les vues génériques fondées sur les classes ; nous n’avons pas à écrire nous-même de logique de vue :

views.py
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from myapp.models import Author


class AuthorCreateView(CreateView):
    model = Author
    fields = ["name"]


class AuthorUpdateView(UpdateView):
    model = Author
    fields = ["name"]


class AuthorDeleteView(DeleteView):
    model = Author
    success_url = reverse_lazy("author-list")

Note

Nous devons utiliser reverse_lazy() au lieu de reverse() car la configuration d’URL n’est pas encore chargée au moment où le fichier est importé.

L’attribut fields fonctionne de la même manière que l’attribut fields de la classe interne Meta de ModelForm. Sauf dans les cas où la classe de formulaire est définie d’une autre manière, cet attribut est obligatoire et la vue génère une exception ImproperlyConfigured s’il est absent.

Si vous définissez à la fois les attributs fields et form_class, une exception ImproperlyConfigured est générée.

Finalement, nous connectons ces nouvelles vues dans la configuration d’URL :

urls.py
from django.urls import path
from myapp.views import AuthorCreateView, AuthorDeleteView, AuthorUpdateView

urlpatterns = [
    # ...
    path("author/add/", AuthorCreateView.as_view(), name="author-add"),
    path("author/<int:pk>/", AuthorUpdateView.as_view(), name="author-update"),
    path("author/<int:pk>/delete/", AuthorDeleteView.as_view(), name="author-delete"),
]

Note

Ces vues héritent de SingleObjectTemplateResponseMixin qui utilise template_name_suffix pour construire template_name en fonction du modèle.

Dans cet exemple :

Si vous aimeriez avoir des gabarits séparés pour CreateView et UpdateView, vous pouvez définir template_name ou template_name_suffix dans votre classe de vue.

Modèles et request.user

Pour garder trace de l’utilisateur ayant créé un objet en utilisant CreateView, vous pouvez utiliser un formulaire ModelForm personnalisé. Premièrement, ajoutez la relation de clé étrangère dans le modèle :

models.py
from django.contrib.auth.models import User
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=200)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)

    # ...

In the view, ensure that you don’t include created_by in the list of fields to edit, and override form_valid() to add the user:

views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from myapp.models import Author


class AuthorCreateView(LoginRequiredMixin, CreateView):
    model = Author
    fields = ["name"]

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)

LoginRequiredMixin prevents users who aren’t logged in from accessing the form. If you omit that, you’ll need to handle unauthorized users in form_valid().

Exemple de négociation de contenu

Voici un exemple montrant une possible implémentation d’un formulaire fonctionnant aussi bien pour un flux basé sur une API que pour des envois de formulaire POST « normaux » :

from django.http import JsonResponse
from django.views.generic.edit import CreateView
from myapp.models import Author


class JsonableResponseMixin:
    """
    Mixin to add JSON support to a form.
    Must be used with an object-based FormView (e.g. CreateView)
    """

    def form_invalid(self, form):
        response = super().form_invalid(form)
        if self.request.accepts("text/html"):
            return response
        else:
            return JsonResponse(form.errors, status=400)

    def form_valid(self, form):
        # We make sure to call the parent's form_valid() method because
        # it might do some processing (in the case of CreateView, it will
        # call form.save() for example).
        response = super().form_valid(form)
        if self.request.accepts("text/html"):
            return response
        else:
            data = {
                "pk": self.object.pk,
            }
            return JsonResponse(data)


class AuthorCreateView(JsonableResponseMixin, CreateView):
    model = Author
    fields = ["name"]

L’exemple ci-dessus part du principe que si le client accepte text/html, il s’agit de sa préférence. Ceci n’est cependant pas toujours vrai. Lors d’une requête pour un fichier .css, bien des navigateurs enverront l’en-tête Accept: text/css,*/*;q=0.1, indiquant qu’ils préfèrent CSS, mais que toute autre valeur conviendra aussi. Cela signifie que request.accepts("text/html") vaudra True.

Pour déterminer le format correct, en prenant en compte la préférence du client, utilisez django.http.HttpRequest.get_preferred_type():

class JsonableResponseMixin:
    """
    Mixin to add JSON support to a form.
    Must be used with an object-based FormView (e.g. CreateView).
    """

    accepted_media_types = ["text/html", "application/json"]

    def dispatch(self, request, *args, **kwargs):
        if request.get_preferred_type(self.accepted_media_types) is None:
            # No format in common.
            return HttpResponse(
                status_code=406, headers={"Accept": ",".join(self.accepted_media_types)}
            )

        return super().dispatch(request, *args, **kwargs)

    def form_invalid(self, form):
        response = super().form_invalid(form)
        accepted_type = self.request.get_preferred_type(self.accepted_media_types)
        if accepted_type == "text/html":
            return response
        elif accepted_type == "application/json":
            return JsonResponse(form.errors, status=400)

    def form_valid(self, form):
        # We make sure to call the parent's form_valid() method because
        # it might do some processing (in the case of CreateView, it will
        # call form.save() for example).
        response = super().form_valid(form)
        accepted_type = self.request.get_preferred_type(self.accepted_media_types)
        if accepted_type == "text/html":
            return response
        elif accepted_type == "application/json":
            data = {
                "pk": self.object.pk,
            }
            return JsonResponse(data)
Changed in Django 5.2:

La méthode HttpRequest.get_preferred_type() a été ajoutée.