Last time, we learned about Python, a programming language that comes with many features and libraries. Today, we’ll use Python to generate HTML for webpages, and see how separations of concerns might be applied.
A few weeks ago, we learned about web requests in HTTP, which might look like this:
GET / HTTP/1.1
Host: www.example.com
...
Hopefully, a server responds with something like:
HTTP/1.1 200 OK
Content-Type: text/html
...
...
is the actual HTML of the page.Today, we’ll use Flask, a microframework, or a set of code that allows us to build programs without writing shared or repeated code over and over. (Bootstrap, for example, is a framework for CSS.)
Flask is written in Python and is a set of libraries of code that we can use to write a web server in Python.
One methodology for organizing web server code is MVC, or Model-View-Controller:
Today, we’ll build a website where students can fill out a form to register for Frosh IMs, freshman year intramural sports.
We can start by opening the CS50 IDE, and write some Python code that is a simple web server program, serve.py
:
from http.server import BaseHTTPRequestHandler, HTTPServer
class HTTPServer_RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"<!DOCTYPE html>")
self.wfile.write(b"<html lang='en'>")
self.wfile.write(b"<head>")
self.wfile.write(b"<title>hello, title</title>")
self.wfile.write(b"</head>")
self.wfile.write(b"<body>")
self.wfile.write(b"hello, body")
self.wfile.write(b"</body>")
self.wfile.write(b"</html>")
port = 8080
server_address = ("0.0.0.0", port)
httpd = HTTPServer(server_address, HTTPServer_RequestHandler)
httpd.serve_forever()
http
library that we can import that handles the HTTP layer, but we have written our own do_GET
function that will be called every time we receive a GET request. As usual, we need to look at the documentation for the library to get a sense of what we should write, and what we have available for us. First, we send a 200 status code, and send the HTTP header indicating that this is an HTML page. Then, we write (as ASCII bytes) some HTML, line by line, into the response.python serve.py
, we can click CS50 IDE > Web Server, which will open our IDE’s web server in another tab for us, and we’ll see the hello, world page we just wrote.We can see that reimplementing many common functions of a web server can get tedious, even with an HTTP library, so a framework like Flask helps a lot in providing abstractions and shortcuts that we can reuse.
With Flask, we can write the following in an application.py
file:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def index():
return "hello, world"
app = Flask(__name__)
, we initialize a Flask application for our application.py
file. Then, we use the @app.route("/")
syntax to indicate that the function below will respond to any requests for /
, or the root page of our site. We call that function index
by convention, and it will just return “hello, world” as the response, without any HTML.flask run
from the terminal in the same folder as our application.py
, and the resulting URL will show a page that reads “hello, world” (which our browser displays even without HTML).We can change the index
function to return a template, or a file that has HTML that we’ve written, that acts as the View.
return render_template("index.html")
In a templates
folder, we’ll have an index.html
file with the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>hello</title>
</head>
<body>
hello,
</body>
</html>
We see a new feature, ``, like a placeholder. So we’ll go back and change the logic of index
, our controller, to check for parameters in the URL and pass them to the view:
return render_template("index.html", name=request.args.get("name", "world"))
request.args.get
to get a parameter from the request’s URL called name
. (The second argument, world
, will be the default value that’s returned if one wasn’t set.) Now, we can visit /?name=David
to see “hello, David” on the page. Now, we can generate an infinite number of webpages, even though we’ve only written a few lines of code.In froshims0
, we can write an application.py
that can receive and respond to a POST request from a form:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/register", methods=["POST"])
def register():
if not request.form.get("name") or not request.form.get("dorm"):
return render_template("failure.html")
return render_template("success.html")
For the default page, we’ll return an index.html
that contains a form:
{% extends "layout.html" %}
{% block body %}
<h1>Register for Frosh IMs</h1>
<form action="/register" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
<select name="dorm">
<option disabled selected value="">Dorm</option>
<option value="Apley Court">Apley Court</option>
<option value="Canaday">Canaday</option>
<option value="Grays">Grays</option>
<option value="Greenough">Greenough</option>
<option value="Hollis">Hollis</option>
<option value="Holworthy">Holworthy</option>
<option value="Hurlbut">Hurlbut</option>
<option value="Lionel">Lionel</option>
<option value="Matthews">Matthews</option>
<option value="Mower">Mower</option>
<option value="Pennypacker">Pennypacker</option>
<option value="Stoughton">Stoughton</option>
<option value="Straus">Straus</option>
<option value="Thayer">Thayer</option>
<option value="Weld">Weld</option>
<option value="Wigglesworth">Wigglesworth</option>
</select>
<input type="submit" value="Register">
</form>
{% endblock %}
We have an HTML form, with an input
tag for a student to type in their name, and a select
tag to create a dropdown list for them to select a dorm. Our form will be submitted to a route we call /register
, and we’ll use the POST method to send the form’s information.
Notice that our template is now using a new feature, extends
, to define blocks that will be substituted themselves in another file, layout.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>froshims0</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{% block body %}{% endblock %}
syntax is a placeholder block in Flask, where other pages, like index.html
, can provide HTML that will be substituted into that block.In our register
function, we’ll indicate that we’re listening for a POST request, and inside the function, just make sure that we got a value for both name
and dorm
. request.form
is an abstraction provided by Flask, such that we can access the arguments, or parameters, from the request’s POST data.
When we run our application with flask run
, and visit the URL, sometimes we might see an Internal Server Error. And if we come back to our terminal, where our Flask server is running, we’ll see an error message that provides us clues to what went wrong. We can press Control+C to stop our web server, make changes that will hopefully fix our error, and start our web server again. And even if nothing is broken but we made a change, sometimes we need to quit Flask and start it again, for it to notice those changes.
We also need a success.html
and failure.html
in our templates
directory, which might look like:
{% extends "layout.html" %}
{% block body %}
You are registered! (Well, not really.)
{% endblock %}
register
function will return that, with the template fully rendered, if we provided both a name and dorm in the form.layout.html
, we didn’t need to copy and paste the same <head>
and other shared markup, making it easier for us to make changes across all the pages we have at once.The failure page, too, will share the same layout but send a different message:
{% extends "layout.html" %}
{% block body %}
You must provide your name and dorm!
{% endblock %}
{% %}
syntax is actually called Jinja, a templating language that Flask is able to understand and put together.And all of this Python code lives on our server in the CS50 IDE, generating a completed HTML page each time and sending it to the browser as a response. We can see that by right-clicking the page in Chrome, clicking View Source, and seeing the full HTML that users will get.
Now let’s actually do something with the submitted form information. In froshims1/application.py
, we’ll create a list to store all the registered students:
from flask import Flask, redirect, render_template, request
# Configure app
app = Flask(__name__)
# Registered students
students = []
@app.route("/")
def index():
return render_template("index.html")
@app.route("/registrants")
def registrants():
return render_template("registered.html", students=students)
@app.route("/register", methods=["POST"])
def register():
name = request.form.get("name")
dorm = request.form.get("dorm")
if not name or not dorm:
return render_template("failure.html")
students.append(f"{name} from {dorm}")
return redirect("/registrants")
We create an empty list, students = []
, and when we get a name and dorm in register
, we’ll use students.append(f"{name} from {dorm}")
to add a formatted string with that name and dorm, to the students
list.
In the registrants
function, we’ll pass in our students
list to the template of registered.html
:
{% extends "layout.html" %}
{% block body %}
<ul>
{% for student in students %}
<li>{{ student }}</li>
{% endfor %}
</ul>
{% endblock %}
for
loop to generate HTML based on variables passed into the template. (We need an endfor
since, in HTML, indentation is only needed for stylistic purposes, so we need to specify when a loop ends.) Here, we’re creating an <li>
for each student
, or string, in the students
variable that was passed in by the controller, application.py
. And notice that the markup, or formatting of the list, is in this template, or view.If we stop our server, and restart it, we’ll have lost all of the data we’ve collected, since the students
variable is only created and stored as long as our program is running.