← Teacher guide

Keep the sign-ups

Students save sign-ups to members.csv (adding a row with "a" and csv.writer) and read the file for every page, so members survive a Run and a reload, then refuse repeats, count members and save wins by rewriting results.csv.

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 15 min · Make 15 min

Before class

Read the unit primer first. members.csv and results.csv are put in each student's Files tab by the lesson (the Files tab can put the originals back). Files are kept per course in the browser, so they survive Run and a page reload. open(name, "a") adds to the end of a file; open(name, "w") empties it first (the Predict and the bug task: saving with "w" wipes the header and every member). csv.writer(f).writerow([...]) writes one row; newline="" stops blank lines appearing between rows on Windows. Every page reads the file again, so it is always up to date. Changing a row (the Make) means reading all rows, changing one, and writing the whole file back with "w", header first. The info question is about why busy sites use a database instead of a file.

Running the lesson

  1. Predict: two sticky notes: "w" throws the old note away, "a" adds to it.
  2. Run: join, press Run, reload: the member is still there. Open members.csv in the Files tab.
  3. Investigate: ask what would happen if the file were read once at the top.
  4. Make: on the board: read all rows, change one, write all back.

The hard part (on purpose)

Read-change-write for the Make. Ask students to write the three steps as comments before any code.

When students get stuck

What you will seeA question to askThe fix
Everyone but the new member has gone.Which mode opened the file to save?"a" to add a row; restore members.csv from the Files tab.
KeyError: 'name' when the page loads.What is the first line of members.csv now?The header row was lost; use "Get this lesson's files again" in the Files tab.
Wins show as 51 instead of 6.Is wins text or a number when 1 is added?int(p["wins"]) + 1.

Answers to the checks

  1. Predict: What does it print?
    Ada, then Sam. "w" makes the file and writes Ada; "a" adds Sam to the end instead of emptying the file.
  2. Read every time: Why does home() call load_members() every time, instead of reading the file once at the start?
    So the page always shows what is in the file now, including members who just joined
    The file changes whenever someone joins. Reading it for every page means the page is never out of date.
  3. A quirk of csv: Why is newline="" there when the file is opened to add a row?
    It stops csv writing blank lines between rows on some computers
    csv.writer writes its own line endings; newline="" stops the file adding extra ones (a known quirk on Windows). Use it whenever csv writes a file.
  4. Spot the bug: After saving Zoe, the file reads back as an empty list, and Ada and Sam have gone. Click the line that is wrong.
    line 5
    "w" empties the file before writing, so the header and every member are lost, and Zoe's row becomes the header. "a" adds to the end.
  5. Where this leads: Two people press Join at the same moment on a busy real site. What could go wrong with a CSV file?
    Both could write at once and spoil the file: real sites use a database, which handles this
    Files are fine for a small app. Busy sites use a database (SQLite, PostgreSQL), which keeps the data safe when many people change it at once.

Solutions

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

Modify › No repeats

import csv
from flask import Flask, render_template_string, request, redirect

app = Flask(__name__)
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>"""


def load_members():
    with open("members.csv") as f:
        return list(csv.DictReader(f))


def save_member(name, group):
    with open("members.csv", "a", newline="") as f:
        csv.writer(f).writerow([name, group])


@app.route("/")
def home():
    return render_template_string(PAGE, members=load_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=load_members(), error="Please type your name.")
    for m in load_members():
        if m["name"].lower() == name.lower():
            return render_template_string(PAGE, members=load_members(), error=name + " is already a member.")
    save_member(name, group)
    return redirect("/")


app.run()

Modify › How many members

import csv
from flask import Flask, render_template_string, request, redirect

app = Flask(__name__)
PAGE = """<h1>Chess club</h1>
<h2>Members ({{ count }})</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>"""


def load_members():
    with open("members.csv") as f:
        return list(csv.DictReader(f))


def save_member(name, group):
    with open("members.csv", "a", newline="") as f:
        csv.writer(f).writerow([name, group])


@app.route("/")
def home():
    members = load_members()
    return render_template_string(PAGE, members=members, count=len(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=load_members(), error="Please type your name.")
    save_member(name, group)
    return redirect("/")


app.run()

results that last

import csv
from flask import Flask, render_template_string, request, redirect

app = Flask(__name__)

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


def load_results():
    with open("results.csv") as f:
        return list(csv.DictReader(f))


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


@app.route("/win", methods=["POST"])
def win():
    name = request.form["player"]
    players = load_results()
    for p in players:
        if p["player"] == name:
            p["wins"] = int(p["wins"]) + 1
    with open("results.csv", "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["player", "wins"])
        for p in players:
            writer.writerow([p["player"], p["wins"]])
    return redirect("/")


app.run()

Stretch and support

Stretch: Add a Leave form that removes a member (read all rows, keep the others, write them back).

Support: Give students the loop that changes the wins, and ask them only to write the rows back.

Exit ticket

Why does the app read members.csv for every page?

The file changes when people join, so reading it each time means the page always shows the current members.

What each step is for

Where it leads

Next lesson sends the members as JSON for other programs. A database (SQLite) is the next step for real apps; Python has sqlite3 built in.