Formulärhantering med klassbaserade vyer

Formulärhantering har i allmänhet 3 vägar:

  • Initial GET (blankett eller förifyllt formulär)

  • POST med ogiltiga data (vanligtvis visas formuläret på nytt med fel)

  • POST med giltiga data (bearbeta data och typiskt omdirigera)

Att implementera detta själv resulterar ofta i en hel del upprepad boilerplate-kod (se Använda ett formulär i en vy). För att hjälpa till att undvika detta tillhandahåller Django en samling generiska klassbaserade vyer för formulärbehandling.

Grundläggande former

Givet ett kontaktformulär:

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

Vyn kan byggas upp med hjälp av en FormView:

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

Anteckningar:

Modellformer

Generiska vyer lyser verkligen när man arbetar med modeller. Dessa generiska vyer kommer automatiskt att skapa en ModelForm, så länge de kan räkna ut vilken modellklass som ska användas:

  • Om attributet model anges kommer den modellklassen att användas.

  • Om get_object() returnerar ett objekt, kommer klassen för det objektet att användas.

  • Om en queryset anges, kommer modellen för den queryset att användas.

Modellformulärsvyer tillhandahåller en form_valid()-implementering som sparar modellen automatiskt. Du kan åsidosätta detta om du har några speciella krav; se nedan för exempel.

Du behöver inte ens tillhandahålla en uccess_url för CreateView eller UpdateView - de kommer att använda get_absolute_url() på modellobjektet om det finns tillgängligt.

Om du vill använda en anpassad ModelForm (t.ex. för att lägga till extra validering), ange form_class på din vy.

Observera

När du anger en anpassad formulärklass måste du fortfarande ange modellen, även om form_class kan vara en ModelForm.

Först måste vi lägga till get_absolute_url() till vår Author klass:

modeller.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})

Sedan kan vi använda CreateView och dess vänner för att göra det faktiska arbetet. Lägg märke till hur vi bara konfigurerar de generiska klassbaserade vyerna här; vi behöver inte skriva någon logik själva:

Vyer.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")

Observera

Vi måste använda reverse_lazy() istället för reverse(), eftersom webbadresserna inte laddas när filen importeras.

Attributet fields fungerar på samma sätt som attributet fields på den inre Meta klassen på ModelForm. Om du inte definierar formulärklassen på ett annat sätt krävs attributet och vyn kommer att ge upphov till ett ImproperlyConfigured undantag om det inte är det.

Om du anger både attributen fields och form_class, kommer ett ImproperlyConfigured undantag att uppstå.

Slutligen kopplar vi in dessa nya vyer i URLconf:

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"),
]

Observera

Dessa vyer ärver SingleObjectTemplateResponseMixin som använder template_name_suffix för att konstruera template_name baserat på modellen.

I detta exempel:

Om du vill ha separata mallar för CreateView och UpdateView kan du ange antingen template_name eller template_name_suffix på din vyklass.

Modeller och request.user

För att spåra den användare som skapade ett objekt med hjälp av en CreateView kan du använda en anpassad ModelForm för att göra detta. Lägg först till den främmande nyckelrelationen i modellen:

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

    # ...

I vyn ska du se till att du inte inkluderar created_by i listan över fält som ska redigeras och åsidosätta form_valid() för att lägga till användaren:

Vyer.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 hindrar användare som inte är inloggade från att komma åt formuläret. Om du utelämnar det måste du hantera obehöriga användare i form_valid().

Exempel på förhandling om innehåll

Här är ett exempel som visar hur du kan gå tillväga för att implementera ett formulär som fungerar med ett API-baserat arbetsflöde såväl som ”vanliga” formulärposts:

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

I exemplet ovan antas att om klienten har stöd för text/html, så kommer de att föredra det. Detta kanske dock inte alltid är sant. Vid begäran om en .css-fil kommer många webbläsare att skicka rubriken Accept: text/css,*/*;q=0.1, vilket indikerar att de föredrar CSS, men allt annat går bra. Detta innebär att request.accepts("text/html") kommer att vara True.

För att bestämma rätt format, med hänsyn till klientens preferenser, använd 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:

Metoden HttpRequest.get_preferred_type() lades till.