Exercise: A Seed Command
You'll:
- migrate the full
AdoptionApplicationmodel - finish a
seedcommand that creates sample animals and adoption applications - run the tests and the command
- write queries for the new rows in the shell
What's provided in your branch:
- the full
AdoptionApplicationmodel, with its foreign key toAnimal, contact fields, status, and submitted time - a
seedcommand that creates Peanut and Maria's application for her, withTODOs for the rest - tests for the
seedcommand
Before You Start
Quit the shell with exit() if it's open. Then set up the starting state for this exercise:
git stash -u
git fetch --all
git checkout -b my-ch03 --no-track origin/ch03
uv sync
If Git prints No local changes to save, there was nothing to set aside.
Files that changed in this chapter:
animals/models.py(modified, addsAdoptionApplication, with its contact fields, status, and submitted time)animals/management/(new, theseedcommand; you'll fill in itsTODOs)tests/test_seed.py(new)tests/conftest.pyandtests/test_models.py(removed)AGENTS.md(modified, adds theseedcommand and a rule for imports)README.md(modified, describes the second model and the command)
Step 1: Check Your Migrations
The branch has the full AdoptionApplication model, with contact fields, a status, and the time it was submitted. Git ignores the animals migrations, so yours are still in animals/migrations/. Before we migrate the full model, list them and whether each one is applied:
uv run manage.py showmigrations animals
If the last migration is one of Animal's, such as 0001_initial or 0002_animal_status, there's nothing to undo, and you can go on to Step 2.
If the last migration creates AdoptionApplication, it's for the model with two fields, and the full model needs a new one. Here, it's 0003_adoptionapplication:
animals
[X] 0001_initial
[X] 0002_animal_status
[X] 0003_adoptionapplication
Undo it by migrating the app back to the migration before it, then deleting its file. See "How do I undo a migration?" on Django Migrations. With the numbers above:
uv run manage.py migrate animals 0002
rm animals/migrations/0003_adoptionapplication.py
If yours is 0002_adoptionapplication, the migration before it is 0001_initial:
uv run manage.py migrate animals 0001
rm animals/migrations/0002_adoptionapplication.py
Unapplying the migration drops the applications table. The animals stay.
Step 2: Migrate the Full Model
Generate and apply the migration:
uv run manage.py makemigrations
uv run manage.py migrate
Migrations for 'animals':
animals/migrations/0003_adoptionapplication.py
+ Create model AdoptionApplication
Your migration's number and name may differ.
What the full model adds
class AdoptionApplication(models.Model):
"""One visitor's request to adopt one animal."""
class Status(models.TextChoices):
SUBMITTED = "submitted", "Submitted"
APPROVED = "approved", "Approved"
DECLINED = "declined", "Declined"
animal = models.ForeignKey(
Animal, on_delete=models.CASCADE, related_name="applications"
)
applicant_name = models.CharField(max_length=100)
email = models.EmailField()
phone = models.CharField(
max_length=20,
blank=True,
help_text="A US number, like 555-010-2000",
)
message = models.TextField(blank=True)
status = models.CharField(max_length=10, choices=Status, default=Status.SUBMITTED)
# Set once, when the row is first saved.
created_at = models.DateTimeField("submitted time", auto_now_add=True)
def __str__(self):
return f"{self.applicant_name} for {self.animal}"
email: AnEmailFieldis aCharFieldwhose validation checks that the value looks like an email address.phoneandmessage: Optional, withblank=True.help_textis shown next to the field in forms and in the admin.status: An application's ownStatuschoices, starting as submitted.created_at:auto_now_add=Truesets the time when the row is first saved. The first argument,"submitted time", is the field'sverbose_name, the readable name that forms and the admin show.__str__: The applicant's name and the animal's, such as "Maria for Peanut".
Step 3: Fill in the Seed Command
We want a command that fills the database with three animals and two adoption applications, and that can run again to replace them.
manage.py runs our commands just like migrate. A management command is a module named after the command. It lives in an app's management/commands/ folder. Ours, animals/management/commands/seed.py, runs with uv run manage.py seed. We use a command instead of a plain script because manage.py loads the project's settings before any model is imported.
Open animals/management/commands/seed.py and fill in the TODOs to create Rocket, Smokey, and Aisha's application:
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from animals.models import AdoptionApplication, Animal
class Command(BaseCommand):
help = "Replace every animal with three animals and two adoption applications."
def handle(self, *args, **options):
# Deleting the animals also deletes their applications, so the command
# can run again.
Animal.objects.all().delete()
peanut = Animal.objects.create(
name="Peanut",
species=Animal.Species.CAT,
birth_date=date.today() - timedelta(days=60),
)
# TODO: Create Rocket, a dog born 3 years ago (3 * 365 days),
# with a breed of "Beagle mix".
# TODO: Create Smokey, a cat born 4 years ago (4 * 365 days),
# with a status of pending.
AdoptionApplication.objects.create(
animal=peanut, applicant_name="Maria", email="maria@example.com"
)
# TODO: Create Aisha's application for Peanut, with an email of
# aisha@example.com.
self.stdout.write("Created 3 animals and 2 adoption applications.")
Command, a subclass ofBaseCommand, is the class Django looks for.helpis whatuv run manage.py help seedprints.handle()runs when the command does.self.stdout.write()writes a line to the output.
Step 4: Run the Tests
uv run pytest
All tests should pass. The tests for the command check the rows it leaves in the database, and that it can run twice.
Step 5: Run the Command
uv run manage.py seed
Created 3 animals and 2 adoption applications.
Step 6: Query the New Rows
Start the shell:
uv run manage.py shell
Write a query for each of these:
- The animals that aren't available.
- Every animal, youngest first.
- The email addresses on Peanut's applications.
Solution
>>> Animal.objects.exclude(status="available")
<QuerySet [<Animal: Smokey>]>
>>> Animal.objects.order_by("-birth_date")
<QuerySet [<Animal: Peanut>, <Animal: Rocket>, <Animal: Smokey>]>
>>> peanut = Animal.objects.get(name="Peanut")
>>> peanut.applications.values_list("email", flat=True)
<QuerySet ['maria@example.com', 'aisha@example.com']>
Success
The full model is migrated and the seed command works if the tests pass, the command prints its line, and your queries find these rows.
Troubleshooting Tips
makemigrations asks for a default for created_at
It says It is impossible to add the field 'created_at' with 'auto_now_add=True' to adoptionapplication without providing a default. The migration for the model with two fields is still there. So Django sees the full model's fields as new columns for a table that already has rows. Choose 2 to quit, then undo that migration, as in Step 1.
ProgrammingError: column "email" of relation "animals_adoptionapplication" does not exist
The two-field model's migration file was deleted before the migration was unapplied. The database still has the table and a record of a migration with the same name as the new one. That's why migrate skipped the new migration. Migrate back to the previous migration, as in Step 1, then forward:
uv run manage.py migrate animals 0002
uv run manage.py migrate
If your applications migration is 0002_adoptionapplication, migrate back to 0001.
Compare with the Solution
git diff solutions/ch03 -- animals/management/
No output, or only the TODO comments you kept, means your command matches the solution.
See the solution on GitHub: animals/management/commands/seed.py.