==================================== Form handling with class-based views ==================================== Form processing generally has 3 paths: * Initial GET (blank or prepopulated form) * POST with invalid data (typically redisplay form with errors) * POST with valid data (process the data and typically redirect) Implementing this yourself often results in a lot of repeated boilerplate code (see :ref:`Using a form in a view`). To help avoid this, Django provides a collection of generic class-based views for form processing. Basic forms =========== Given a contact form: .. code-block:: python :caption: ``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 The view can be constructed using a ``FormView``: .. code-block:: python :caption: ``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: * FormView inherits :class:`~django.views.generic.base.TemplateResponseMixin` so :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` can be used here. * The default implementation for :meth:`~django.views.generic.edit.FormMixin.form_valid` simply redirects to the :attr:`~django.views.generic.edit.FormMixin.success_url`. Model forms =========== Generic views really shine when working with models. These generic views will automatically create a :class:`~django.forms.ModelForm`, so long as they can work out which model class to use: * If the :attr:`~django.views.generic.edit.ModelFormMixin.model` attribute is given, that model class will be used. * If :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` returns an object, the class of that object will be used. * If a :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` is given, the model for that queryset will be used. Model form views provide a :meth:`~django.views.generic.edit.ModelFormMixin.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 :class:`~django.views.generic.edit.CreateView` or :class:`~django.views.generic.edit.UpdateView` - they will use :meth:`~django.db.models.Model.get_absolute_url()` on the model object if available. If you want to use a custom :class:`~django.forms.ModelForm` (for instance to add extra validation), set :attr:`~django.views.generic.edit.FormMixin.form_class` on your view. .. note:: When specifying a custom form class, you must still specify the model, even though the :attr:`~django.views.generic.edit.FormMixin.form_class` may be a :class:`~django.forms.ModelForm`. First we need to add :meth:`~django.db.models.Model.get_absolute_url()` to our ``Author`` class: .. code-block:: python :caption: ``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}) Then we can use :class:`CreateView` and friends to do the actual work. Notice how we're just configuring the generic class-based views here; we don't have to write any logic ourselves: .. code-block:: python :caption: ``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:: We have to use :func:`~django.urls.reverse_lazy` instead of ``reverse()``, as the urls are not loaded when the file is imported. The ``fields`` attribute works the same way as the ``fields`` attribute on the inner ``Meta`` class on :class:`~django.forms.ModelForm`. Unless you define the form class in another way, the attribute is required and the view will raise an :exc:`~django.core.exceptions.ImproperlyConfigured` exception if it's not. If you specify both the :attr:`~django.views.generic.edit.ModelFormMixin.fields` and :attr:`~django.views.generic.edit.FormMixin.form_class` attributes, an :exc:`~django.core.exceptions.ImproperlyConfigured` exception will be raised. Finally, we hook these new views into the URLconf: .. code-block:: python :caption: ``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//", AuthorUpdateView.as_view(), name="author-update"), path("author//delete/", AuthorDeleteView.as_view(), name="author-delete"), ] .. note:: These views inherit :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` which uses :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` to construct the :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` based on the model. In this example: * :class:`CreateView` and :class:`UpdateView` use ``myapp/author_form.html`` * :class:`DeleteView` uses ``myapp/author_confirm_delete.html`` If you wish to have separate templates for :class:`CreateView` and :class:`UpdateView`, you can set either :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` or :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` on your view class. Models and ``request.user`` =========================== To track the user that created an object using a :class:`CreateView`, you can use a custom :class:`~django.forms.ModelForm` to do this. First, add the foreign key relation to the model: .. code-block:: python :caption: ``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 :meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the user: .. code-block:: python :caption: ``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) :class:`~django.contrib.auth.mixins.LoginRequiredMixin` prevents users who aren't logged in from accessing the form. If you omit that, you'll need to handle unauthorized users in :meth:`~.ModelFormMixin.form_valid()`. .. _content-negotiation-example: Content negotiation example =========================== Here is an example showing how you might go about implementing a form that works with an API-based workflow as well as 'normal' form POSTs:: 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"] The above example assumes that if the client supports ``text/html``, that they would prefer it. However, this may not always be true. When requesting a ``.css`` file, many browsers will send the header ``Accept: text/css,*/*;q=0.1``, indicating that they would prefer CSS, but anything else is fine. This means ``request.accepts("text/html") will be ``True``. To determine the correct format, taking into consideration the client's preference, use :func:`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 = 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 = 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) .. versionchanged:: 5.2 The :meth:`.HttpRequest.get_preferred_type` method was added.