Skip to content

Exercise: Saving an Application

You'll:

  • set the application's animal in form_valid(), so it can be saved
  • thank the visitor with a success message that names the animal
  • redirect to the animal's page from get_success_url()
  • run the tests
  • apply for an animal in the browser, and find the application in the admin and the log message in the terminal

What's provided in your branch:

  • AdoptView, with dispatch(), get_context_data(), and form_class, as we learned in this chapter. There are TODOs in form_valid() and get_success_url().
  • the application form, its page, its URL pattern, and the link from an animal's page
  • the list page's search: AnimalSearchForm, its queries, and the search in AnimalListView
  • tests for sending the adoption form

Before You Start

Stop the development server with Ctrl+C if it's running. Then set up the starting state for this exercise:

shell
git stash -u
git fetch --all
git checkout -b my-ch06 --no-track origin/ch06
uv sync

If Git prints No local changes to save, there was nothing to set aside.

Files that changed in this chapter:
  • animals/validators.py (new, the phone number rule)
  • animals/models.py (modified, puts the phone number rule on phone)
  • animals/forms.py (new, AdoptionApplicationForm and AnimalSearchForm)
  • animals/queries.py (modified, adds available_animals(), named(), and of_species())
  • animals/views.py (modified, adds AdoptView and the list page's search; you'll finish AdoptView)
  • animals/urls.py (modified, adds the form's pattern)
  • animals/templates/animals/adopt.html (new, the application form's page)
  • animals/templates/animals/animal_detail.html (modified, links an available animal's page to the form)
  • animals/templates/animals/animal_list.html (modified, the search form above the list)
  • config/settings.py (modified, adds LOGGING)
  • tests/test_forms.py (new)
  • tests/conftest.py (modified, only the fixture these tests use)
  • tests/test_pages.py (removed)
  • AGENTS.md (modified, adds CreateView to the rule for the pages' views, and says where to put a rule that every entry point must enforce)
  • README.md (modified, describes the application form, the search form, and the phone number rule)

Step 1: Migrate the Phone Number Rule

The phone field's validator needs a migration. Generate and apply it:

shell
uv run manage.py makemigrations
uv run manage.py migrate

makemigrations writes a migration with ~ Alter field phone on adoptionapplication, or prints No changes detected if your migrations already have it.

Step 2: Save the Application

Open animals/views.py. Below get_context_data(), AdoptView has a form_valid() that saves the application and logs it:

animals/views.py
    def form_valid(self, form):
        # TODO: The form has no animal field. Set the application's animal on
        # form.instance before the save below.

        # Saves the application as self.object and redirects to get_success_url().
        response = super().form_valid(form)
        # Ids only: personal info about who applied shouldn't be logged.
        logger.info(
            "Adoption application %s saved for animal %s",
            self.object.pk,
            self.animal.pk,
        )
        # TODO: Thank the visitor with a success message that names the animal.
        messages.success(self.request, "")
        return response

self.animal is the animal dispatch() looked up.

Step 3: Redirect to the Animal's Page

Below form_valid(), get_success_url() returns the URL the redirect goes to:

animals/views.py
    def get_success_url(self):
        # TODO: Return the URL of the animal's page, with the animal's pk.
        # reverse() takes captured values by name or in order:
        # kwargs={"pk": 2} or args=[2].
        return reverse("animals:detail")

The detail page's URL pattern in animals/urls.py captures pk. Pass the captured values to reverse() as a dictionary in its kwargs argument.

Step 4: Run the Tests

shell
uv run pytest

All tests should pass.

Step 5: Apply in the Browser

Load the sample data, then start the server:

shell
uv run manage.py seed --reset --accounts
uv run manage.py runserver

Open http://127.0.0.1:8000/, click Maple, and click Apply to adopt Maple.

  1. Click Send application with the form empty. The browser stops it, because Django rendered the name and email inputs with the required attribute.
  2. Enter Maria as the name, maria@example as the email, and 555-1234 as the phone number. Send the form. The browser's email check accepts that address, but Django's rejects it. The form comes back with the name still filled in, and errors at the email and the phone number.
  3. Fix the email and the phone number, and send it again. The redirect takes you to Maple's page, which shows your message. Refresh, and the message is gone.
  4. Log in to the admin at http://127.0.0.1:8000/admin/ as admin, with the password rescue-admin (or yours, if you created admin yourself), and open Adoption applications. Maria's application for Maple is at the top of the list, with the status Submitted.

In the terminal running the server, your log message is just above the POST's request line, something like:

Text
Adoption application 14 saved for animal 2
[19/Oct/2026 10:52:29] "POST /animals/2/adopt/ HTTP/1.1" 302 0

Success

Your AdoptView is finished if the tests pass, the form saves an application for its animal, the animal's page thanks the visitor once, and the server logs the save.

Troubleshooting Tips

IntegrityError when you send the form

It says null value in column "animal_id" of relation "animals_adoptionapplication" violates not-null constraint. Django tried to save the application without its animal. Set form.instance.animal above super().form_valid(form), which is where the save happens.

NoReverseMatch after you send the form

It says Reverse for 'detail' with no arguments not found. The application was saved, but reverse() can't build the detail page's URL without the pk its pattern captures. Pass the animal's pk in kwargs.

Maple's page shows no message

The message's text is still the empty string. Give messages.success() a message that names the animal, such as an f-string with self.animal.name.

test_valid_post_thanks_the_visitor_once fails with assert 'Clover' in ...

The test applies for Clover and expects the message to name the animal. Include self.animal.name in an f-string.

Compare with the Solution

shell
git diff solutions/ch06 -- animals/views.py

See the solution on GitHub: animals/views.py.