Skip to content

Exercise: Status Choices

You'll:

  • add an adoption status to Animal, with its choices and a default
  • generate and apply the migration
  • run the tests, and create an animal in the shell

What's provided in your branch:

  • the project and its two apps, with the settings, the Postgres connection, and the custom user model
  • the Animal model, with its fields, species choices, and string representation
  • age_label(), which labels an animal Puppy, Kitten, or Bunny under a year old, and Adult after that
  • compose.yaml, which runs Postgres, and config/test_settings.py, the settings the tests run with
  • tests for the Animal model's status

Before You Start

Quit the shell with exit() if it's open, and 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-ch02 --no-track origin/ch02
uv sync

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

Files that changed in this chapter:
  • pyproject.toml (modified, adds Django and psycopg[binary], and points pytest at the test settings)
  • manage.py and config/ (new, the project startproject generated; settings.py lists our apps, connects to Postgres, and sets AUTH_USER_MODEL)
  • config/test_settings.py (new, the settings the tests run with)
  • compose.yaml (new, runs Postgres in a container)
  • accounts/ (new, the custom user model and its migration)
  • animals/ (new, the Animal model and age_label(); you'll add its status)
  • tests/conftest.py (new, a fixture that creates animals)
  • tests/__init__.py and tests/test_models.py (new)
  • AGENTS.md (modified, adds the Docker, migration, and test commands, rules about migrations and settings, and a rule for accounts and tests/)
  • README.md (modified, describes the project and the database)
Didn't clone the project repo yet?

Clone the starter repo and add the solutions remote, as in Setting Up the Project, then run the commands above. Step 3 starts the database and creates the tables.

Troubleshooting Tips

Connection refused

The Postgres container isn't running, which happens after Docker or your computer restarts. Start it:

shell
docker compose up -d --wait

Step 1: Add the Status Choices

We need to track whether each animal is available, has an adoption pending, or has been adopted. Open animals/models.py. Below Species, the Status class has its first choice and a TODO for the other two:

animals/models.py
    class Species(models.TextChoices):
        DOG = "dog", "Dog"
        CAT = "cat", "Cat"
        RABBIT = "rabbit", "Rabbit"

    class Status(models.TextChoices):
        AVAILABLE = "available", "Available"
        # TODO: Add PENDING, labeled "Adoption pending", and ADOPTED, labeled "Adopted".

Replace the TODO with the two choices.

Step 2: Add the status Field

A new animal should start out as available. Further down, the TODO for the field sits between description and intake_date:

animals/models.py
    name = models.CharField(max_length=100)
    species = models.CharField(max_length=10, choices=Species)
    breed = models.CharField(max_length=100, blank=True)
    birth_date = models.DateField()
    description = models.TextField(blank=True)
    # TODO: Add status, a CharField like species with the Status choices and a
    # default of available (Status.AVAILABLE).
    intake_date = models.DateField(default=date.today)

Replace the TODO with the field.

Step 3: Generate and Apply the Migration

Our model has a new field, so we need a migration. Start the database container if it isn't running, then generate the migration and apply it:

shell
docker compose up -d --wait
uv run manage.py makemigrations
uv run manage.py migrate

If your project had no migrations for the animals app, makemigrations writes one that creates the whole model:

Text
Migrations for 'animals':
  animals/migrations/0001_initial.py
    + Create model Animal

If your project already had the migration that creates Animal, it writes 0002_animal_status.py instead, with + Add field status to animal. When migrate adds the column to a table that already has rows, it fills each row with the default.

Step 4: Run the Tests

The project comes with tests. Writing tests isn't part of this course: they're there so you can confirm your code works. Run them:

shell
uv run pytest

All tests should pass.

How the tests run

The tests run with config/test_settings.py. It imports everything from config/settings.py and replaces two settings:

  • DATABASES becomes an in-memory SQLite database.
  • Django's password hasher, slow on purpose, becomes a fast one that's only safe in tests.

pyproject.toml tells pytest to use these settings, so the tests run with the container stopped.

pytest-django builds the test database by running the migrations in animals/migrations/, so the tests need Step 3's migration file.

Step 5: Create an Animal in the Shell

Start the shell:

shell
uv run manage.py shell

Create a kitten, and check its age label and its status:

Python REPL
>>> from datetime import date, timedelta
>>> peanut = Animal.objects.create(name="Peanut", species="cat", birth_date=date.today() - timedelta(days=60))
>>> peanut.age_label()
'Kitten'
>>> peanut.status == Animal.Status.AVAILABLE
True

Peanut was born 60 days ago, so age_label() returns the young label for a cat, and status has its default.

Success

If the tests pass and the shell shows 'Kitten' and True, the status column exists in Postgres, and a new animal starts out as available.

Compare with the Solution

shell
git diff solutions/ch02 -- animals/models.py

No output means your file matches the solution. Your migration is in animals/migrations/, which Git ignores, so the diff doesn't show it.

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