==================================== 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 ContactView(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 AuthorCreate(CreateView): model = Author fields = ['name'] class AuthorUpdate(UpdateView): model = Author fields = ['name'] class AuthorDelete(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 AuthorCreate, AuthorDelete, AuthorUpdate urlpatterns = [ # ... path('author/add/', AuthorCreate.as_view(), name='author-add'), path('author//', AuthorUpdate.as_view(), name='author-update'), path('author//delete/', AuthorDelete.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 AuthorCreate(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 AuthorCreate(JsonableResponseMixin, CreateView): model = Author fields = ['name']