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.
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.
Predict and Run 10 min · Investigate 10 min · Modify 15 min · Make 15 min
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.
Read-change-write for the Make. Ask students to write the three steps as comments before any code.
| What you will see | A question to ask | The 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. |
One possible solution for each task. Other correct programs are fine.
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()
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()
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: 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.
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.
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.