← Web apps with Flask

Keep the sign-ups

Save each new member to a CSV file, and read the file to show the list, so sign-ups last after the app stops.

A web app that forgets everyone when it restarts is no use. Real apps keep their data in files and databases.

Last lesson the members vanished on every Run, because they lived in a variable. Now they live in a file, members.csv (open it in the Files tab). The home page reads the file every time; joining adds a row to the end of it. open(name, "a") opens a file to add to the end, and csv.writer(f).writerow([name, group]) writes one row, and list(csv.DictReader(f)) reads the file back as a list of dictionaries, one per row, using the first line (name,group) as the keys.

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
with open("log.txt", "w") as f:
    f.write("Ada\n")
with open("log.txt", "a") as f:
    f.write("Sam\n")
with open("log.txt") as f:
    print(f.read())

"w" opens a file for writing (it empties it first). What does "a" do differently?

What does it print?

RunRun it and compare the result with your prediction.

Run

Join the club, then press Run again, and reload the page: the new member is still there. Open members.csv in the Files tab to see the new row.

club.py
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.")
    save_member(name, 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
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.")
    save_member(name, group)
    return redirect("/")


app.run()

Why the page reads the file each time.

What newline="" is for.

A save that wipes the club.

Why does home() call load_members() every time, instead of reading the file once at the start?

Why is newline="" there when the file is opened to add a row?

After saving Zoe, the file reads back as an empty list, and Ada and Sam have gone. Click the line that is wrong.

ModifyChange the code on purpose and check the result.

Modify

Refuse a name that is already in the file, whatever its capitals, with a message saying it is already a member.

Need a hint?

In join(), before saving: loop over load_members(), and if a member's name matches (compare with .lower()), return the page with an error.

Another hint

error=name + " is already a member."

Show the number of members in the Members heading.

Need a hint?

In home(): members = load_members(), then pass count=len(members) to the template, and show {{ count }} in the heading.

Try it yourselfYour edits are saved in this browser.

Refuse a name that is already a member, with a message that says it is "already a member".

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

    Show how many members there are in the heading.

      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.

      Results that last

      The Add a win form is there, but win() does nothing yet. Make it add 1 to the chosen player's wins and save the whole table back to results.csv, header first, so the wins last after a Run. ("a" only adds rows; changing a row means writing the file again.)

      Try it yourselfYour edits are saved in this browser.

      Add a win to the chosen player and save every row back to results.csv, so wins last.

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

        Info

        Where this leads

        On a computer, members.csv would sit next to app.py. Next: share the members with other programs as JSON.

        Two people press Join at the same moment on a busy real site. What could go wrong with a CSV file?

        For teachers

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

        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.

        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.

        What each step is for

        Now you can
        Next lesson5. An API for the clubSend the club's data as JSON, so other programs (an app, a game, another website) can read it: routes that return data instead of pages.