Flask 4
In this reading, we will round out our use of the core of Flask. This is a good time to ask questions about anything we've covered, before we turn to other topics.
This reading is, at this point, mostly review, but you might refresh your memory.
One-Route GET/POST Forms¶
So far, we've seen forms that post to a different route:
@route('/get-form/')
def get_form():
# this is just a blank form
return render_template('form.html', action=url_for('do_form'))
@route('/do-form/')
def do_form():
if request.method == 'GET':
data = request.args
else:
data = request.form
...
The first route just sends a blank form to the user. The second processes the form submission. That pattern works fine.
In the very common case that the form uses the POST method, we can collapse the two routes/URLs to a single URL, where a GET of that URL sends the blank form, and the POST processes the form. Like this:
@route('/form1/')
def form1():
if request.method == 'GET':
# this is just a blank form
return render_template('form.html')
else:
data = request.form
...
Furthermore, the FORM tag in the HTML can just have the empty string as the ACTION, like this:
<form method=POST action="">
...
</form>
That's because an empty URL goes to the current URL. Thus, a single route does it all, and the form's HTML is independent of the route, which is a nice bit of modularity.
However, an empty ACTION attribute can be exploited for certain web vulnerabilities, so it's better to supply it anyhow:
<form method=POST action="{{url_for('form1')}}">
...
</form>
I've updated all the examples in the flask3
folder to avoid the
action=""
vulnerability.
One-Route Example¶
There is a working example in the flask3
folder, in the file
example_form_processing_one_route.py
.
Here's the HTML (look at the code in templates/ln.html
:
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Natural Logs</title>
</head>
<body>
<h1>Compute a Natural Logarithm</h1>
{% if msg %}
<p>{{msg}}</p>
{% endif %}
{% if y %}
<p>The Natural Log of {{x}} is {{y}}</p>
{% endif %}
<form method="POST" action="{{url_for('ln')}}">
<p><label>Number to Logify
<input name="x" type="number" step="any">
</label></p>
<p><input type="submit" value="POST"></p>
</form>
<h2>Menu</h2>
<form method="POST" action="{{url_for('ln')}}">
<p><label>Number (radicand)
<select name="x">
<option>Choose One</option>
<option value="2.718281828459">e</option>
<option value="7.389">e squared</option>
</select></label></p>
<p><input type="submit" value="submit"></p>
</form>
</body>
</html>
- The ACTION is the empty string, defaulting to the current URL
And the corresponding Flask route in
example_form_processing_one_route.py
:
from flask import (Flask, url_for, render_template, request)
import math
from all_examples import app
@app.route('/ln/', methods=['GET','POST'])
def ln():
if request.method == 'GET':
return render_template('ln.html')
else:
x = request.form.get('x')
try:
y = math.log(float(x))
return render_template(
'ln.html',x=x,y=y)
except:
return render_template(
'ln.html',
msg=('Error computing ln of {x}'
.format(x=x)))
- Now the ACTION is easier
- There is only one route
- We use GET to request an empty form
- We use POST to request an answer
You can run this example by running the all_examples.py
file in the
flask3
folder and choosing the routes shown above.
The POST-Redirect-GET Pattern¶
Anytime we POST to a URL, the user might want to refresh the page, which can cause re-submission of the form data (though the browser will try to prevent that). One way to reduce the risks and consequences of unwanted re-submission is the POST-Redirect-GET pattern.
We might also want to allow the user the user to re-submit. For example, when I want to refresh my classlist, to see if anyone has added or dropped, I might want to refresh the page, even though that page was the result of a submitting a form that used POST.
To do either of these, the route that we POST to returns a redirect to a route that is safe to refresh. Imagine an e-commerce site that, after you post to the route to submit your order, redirects you to a thanks/receipt page that is safe to refresh.
POST-Redirect-GET Example¶
You can look at a working example in your flask3
folder. The code is
in example_post_redirect_get.py
. This code refers to two
templates. One is templates/msg.html
which just displays a message
to the user. The other is the form, templates/city_form.html
:
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Country Lookup</title>
</head>
<body>
<h1>Look up a Country</h1>
<h2>POST</h2>
<!-- specifying the url in the action avoids certain vulnerabilities -->
<form method="POST" action="{{url_for('ask_city')}}">
<p><label>City
<input type="text" id="city1" name="city">
</label></p>
<p><input type="submit" value="POST"></p>
</form>
<h2>Menu</h2>
<form method="POST" action="{{url_for('ask_city')}}">
<p><label>City
<select id="city2" name="city">
<option>Choose One</option>
{% for city in cities %}
<option>{{city}}</option>
{% endfor %}
</select></label></p>
<p><input type="submit" value="submit"></p>
</form>
</body>
</html>
- Note how the menu is generated from data!
Here are the Flask routes, from example_post_redirect_get.py
:
from flask import (Flask, url_for, render_template, request,
redirect, flash)
from all_examples import app
import countries
@app.route('/ask_city/', methods=['GET','POST'])
def ask_city():
if request.method == 'GET':
return render_template(
'city_form.html',
cities=countries.known.keys())
else:
city = request.form.get('city')
# redirect is a new import from flask
return redirect(url_for('country',city=city))
@app.route('/country/<city>')
def country(city):
if city in countries.known:
return render_template(
'msg.html',
msg=('{city} is the capital of {country}'
.format(city=city,
country=countries.known[city])))
else:
return render_template(
'msg.html',
msg=('''I don't know the country whose capital is {city}'''
.format(city=city)))
- There's a nice division of labor between the two routes.
- This technique also avoids certain anomalies when answer are cached, or retrieved again. See: POST/REDIRECT/GET.
You can run this example by running the all_examples.py
file in the
flask3
folder and choosing the routes shown above.
Template Inheritance¶
Finally, let's see template inheritance in action. Your flask3
folder has an example Python file called
example_template_inheritance.py
:
# -*- coding: utf-8 -*-
from flask import (Flask, url_for, render_template, request, redirect, flash)
from all_examples import app
@app.route('/base/')
def base():
# the base template needs only one filler
return render_template('base.html',title='About')
# The reading has three inputs.
# Note the Unicode string; we'll talk about Unicode much later.
@app.route('/reading/')
def reading():
stuff = {'title' : '''Irish Welcomes (for class on St. Patrick's Day)''',
'classdate' : '3/17/2017',
'material' : u'céad míle fáilte or a hundred thousand welcomes'}
return render_template('reading.html',
title=stuff['title'],
classdate=stuff['classdate'],
material=stuff['material'])
@app.route('/inclass/')
def inclass():
stuff = {'title' : 'Irish Welcomes',
'toc' : 'roll call, assignments, review, new material',
'content' : 'The Irish speak "Irish" not "Gaelic".'}
# slightly different placeholders
return render_template('inclass.html',
title=stuff['title'],
toc=stuff['toc'],
content=stuff['content'])
This short file has three routes, each of which uses a different
template, base.html
, reading.html
and inclass.html
. Let's look
at each in turn.
Base Template¶
Let's start with the first, base
, which uses base.html
. Here's the
template file:
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name=author content="Scott D. Anderson">
<title>{{title}}</title>
<link rel="stylesheet"
href="{{url_for('static',filename='inheritance.css')}}"
type="text/css">
{% block headstuff %}{% endblock %}
</head>
<body>
<header id="top">
{% block top %}
This text is at the top of every page, unless overridden.
{% endblock %}
</header>
<article id="content">
{% block content %}
<h1>{{title}}</h1>
<p>This stuff appears on every page, unless overridden.</p>
<p>Lorem ipsum ...</p>
{% endblock %}
</article>
<footer>
{% block footer %}
Default Copyright notices and stuff
{% endblock %}
</footer>
</body>
</html>
This is pretty ordinary HTML, mixed with some Jinja2 templating language, but the template has four named blocks:
- line 10
headstuff
which is completely empty - lines 14-16
top
which has a single line of text in it - lines 20-26
content
which has an H1 and two P elements in it - lines 30-32
footer
which has a single line of text in it
When we render the template, the begin/end markers are removed, though not the content of the blocks. The markers will be important if we use inheritance.
Reading Template¶
Now let's look at the reading.html
template:
{% extends "base.html" %}
{% block top %}
Overridden by the reading.html template
{% endblock %}
{# replaces default content block,
adds holes classdate and material #}
{% block content %}
<h1>Reading on {{title}}</h1>
<p>Please read the following before class on {{ classdate }}</p>
<section>{{ material }}</section>
{% endblock %}
{# replaces default footer #}
{% block footer %}
© 2019 Scott D. Anderson and the CS 304 staff
{% endblock %}
This doesn't look like an HTML file at all! Well, there's a little HTML in it, but only inside these named blocks. This file also starts like this:
{% extends "base.html" %}
That's because it's a child template. It's a variation of the
base.html
template, so we start with all of that HTML. Then we
override/replace certain named blocks.
- lines 3-5 replace line of text in the
top
block with a different line of text. - lines 9-15 replace the
content
block with a different bunch of HTML. Now we have an H1, a P, and a SECTION. - lines 18-20 replace the footer with a different one
Notice that the content block now has 3 placeholders, title
,
classdate
and material
, while the content block in the base
template only had two. This shows:
- you can add/remove placeholders using inheritance
- you have to change the
render_template
call to pass values for all the placeholders.
Look back at the Python file to see the changed arguments to
render_template
The inclass Template¶
The last template is a variation on the theme:
{% extends "base.html" %}
{# additional stuff for the head #}
{% block headstuff %}
<style> .optional { color: gray; }</style>
{% endblock %}
{# replaces default content block #}
{% block content %}
<h1>Class Activities on {{title}}</h1>
<p class="optional">We'll do the following in class; no need to read beforehand.</p>
<nav>{{toc}}</nav>
<section>{{content}}</section>
{% endblock %}
- lines 4-6 replace the
headstuff
block, hitherto empty, with some CSS that we'll use only in this template. - lines 9-16 replace the
content
block again, this time with three placeholders namedtitle
,toc
(short fortable of contents
) andcontent
.
That concludes our expanded description of template inheritance.
Flask-Starter¶
Our imports from Flask have been steadily growing and they'll grow a
bit more as the semester continues. There are also some
initializations, such as app.secret_key
and dbi.cache_cnf()
, that
we need to remember to do. There's also the structure of the app, with
sub-folders for templates
and static
. It all gets to be a bit
much.
Indeed, one reason I was initially reluctant to commit to Flask in CS 304 is that I worried that it would be hard to remember all these pieces, and I haven't been entirely wrong.
Of course, once you've done this once, you can just cp -r
some
working app to be a starting point for another app.
To make this easier, I've created a "starter" app for just that purpose. It has:
- a
templates
folder with abase.html
template and several child templates - a
static
folder with astyle.css
file - an
app.py
file that includes our boilerplate Flask app code, plus some initialization and example routes.
The base.html
template has some Flask/Jinja2 code to show flashed
messages in a default way.
The base.html
template also has an example of the HTML for a simple
a navbar. The CSS file makes it looks a little nicer, while still
being fairly simple.
Here's a screenshot of what starter app looks like, including the navbar:
Here's the HTML code for that page. Notice the use of url_for()
in
loading the CSS and creating the navbar.
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<!-- for mobile-friendly pages -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name=author content="">
<title>{{ page_title }}</title>
<link rel='stylesheet' href="{{url_for('static', filename = 'style.css')}}">
{% block head_stuff %} {% endblock %}
</head>
<body>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div id="messages">
{% for msg in messages %}
<p>{{msg}}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block nav %}
<nav>
<ul>
<li><a href="{{url_for('index')}}">home</a></li>
<li><a href="{{url_for('greet')}}">greet</a></li>
<li><a href="{{url_for('testform')}}">testform</a></li>
</ul>
</nav>
{% endblock %}
{% block main_content %}
<h1>Welcome!</h1>
{% endblock %}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
{% block end_scripts %}
{% endblock %}
</body>
</html>
Here's the code in app.py
that makes it work:
from flask import (Flask, render_template, make_response, url_for, request,
redirect, flash, session, send_from_directory, jsonify)
from werkzeug.utils import secure_filename
app = Flask(__name__)
# one or the other of these. Defaults to MySQL (PyMySQL)
# change comment characters to switch to SQLite
import cs304dbi as dbi
# import cs304dbi_sqlite3 as dbi
import random
app.secret_key = 'your secret here'
# replace that with a random key
app.secret_key = ''.join([ random.choice(('ABCDEFGHIJKLMNOPQRSTUVXYZ' +
'abcdefghijklmnopqrstuvxyz' +
'0123456789'))
for i in range(20) ])
# This gets us better error messages for certain common request errors
app.config['TRAP_BAD_REQUEST_ERRORS'] = True
@app.route('/')
def index():
return render_template('main.html',title='Hello')
@app.route('/greet/', methods=["GET", "POST"])
def greet():
if request.method == 'GET':
return render_template('greet.html', title='Customized Greeting')
else:
try:
username = request.form['username'] # throws error if there's trouble
flash('form submission successful')
return render_template('greet.html',
title='Welcome '+username,
name=username)
except Exception as err:
flash('form submission error'+str(err))
return redirect( url_for('index') )
@app.route('/formecho/', methods=['GET','POST'])
def formecho():
if request.method == 'GET':
return render_template('form_data.html',
method=request.method,
form_data=request.args)
elif request.method == 'POST':
return render_template('form_data.html',
method=request.method,
form_data=request.form)
else:
# maybe PUT?
return render_template('form_data.html',
method=request.method,
form_data={})
@app.route('/testform/')
def testform():
# these forms go to the formecho route
return render_template('testform.html')
@app.before_first_request
def init_db():
dbi.cache_cnf()
# set this local variable to 'wmdb' or your personal or team db
db_to_use = 'put_database_name_here_db'
dbi.use(db_to_use)
print('will connect to {}'.format(db_to_use))
if __name__ == '__main__':
import sys, os
if len(sys.argv) > 1:
# arg, if any, is the desired port number
port = int(sys.argv[1])
assert(port>1024)
else:
port = os.getuid()
app.debug = True
app.run('0.0.0.0',port)
Let me know if you'd like me to record a demo of it working.
If you know and prefer Bootstrap or any other way of doing your website, you are welcome to rip out that navbar and replace it with your own. If you are new to HTML and CSS and want to just use this one, you are welcome to do so.
Your project app is yours. It need not work or look like mine.