Programming for the Web

Detail Pages and Identifiers


Learning Objectives

  • You can select a record by its stable identifier.
  • You can connect a path parameter to a lookup and detail page.
  • You can return a 404 response when a requested record is absent.

A detail page gives one record its own address. Continue the web-app project from Chapter 4, adding a lookup, a detail route, and links from the list.

Stable Identity

The address /tasks/T02 should select the record with identifier T02, even if the list is filtered or another record is inserted before it. An identifier is different from a record’s current list position.

In main.py, add the following function above the routes:

def find_task(records, task_id):
for record in records:
if record["id"] == task_id:
return record
return None

This function returns the complete matching record or None. It leaves the collection unchanged. Try it with T02, an unfamiliar identifier, and an empty list before adding the browser interface.

A Value in the Path

Braces in a route’s path name a path parameter. FastAPI passes the corresponding address segment to the function argument with that name. Add from fastapi.responses import PlainTextResponse to the imports, then add this detail route below show_tasks:

@app.get("/tasks/{task_id}")
def show_task(request: Request, task_id: str):
task = find_task(read_records(DATA_FILE), task_id)
if task is None:
return PlainTextResponse("Task not found.", status_code=404)
return templates.TemplateResponse(
request=request, name="task.html", context={"task": task}
)

For GET /tasks/T02, FastAPI supplies two different arguments: the request object under request, and the text "T02" under task_id. The str annotation identifies the path value as text. The name task_id must agree with {task_id} in the registered path. Your code then makes an ordinary Python call to find_task, passing the records and that identifier.

Follow the successful request using the supplied data:

ConnectionValue
Requested path/tasks/T02
Route parametertask_id contains "T02"
Lookup result{"id": "T02", "title": "Practise loops", "category": "practice"}
Context entry"task" refers to that complete dictionary
Template expression{{ task["title"] }} retrieves "Practise loops"
Returned HTML<h1>Practise loops</h1>

If lookup instead returns None, the route returns 404 before trying to render a missing record. The server is running in both cases; a missing record is an application response.

Create templates/task.html with this complete document. It displays fields from the supplied record:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ task["title"] }}</title>
</head>
<body>
<h1>{{ task["title"] }}</h1>
<p>Identifier: {{ task["id"] }}</p>
<p>Category: {{ task["category"] }}</p>
<a href="/tasks">All tasks</a>
</body>
</html>

Replace templates/tasks.html with the following complete document. Each list item now links to the detail route using the record’s identifier:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Study tasks</title>
</head>
<body>
<h1>Study tasks</h1>
{% if tasks %}
<ul id="task-list">
{% for task in tasks %}
<li>
<a href="/tasks/{{ task['id'] }}">{{ task["title"] }}</a>
({{ task["category"] }})
</li>
{% endfor %}
</ul>
{% else %}
<p>No tasks.</p>
{% endif %}
</body>
</html>

During list rendering, task['id'] supplies "T02", so the link’s href becomes /tasks/T02. The title supplies its visible text. Following this link sends a GET request whose path matches /tasks/{task_id}.

The following sequence diagram separates building the link from following it:

The template builds a detail link; following that link requests the record.

Building the link does not call show_task; the later detail request does.

Run the example and follow each title. Then request /tasks/unknown and compare its status with a working detail page. Reorder the JSON records, refresh the list, and follow the links again. Their order changes, but each identifier still selects its own record.

Task Detail Pages

0 / 40 points

Complete the marked code in main.py, templates/task.html, and templates/tasks.html.

The stages award points independently:

StageRequired behaviorPoints
1 Lookupfind_task(records, task_id) returns the complete record with that identifier, or None. Leave the input records and their order unchanged.15
2 RouteGET /tasks/{task_id} reads the current records and renders task.html with the matching record under task. Return 404 when no record matches.15
3 PagesEach list title links to its record’s detail page. The detail page displays the selected title in its main heading, plus the identifier and category.10

For example, /tasks/T02 displays Practise loops. Use ordinary Jinja insertion and quoted attributes so record text remains text.

Tracing a Detail Request

Use Chapter 2’s print technique to inspect task_id and the lookup result task inside show_task. If the requested identifier is unexpected, check the list link’s href in View page source.

A detail page selects one record by identity. The next chapter selects records by their contents.

Check Your Understanding

  1. The JSON records are reordered. Why should /tasks/T02 still display the same task?
  2. For GET /tasks/T02, what value does show_task receive as task_id, and how does it use that value to find the record?
  3. Why does show_task check whether the lookup returned None before rendering task.html? What response should it return when no record matches?
  4. Rendering the list creates a link to /tasks/T02. Does the detail route run while the link is created or when the visitor follows it?