← Teacher guide

Pages from data

Students build pages from a list of dictionaries with a template (Jinja), add a page for each player with an address part that changes, then add a wins column, group pages and a leaders page.

Web apps with Flask · Pages from Python · about 50 minutes · Open the student lesson

Classroom resources

Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.

Students will be able to

Timing

Predict and Run 10 min · Investigate 10 min · Modify 20 min · Make 10 min

Before class

Read the unit primer first. A template is HTML with gaps, in Flask's template language (Jinja): {{ value }} shows a value and {% for p in players %} ... {% endfor %} repeats a part. render_template_string(PAGE, players=players) fills it in; the template sees only what is passed by name (the bug task forgets to pass it, and the list comes out empty, with no error). In a route, <name> matches any word in that place of the address and is passed to the function. A route can return a page and a status: return "...", 404. On a computer templates usually live in templates/ files with render_template; the playground keeps them in strings so everything is in one file.

Running the lesson

  1. Predict: write the li line once on the board, then repeat it for each name.
  2. Run: try /player/Zoe and read the page; ask what 404 tells a browser.
  3. Investigate: compare the template with the page's source: which parts came from the list?
  4. Modify: test /group/Players and /group/Nobody.

The hard part (on purpose)

Passing data into a template by name. Ask what the template can see, and let them find the missing argument themselves.

When students get stuck

What you will seeA question to askThe fix
The table or list is empty.Is the list passed to render_template_string by name?render_template_string(PAGE, players=players).
jinja2.exceptions.TemplateSyntaxError.Does every {% for %} have an {% endfor %}, and every {{ a }}?Close each one; check the brackets.
/group/players finds nobody.Does the address match the data exactly, capitals included?Use /group/Players, or compare with .lower() on both sides.

Answers to the checks

  1. Predict: How many list items (li) does the page have?
    3. {% for p in players %} repeats the li for each name in players, and {{ p }} shows the name.
  2. A part that changes: Someone visits /player/Leo. What is name inside player()?
    "Leo"
    <name> in the route matches whatever is in that place of the address, and Flask passes it to the function as the parameter name.
  3. When there is no such player: What does return "...", 404 do when no player matches?
    Sends the page with the status 404 Not Found, so browsers and search engines know the page is missing
    A route can return the page and a status code. 404 means not found; the normal 200 means OK.
  4. Spot the bug: The page shows an empty list, though players has two names. Click the line that is wrong.
    line 15
    The template only sees what is passed to it by name. render_template_string(PAGE, players=players) hands the list over.
  5. Where this leads: The club gets 200 players. What changes in your app?
    Only the data: the same template and routes make every page
    This is why sites use templates: the code stays the same size however much data there is.

Solutions

One possible solution for each task. Other correct programs are fine.

Modify › Wins

from flask import Flask, render_template_string

app = Flask(__name__)
players = [
    {"name": "Ada", "group": "Champions", "wins": 5},
    {"name": "Sam", "group": "Players", "wins": 4},
    {"name": "Mia", "group": "Beginners", "wins": 1},
    {"name": "Leo", "group": "Players", "wins": 3},
]

PAGE = """<h1>Chess club</h1>
<table>
  <tr><th>Player</th><th>Group</th><th>Wins</th></tr>
  {% for p in players %}
  <tr><td><a href="/player/{{ p['name'] }}">{{ p["name"] }}</a></td><td>{{ p["group"] }}</td><td>{{ p["wins"] }}</td></tr>
  {% endfor %}
</table>"""

PLAYER = """<h1>{{ p["name"] }}</h1>
<p>Group: {{ p["group"] }}</p>
<p><a href="/">All players</a></p>"""


@app.route("/")
def home():
    return render_template_string(PAGE, players=players)


@app.route("/player/<name>")
def player(name):
    for p in players:
        if p["name"] == name:
            return render_template_string(PLAYER, p=p)
    return "<h1>No player called " + name + "</h1>", 404


app.run()

Modify › A page for each group

from flask import Flask, render_template_string

app = Flask(__name__)
players = [
    {"name": "Ada", "group": "Champions", "wins": 5},
    {"name": "Sam", "group": "Players", "wins": 4},
    {"name": "Mia", "group": "Beginners", "wins": 1},
    {"name": "Leo", "group": "Players", "wins": 3},
]

PAGE = """<h1>Chess club</h1>
<table>
  <tr><th>Player</th><th>Group</th></tr>
  {% for p in players %}
  <tr><td><a href="/player/{{ p['name'] }}">{{ p["name"] }}</a></td><td>{{ p["group"] }}</td></tr>
  {% endfor %}
</table>"""

PLAYER = """<h1>{{ p["name"] }}</h1>
<p>Group: <a href="/group/{{ p['group'] }}">{{ p["group"] }}</a></p>
<p><a href="/">All players</a></p>"""


@app.route("/")
def home():
    return render_template_string(PAGE, players=players)


@app.route("/player/<name>")
def player(name):
    for p in players:
        if p["name"] == name:
            return render_template_string(PLAYER, p=p)
    return "<h1>No player called " + name + "</h1>", 404


@app.route("/group/<group>")
def group_page(group):
    members = []
    for p in players:
        if p["group"] == group:
            members.append(p)
    return render_template_string(PAGE, players=members)


app.run()

the leaders

from flask import Flask, render_template_string

app = Flask(__name__)
players = [
    {"name": "Ada", "group": "Champions", "wins": 5},
    {"name": "Sam", "group": "Players", "wins": 4},
    {"name": "Mia", "group": "Beginners", "wins": 1},
    {"name": "Leo", "group": "Players", "wins": 3},
]

LEADERS = """<h1>Leaders</h1>
<ol>
{% for p in leaders %}
  <li>{{ p["name"] }}: {{ p["wins"] }} wins</li>
{% endfor %}
</ol>"""


@app.route("/leaders")
def leaders_page():
    leaders = []
    for p in players:
        if p["wins"] >= 3:
            leaders.append(p)
    return render_template_string(LEADERS, leaders=leaders)


app.run()

Stretch and support

Stretch: Sort the leaders by wins, highest first (sorted(leaders, key=lambda p: p["wins"], reverse=True)), or show the number of players in each group on the home page.

Support: Give students the group route's loop, and ask them only to write the route line and the return.

Exit ticket

What does {% for p in players %} do in a template?

Repeats the part up to {% endfor %} once for each item in players, with p as that item.

What each step is for

Where it leads

Next lesson adds a form that adds players, then saving them to a file. Data detective's tables and this template idea meet again in any web dashboard.