← Teacher guide

Join the club

Students make the club page's sign-up form work with Flask, reading the answers with request.form and redirecting home, then refuse blank names, add a year level and build a form that records a win.

Web apps with Flask · Forms and saving · 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. <form method="post" action="/join"> sends the form to /join when the button is pressed; each box is sent only if it has a name attribute, under that name (the id is for the label; the bug task has an id and no name, and Flask replies 400 Bad Request). A route accepts a sent form with methods=["POST"] and reads it with request.form["name"]. After changing the data it returns redirect("/"), so the browser shows the home page again (and a reload does not send the form twice). The server checks what it gets (a blank name), because anyone can send anything to an address. Members live in a variable, so they vanish on Run: that motivates the next lesson. In the playground the form is passed to the app by the App tab; on a computer the browser sends it to Flask.

Running the lesson

  1. Predict: act it out: one student is the form, one is join(), one is the home page.
  2. Run: join twice, then press Run. Where did they go?
  3. Investigate: in the bug task, ask what the label's for= and the box's id do, and what name= does.
  4. Modify: test with a blank name and with spaces only (strip() makes them blank).

The hard part (on purpose)

The name attribute on form boxes. Ask students to list what the form sends, box by box, before they look at the route.

When students get stuck

What you will seeA question to askThe fix
400 Bad Request when the form is sent.Does every box the route reads have a name attribute, spelled the same?name="year" on the box, request.form["year"] in the route.
405 Method Not Allowed.Does the route accept POST?@app.route("/join", methods=["POST"]).
The error message never shows.Is error passed to the template, and does the template show it?error="..." in render_template_string, and {% if error %}{{ error }}{% endif %} in PAGE.

Answers to the checks

  1. Predict: You type Zoe and press Join. What does the page show?
    ['Ada', 'Zoe']. join() appends what was typed in the box named name, then redirect("/") shows the home page again, with the longer list.
  2. Back home: Why does join() end with redirect("/") instead of returning a page?
    So the browser goes back to the home page, which shows the list with the new member
    The home route already shows the members. Redirecting there after a change is the usual pattern, and reloading the page then does not send the form twice.
  3. A choice from a list: What does request.form["group"] give when someone chooses Champions?
    "Champions"
    A select sends the chosen option's text under its name, group.
  4. Spot the bug: Pressing Join gives 400 Bad Request. Click the line that is wrong.
    line 8
    The box has an id but no name, so the form sends nothing for it, and request.form["name"] has nothing to find. name="name" sends it.
  5. Where this leads: Why should the server check the name, when the form could just say the box is required?
    Anyone can send anything to /join without using your form, so the server must check what it gets
    A form's checks help honest users; the server's checks protect the data. Every real web app checks again on the server.

Solutions

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

Modify › No blank names

from flask import Flask, render_template_string, request, redirect

app = Flask(__name__)
members = [
    {"name": "Ada", "group": "Champions"},
    {"name": "Sam", "group": "Players"},
]

PAGE = """<h1>Chess club</h1>
<h2>Members</h2>
<ul>
{% for m in members %}
  <li>{{ m["name"] }} ({{ m["group"] }})</li>
{% endfor %}
</ul>
<h2>Join the club</h2>
{% if error %}<p style="color: crimson">{{ error }}</p>{% endif %}
<form method="post" action="/join">
  <label for="name">Name</label>
  <input id="name" name="name">
  <label for="group">Group</label>
  <select id="group" name="group">
    <option>Beginners</option>
    <option>Players</option>
    <option>Champions</option>
  </select>
  <button>Join</button>
</form>"""


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


@app.route("/join", methods=["POST"])
def join():
    name = request.form["name"].strip()
    group = request.form["group"]
    if name == "":
        return render_template_string(PAGE, members=members, error="Please type your name.")
    members.append({"name": name, "group": group})
    return redirect("/")


app.run()

Modify › Year level

from flask import Flask, render_template_string, request, redirect

app = Flask(__name__)
members = [
    {"name": "Ada", "group": "Champions", "year": "9"},
    {"name": "Sam", "group": "Players", "year": "8"},
]

PAGE = """<h1>Chess club</h1>
<h2>Members</h2>
<ul>
{% for m in members %}
  <li>{{ m["name"] }} ({{ m["group"] }}, Year {{ m["year"] }})</li>
{% endfor %}
</ul>
<h2>Join the club</h2>
<form method="post" action="/join">
  <label for="name">Name</label>
  <input id="name" name="name">
  <label for="group">Group</label>
  <select id="group" name="group">
    <option>Beginners</option>
    <option>Players</option>
    <option>Champions</option>
  </select>
  <label for="year">Year</label>
  <input id="year" name="year">
  <button>Join</button>
</form>"""


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


@app.route("/join", methods=["POST"])
def join():
    name = request.form["name"].strip()
    group = request.form["group"]
    year = request.form["year"]
    members.append({"name": name, "group": group, "year": year})
    return redirect("/")


app.run()

record a win

from flask import Flask, render_template_string, request, redirect

app = Flask(__name__)
players = [
    {"name": "Ada", "wins": 5},
    {"name": "Sam", "wins": 4},
    {"name": "Mia", "wins": 1},
]

PAGE = """<h1>Results</h1>
<table>
  <tr><th>Player</th><th>Wins</th></tr>
  {% for p in players %}
  <tr><td>{{ p["name"] }}</td><td>{{ p["wins"] }}</td></tr>
  {% endfor %}
</table>
<h2>Record a win</h2>
<form method="post" action="/win">
  <select name="player">
    {% for p in players %}
    <option>{{ p["name"] }}</option>
    {% endfor %}
  </select>
  <button>Add a win</button>
</form>"""


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


@app.route("/win", methods=["POST"])
def win():
    name = request.form["player"]
    for p in players:
        if p["name"] == name:
            p["wins"] = p["wins"] + 1
    return redirect("/")


app.run()

Stretch and support

Stretch: Refuse a name that is already a member, with its own message.

Support: Give students the if with the error return, and ask them only to add the error to the template.

Exit ticket

A form box has id="name" but no name attribute. What does the server get from it?

Nothing: only boxes with a name attribute are sent, under that name.

What each step is for

Where it leads

Next lesson saves the members to a CSV file so they survive a Run. Later, a JSON route lets other programs read the list.