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 callsis_valid().- If the data is valid,
form_valid()saves the form, keeps the new row asself.object, and redirects to the URLget_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 data is valid,
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:
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,CreateViewwould look foranimals/adoptionapplication_form.html.dispatch(): Called for every request, and callsget()orpost()according to the request's method. The view needs the animal for both, so it looks the animal up here and keeps it asself.animal.get_context_data(): Builds the template's context. Ours calls the parent's version, to keep theformthatCreateViewputs 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:
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:
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:
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:
{% 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 asself.object, and returns the redirect. Our version first sets the animal onform.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 withreverse()fromdjango.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.