← Web apps with Flask

Join the club

Make 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.

In HTML & CSS the Join button did nothing. Now Python is behind the form, so it can really sign people up.

In HTML & CSS your club page had a form that went nowhere. With method="post" action="/join", pressing the button sends the form's answers to the address /join. A route with methods=["POST"] receives them, and request.form["name"] looks up the box whose name attribute is name. members.append(...) adds the new member to the end of the list. Then redirect("/") sends the browser back to the home page.

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, request, redirect

app = Flask(__name__)
members = ["Ada"]

PAGE = """<p>{{ members }}</p>
<form method="post" action="/join">
  <input name="name">
  <button>Join</button>
</form>"""


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


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


app.run()

Follow the answer: the form sends it to /join, join() appends it, then redirect("/") goes back home.

You type Zoe and press Join. What does the page show?

RunRun it and compare the result with your prediction.

Run

Run it and join the club twice, with different groups. Then press Run again: where did the new members go?

club.py
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>
<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"]
    members.append({"name": name, "group": group})
    return redirect("/")


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, 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>
<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"]
    members.append({"name": name, "group": group})
    return redirect("/")


app.run()

Why a form ends with a redirect.

What a select sends.

A box the form does not send.

Why does join() end with redirect("/") instead of returning a page?

What does request.form["group"] give when someone chooses Champions?

Pressing Join gives 400 Bad Request. Click the line that is wrong.

ModifyChange the code on purpose and check the result.

Modify

Press Join with an empty box: a member with no name appears. Make the app refuse a blank name and say why.

Need a hint?

In join(), after reading name: if name == "": return the page with an error message instead of adding the member.

Another hint

render_template_string(PAGE, members=members, error="Please type your name."), and in the template {% if error %}<p>{{ error }}</p>{% endif %}.

Ask for each member's year as well, and show it in the list.

Need a hint?

Add a Year box with name="year", read it with request.form["year"], store it in the member's dictionary and show it in the list.

Try it yourselfYour edits are saved in this browser.

If the name is blank, show the page again with a message, and do not add anyone.

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

    Also ask for each member's year level, and show it in the list.

      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.

      Record a win

      Add a form under the results table: a dropdown of the players (made by the template) and an Add a win button that adds 1 to that player's wins.

      Try it yourselfYour edits are saved in this browser.

      Add a form to record a win: choose a player, press the button, and their wins go up by 1.

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

        Info

        Where this leads

        The members disappear on every Run, because they live in a variable. Next: save them to a file so they last.

        Why should the server check the name, when the form could just say the box is required?

        For teachers

        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. 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. <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

        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.

        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.

        What each step is for

        Now you can
        Next lesson4. Keep the sign-upsSave each new member to a CSV file, and read the file to show the list, so sign-ups last after the app stops.