Skip to content

Capturing the Animal's ID from the URL

A page for a single animal needs to know which animal was asked for. We can put the animal's id in the URL: /animals/7/.

The Detail Pattern

In a URL pattern, a path converter goes between angle brackets. It matches part of the path, converts it to a Python type, and passes it to the view as a keyword argument. Add the pattern for an animal's page to animals/urls.py:

Python
urlpatterns = [
    path("", views.animal_list, name="list"),
    path("animals/<int:pk>/", views.animal_detail, name="detail"),
]

<int:pk> matches a run of digits, converts it to an int, and passes it to the view as pk. A request for /animals/seven/ doesn't match the pattern at all.

Until the view exists, the running server stops with an AttributeError. It starts again once we save the view.

As in clean(), pk is the row's primary key. Every model has a pk attribute, whatever its primary key field is called.

Does the order of URL patterns matter?

Yes. Django tries the patterns in order and uses the first one that matches.

The View

Add the view to animals/views.py. It needs get_object_or_404 from django.shortcuts, and the model.

At the top of the file:

Python
from django.shortcuts import get_object_or_404, render

from . import queries
from .models import Animal

Below animal_list():

Python
def animal_detail(request, pk):
    animal = get_object_or_404(Animal, pk=pk)
    return render(request, "animals/animal_detail.html", {"animal": animal})

The view takes pk after the request. get_object_or_404() calls Animal.objects.get() with the arguments we give it and returns the row. If no row matches, it raises Http404, and the visitor gets a 404 page.

Why not call get() directly?

Animal.objects.get(pk=999) raises Animal.DoesNotExist. Nothing in the view catches it, so the visitor would get a 500 error for a page that doesn't exist. The model layer knows nothing about HTTP, and get_object_or_404() is the shortcut that connects the two.

The Template

Create animals/templates/animals/animal_detail.html:

HTML
{% extends "base.html" %}

{% block title %}{{ animal.name }} · RescueScout{% endblock %}

{% block content %}
  <h1>{{ animal.name }}</h1>
  <p>Arrived: {{ animal.intake_date|date:"j F Y" }}</p>
{% endblock %}

This page fills in the title block too, so the browser's tab shows the animal's name.

A filter changes a value before the template writes it. The Arrived: line uses the date filter to format the date.

A filter follows a |, and an argument follows a colon: {{ value|filter:"argument" }}. Here, "j F Y" writes a date such as 17 October 2026.

We'll add the rest of the animal's details in the exercise.

Linking Each Animal

In animal_list.html, link each animal's name to its page:

animals/templates/animals/animal_list.html
        <h2><a href="{% url 'animals:detail' animal.pk %}">{{ animal.name }}</a></h2>

The url tag takes the pattern's name and the values for its converters, and writes the path: /animals/7/ for the animal whose id is 7.

Click an animal's name to open its page. Then open http://127.0.0.1:8000/animals/999/. With DEBUG on, Django's 404 page says "No Animal matches the given query." and names the view that raised it.

Showing the Breed on the List

A breed tells a visitor more than a species, but not every animal has one. The default filter writes its argument when the value is empty. That argument can be a variable or a quoted string. In animal_list.html, change the line under the name:

animals/templates/animals/animal_list.html
        <p>{{ animal.breed|default:animal.get_species_display }}, {{ animal.age_label }}, {{ animal.get_status_display }}</p>

Reload the list. Clover's line reads Lionhead mix, Bunny, Available, and Truffle, whose breed is empty, reads Cat, Adult, Available.