← Web apps with Flask

Pages from data

Keep the club's players in a list, and let a template build the pages from it: one table for all, and a page for each player.

A sports site does not have a hand-written page for every player. It has one template, filled in from data.

Writing every page by hand stops working when there are many of them. A template is HTML with gaps: {{ p }} shows a value, and {% for p in players %} ... {% endfor %} repeats a part for each item in a list. render_template_string(PAGE, players=players) fills in the template with the values you pass by name. Each player here is a dictionary, which looks a value up by its name (its key): p["name"].

PredictRead the code and say what it will do before you run anything. A wrong guess is fine: it shows you what to look for.

Predict

club.py
from flask import Flask, render_template_string

app = Flask(__name__)
players = ["Ada", "Sam", "Mia"]

PAGE = """<h1>Players</h1>
<ul>
{% for p in players %}
  <li>{{ p }}</li>
{% endfor %}
</ul>"""


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


app.run()

The li line is written once in the template. What does {% for %} do with it?

How many list items (li) does the page have?

RunRun it and compare the result with your prediction.

Run

Run it, then click a player's name. The address /player/<name> has a part that changes: whatever is in that place of the address is passed to the function as name. Try /player/Zoe in the address bar. Then add a fifth player to the list and run again.

club.py
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: {{ 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()
Try it yourselfYour edits are saved in this browser.

InvestigateTrace how each line of code connects to what you see.

Investigate

club.py
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: {{ 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()

What the function receives.

A page and a status code.

A template with nothing to show.

Someone visits /player/Leo. What is name inside player()?

What does return "...", 404 do when no player matches?

The page shows an empty list, though players has two names. Click the line that is wrong.

ModifyChange the code on purpose and check the result.

Modify

Show each player's wins in a third column.

Need a hint?

Add a Wins heading cell, and a cell with {{ p["wins"] }} in the row.

Add /group/<group>, a page listing only that group's players (try /group/Players), and link to it from each player's page.

Need a hint?

A new route with a part that changes, like the player route, and a function that keeps only the players in that group.

Another hint

@app.route("/group/<group>"), then def group_page(group): with a loop that appends the matching players to a list.

Another hint

Reuse PAGE: render_template_string(PAGE, players=members). Link to it from the player page.

Try it yourselfYour edits are saved in this browser.

Show each player's wins in a third column of the table.

    Run your program. This ticks when your code does all of the above.

    Add a page for each group, /group/<group>, listing only its players.

      Run your program. This ticks when your code does all of the above.

      MakeBuild something new using what you have learned. Expect a few tries; that is normal.

      The leaders

      Add a /leaders page: the players with 3 or more wins, as a numbered list, made with a template of your own.

      Try it yourselfYour edits are saved in this browser.

      Add a /leaders page listing the players with 3 or more wins, built with a template.

        Run your program. This ticks when your code does all of the above.

        Info

        Where this leads

        On a computer, templates usually live in their own files in a templates folder, loaded with render_template("club.html", ...); the {{ }} and {% %} work exactly the same. Next: a form that adds players.

        The club gets 200 players. What changes in your app?

        For teachers

        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. About 50 minutes.

        Open the teacher guide: answers, solutions, slides and a worksheet.

        Students will be able to

        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

        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.

        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.

        What each step is for

        Now you can
        Next lesson3. Join the clubMake the club page's sign-up form work: the form sends its answers, a route reads them, adds the new member and goes back home.