123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- ====================================
- 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<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/<int:pk>/", AuthorUpdateView.as_view(), name="author-update"),
- path("author/<int:pk>/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.
|