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
Animalmodel, 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 thatcompose.yaml, which runs Postgres, andconfig/test_settings.py, the settings the tests run with- tests for the
Animalmodel'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:
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 andpsycopg[binary], and points pytest at the test settings)manage.pyandconfig/(new, the projectstartprojectgenerated;settings.pylists our apps, connects to Postgres, and setsAUTH_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, theAnimalmodel andage_label(); you'll add its status)tests/conftest.py(new, a fixture that creates animals)tests/__init__.pyandtests/test_models.py(new)AGENTS.md(modified, adds the Docker, migration, and test commands, rules about migrations and settings, and a rule foraccountsandtests/)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:
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:
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:
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:
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:
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:
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:
DATABASESbecomes 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:
uv run manage.py shell
Create a kitten, and check its age label and its status:
>>> 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
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.