Skip to content

Handling a Form with CreateView

We need a page at /animals/<id>/adopt/ that shows the form, and saves an application when the form is submitted.

A view that handles a form responds to two HTTP methods, GET and POST. The steps are the same in any view that creates a row from a model form, and CreateView is the generic view for them:

  • On GET: It renders its template with an empty form, as form.
  • On POST: It binds the form to the submitted data, in request.POST, and calls is_valid().
    • If the data is valid, form_valid() saves the form, keeps the new row as self.object, and redirects to the URL get_success_url() returns.
    • If it isn't, form_invalid() renders the template again with the bound form, which holds the submitted values and the errors.

If the response to a POST were a page, refreshing it would send the POST again and save a second application. Redirecting after a successful POST is known as the Post/Redirect/Get pattern.

AdoptView

The starting branch has AdoptView at the bottom of animals/views.py:

animals/views.py
class AdoptView(CreateView):
    model = AdoptionApplication
    template_name = "animals/adopt.html"

    def dispatch(self, request, *args, **kwargs):
        self.animal = get_object_or_404(
            queries.available_animals(), pk=kwargs["animal_id"]
        )
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["animal"] = self.animal
        return context
  • model: The model the view creates a row of.
  • template_name: The template it renders. Without it, CreateView would look for animals/adoptionapplication_form.html.
  • dispatch(): Called for every request, and calls get() or post() according to the request's method. The view needs the animal for both, so it looks the animal up here and keeps it as self.animal.
  • get_context_data(): Builds the template's context. Ours calls the parent's version, to keep the form that CreateView puts there, and adds the animal for the page's heading.

kwargs holds the values captured from the URL. The view's URL pattern, in animals/urls.py, captures the animal's id as animal_id:

animals/urls.py
    path("animals/<int:animal_id>/adopt/", views.AdoptView.as_view(), name="adopt"),

get_object_or_404() takes a QuerySet as well as a model. queries.available_animals() in animals/queries.py returns only the animals that take applications. So a pending or adopted animal gets a 404 before get() or post() runs. An animal's page links to the form only when the animal is available.

Connecting the Form

AdoptView doesn't have a form yet. Import ours in animals/views.py:

animals/views.py
import logging

from django.shortcuts import get_object_or_404
from django.views.generic import CreateView, DetailView, ListView

from . import queries
from .forms import AdoptionApplicationForm
from .models import AdoptionApplication, Animal

Then set form_class in AdoptView to our form. The view shows it on GET and validates it on POST:

animals/views.py
class AdoptView(CreateView):
    model = AdoptionApplication
    form_class = AdoptionApplicationForm
    template_name = "animals/adopt.html"

If the server isn't running, start it with uv run manage.py runserver. Open Maple from the list, and click Apply to adopt Maple. The page shows the form's fields, with our labels.

Warning

Sending the form now raises an IntegrityError: the form has no animal field, and AdoptView doesn't set the application's animal yet.

The Template

The template that renders the form is provided in this chapter's starting branch:

animals/templates/animals/adopt.html
{% extends "base.html" %}

{% block content %}
  <h1>Apply to adopt {{ animal.name }}</h1>
  <form method="post">
    {% csrf_token %}
    {{ form }}
    <button type="submit">Send application</button>
  </form>
{% endblock %}

{{ form }} renders every field in the form: its label, its input, any help text, and any errors.

A form sent with POST needs {% csrf_token %} inside it. Without the token, the submission is refused with a 403 Forbidden response.

Saving and Redirecting

On a valid POST, CreateView calls two more methods, and AdoptView needs its own version of each:

  • form_valid(): Saves the form, keeps the new row as self.object, and returns the redirect. Our version first sets the animal on form.instance, the application the form will save. Then it calls the parent's version.
  • get_success_url(): Returns the URL for the redirect. Ours is the animal's page. We build its URL with reverse() from django.urls. Like {% url %} in a template, it returns the URL for a pattern's name.

We'll write both in the exercise.

Nothing stops the same person from applying twice for the same animal: this version doesn't check for a duplicate application.