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 %}
&copy; 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 named title, toc (short for table of contents) and content.

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 a base.html template and several child templates
  • a static folder with a style.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:

flask starter page

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.