A document from MCS 275 Spring 2024, instructor Emily Dumas. You can also get the notebook file.

MCS 275 Spring 2024 Homework 12 Solutions

  • Course Instructor: Emily Dumas

2. First come, first served

Write a Flask web application that supports the following URLs and behaviors. (When I list a URL as e.g. /asdf/ it is an abbreviation for the full URL that would look something like http://localhost:5000/asdf/.)

  • /<n>/ where n is any positive integer - If this URL has never been visited before (see below for how to determine this), displays a HTML document with a large header reading "Congratulations, you were first!". But if this URL has been visited before, it instead displays a message like "Sorry, this integer was claimed at 10:38AM on April 04, 2024." Of course, the date/time in this example message is replaced with the actual time of the first visit to that URL.

  • / - The front page of the application is an HTML document that has a header reading "First come, first served", followed by an explanation that the user can visit /<n>/ for any integer n to attempt to claim that number. It should then say that the user can also try one of the sample integers below, followed by a bulleted list similar to this:

    • /5782/
    • /1729/
    • /42/
    • /9718/
    • /210/

      but these numbers should be replaced by five random integers less than 10000, chosen again each time the front page is served. Each item in the list should link to the corresponding page in the application.

In other words, this application treats positive integers as things you can 'claim' by visiting a url like /2752024/. The first such visit stakes a claim, and anyone else visiting the URL from then onward will be told it is already taken. The front page just gives you an easy way to visit a few randomly chosen URLs of that form.

Requirements for how the application does this:

  • This application must use a SQLite database table to keep track of which integers have been visited, and when the first visit happened. The application should create the database and table if it doesn't exist when the program is first run. If it does exist, though, the application should use (and add to) the data already in the table. That means the application can tell whether a URL has been visited even if it happened during a previous time the program was run.

  • The only mutable global variable you are allowed to create is the flask.Flask object representing your application. (We usually call it app.) Any data that needs to remain available from one request to the next needs to be stored in the database.

  • Every function that handles HTTP requests should establish, use, and close its own database connection.

Solution

Here it is in a single notebook cell. It uses string formatting to make HTML, which is not optimal for complex pages but allows the entire solution to be shown here.

In [9]:
from flask import Flask
import time
import sqlite3
import datetime
import random

"First come first served web app"

DB_FN = "claims.db"

STATIC_FIRST="""<!doctype html>
<html>
<head>
<title>Congratulations</title>
</head>
<body>
<h1>Congratulations, you were first!</h1>
</body>
</html>"""

FRONT_TEMPLATE = """<!doctype html>
<html>
<head>
<title>First come, first served</title>
</head>
<body>
<h1>First come, first served</h1>
<p>
<ul>
{}
</ul>
</body>
</html>"""

def ts_fmt(timestamp):
    "Format a `timestamp` (seconds since epoch) as a nice string"
    dt = datetime.datetime.fromtimestamp(timestamp)
    return dt.strftime("%I:%M%p on %B %d, %Y")

app = Flask(__name__)

@app.route("/")
def front():
    "Front page"
    return FRONT_TEMPLATE.format(
        "\n".join(
            [ '<li><a href="/{0}/">{0}</a></li>'.format(random.randrange(10000)) for _ in range(5) ]
        )
    )

@app.route("/<int:claimid>/")
def attempt_claim(claimid):
    "Claim an integer if possible"
    now = time.time()
    con = sqlite3.connect(DB_FN)
    try:
        con.execute(
            """
                    INSERT INTO claims (claimid,claimed_ts)
                    VALUES (?,?);
                    """,
            [
                claimid,
                now
            ],
        )
        con.commit()
    except sqlite3.Error:
        # Error when inserting means it was claimed already.
        res = con.execute("SELECT claimed_ts FROM claims WHERE claimid=?;",[claimid])
        return """<!doctype html>
<html>
<head>
<title>Already claimed!</title>
</head>
<body>
<h1>Sorry!</h1>
<p>That integer was claimed at {}.</p>
</body>
</html>""".format(ts_fmt(res.fetchone()[0]))
    finally:
        con.close()
    
    return STATIC_FIRST

# Make sure we have a database, create if needed
try:
    tstcon = sqlite3.connect(DB_FN)
    tstcon.execute("SELECT COUNT(*) FROM claims;")
except sqlite3.OperationalError:
    print("WARNING: Database not found; creating.")
    tstcon.execute("DROP TABLE IF EXISTS claims;")
    tstcon.execute(
        """
        CREATE TABLE claims (
            claimid INTEGER PRIMARY KEY,
            claimed_ts REAL NOT NULL
        );"""
    )
    tstcon.commit()
finally:
    tstcon.close()

app.run()
 * Serving Flask app '__main__'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Revision history

  • 2024-04-18 Initial publication