mirror of
				https://github.com/django/django.git
				synced 2025-10-29 00:26:07 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			329 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			329 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| ====================================
 | |
| 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.
 |