Routing

  • When Flask sees a URL it needs to call one of your functions.
  • The route decorator tells flask what do do.
  • The simplest form of route looks like this:
app = Flask(__name__) 
 
# This app is for http://www.myapp.com
 
@app.route('/') 
def index() :
  '''This function is called when the browser hits http://www.myapp.com/''' 
  return "Hello World"
  • Routes let you call different functions for different URLs

Here's an example that lets you say “Hello World” in different languages:

app = Flask(__name__)
 
@app.route('/') 
def index() :
  '''This function is called when the browser hits http://www.myapp.com/''' 
  return "Hello World"
 
@app.route('/spanish')
def sp_index() : 
  '''This function is called when the browser hits http://www.myapp.com/spanish''' 
  return "Hola Mundo"
 
@app.route('/chinese')
def ch_index() : 
  '''This function is called when the browser hits http://www.myapp.com/chinese''' 
  return "你好,世界"  
  • Routing is very powerful and flexible.
  • Routes let you pass arguments to functions
  • The arguments are derived from parts of the URL

Here's an example of a route taking an argument:

app = Flask(__name__) 
 
@app.route('/add/<item>')
def do_add(item) :
  '''This function is called when the browser hits any URL that starts with:
      http://www.myapp.com/add/
    If the URL is 
      http://www.myapp.com/add/foo
      The argument item will be "foo"'''
  return 'You called me the URL argument {}'.format(item)

Formatting HTML

  • The advantage of the format() function and f-strings is that they are easy!
  • Here are some useful features of the format() function.

The advanced use of the format() function is extremely useful for building web pages because there's lots of HTML that's boilerplate. Here's an example of a Flask program that produces a nice looking HTML page.

pretty.py
''' CIS-15 pretty.py 
Mike Matera 
 
This webapp presents the user with a pretty HTML page that's parameterizalbe. 
'''
 
from flask import Flask 
 
app = Flask(__name__)
 
pretty_html = '''
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{page_title}</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>  </head>
    <style>
    /* Space out content a bit */
    body {{
      padding-top: 1.5rem;
      padding-bottom: 1.5rem;
    }}
    </style>    
  <body>
    <div class="container">
      <div class="jumbotron">
        <h1 class="display-3">{main_heading}</h1>
        <p class="lead">{main_text}</p>
        <p><a class="btn btn-lg btn-success" href="#" role="button">Sign up today</a></p>
      </div>
      <footer class="footer">
        <p>&copy; {company_name} 2017</p>
      </footer>
    </div> <!-- /container -->
  </body>
</html>
'''
 
@app.route('/')
def index() :
    return pretty_html.format(
        page_title = "Another CIS-15 Example",
        project_name = "Mike's Awesome Website",
        main_heading = "Eggs, Milk and Sugar",
        main_text = "Making custard...",
        company_name = "M3, LLC."
        )
 
 
if __name__ == '__main__' :
    app.run(host='0.0.0.0', port=8080)
  • Does anything seem wrong here?
  • Most of our Python program is HTML and CSS
  • That makes things a bit of a pain to debug
  • Next we'll see how to fix this problem with Jinja templates

HTML Templates with Jinja2

It's best to separate your HTML and your Python code. Also, though Python format strings are powerful, there are things you might want to do in an HTML document that are too complicated to be done with format().

  • Templates keep your HTML code in a separate file
  • Templates allow you to change your HTML and code independently
  • Flask has built-in support for the Jinja2 template system.

Here's an example of the same program above in a Jina2 template:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="../../favicon.ico">
 
    <title>{{page_title}}</title>
 
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
 
    <!-- Optional theme -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
 
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>  </head>
 
    <style>
 
    /* Space out content a bit */
    body {
      padding-top: 1.5rem;
      padding-bottom: 1.5rem;
    }
 
    /* Everything but the jumbotron gets side spacing for mobile first views */
    .header,
    .marketing,
    .footer {
      padding-right: 1rem;
      padding-left: 1rem;
    }
 
    /* Custom page header */
    .header {
      padding-bottom: 1rem;
      border-bottom: .05rem solid #e5e5e5;
    }
 
    /* Make the masthead heading the same height as the navigation */
    .header h3 {
      margin-top: 0;
      margin-bottom: 0;
      line-height: 3rem;
    }
    </style>
 
  <body>
 
    <div class="container">
      <div class="header clearfix">
        <nav>
          <ul class="nav nav-pills float-right">
            <li class="nav-item">
              <a class="nav-link active" href="#">Home <span class="sr-only">(current)</span></a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">About</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">Contact</a>
            </li>
          </ul>
        </nav>
        <h3 class="text-muted">{{project_name}}</h3>
      </div>
 
      <div class="jumbotron">
        <h1 class="display-3">{{main_heading}}</h1>
        <p class="lead">{{main_text}}</p>
        <p><a class="btn btn-lg btn-success" href="#" role="button">Sign up today</a></p>
      </div>
 
      <div class="row marketing">
        {% for block in content %}
          <div class="col-lg-6">
            <h4>{{block[0]['title']}}</h4>
            <p>{{block[0]['content']}}</p>
          </div>
          <div class="col-lg-6">
            <h4>{{block[1]['title']}}</h4>
            <p>{{block[1]['content']}}</p>
          </div>
        {% endfor %} 
      </div>
 
      <footer class="footer">
        <p>&copy; {{company_name}} 2017</p>
      </footer>
 
    </div> <!-- /container -->
  </body>
</html>
  • The jinja2 template above a lot like a format string.
  • Notice the differences:
    • In a format string variables are surrounded with braces { and }
    • In Jinja they're surrounded with double braces and

Here's an example:

      <footer class="footer">
        <p>&copy; {{company_name}} 2017</p>
      </footer>
  • Jinja lets you use looping constructs.

Look at the loop in the template:

        {% for block in content %}
          <div class="col-lg-6">
            <h4>{{block[0]['title']}}</h4>
            <p>{{block[0]['content']}}</p>
          </div>
          <div class="col-lg-6">
            <h4>{{block[1]['title']}}</h4>
            <p>{{block[1]['content']}}</p>
          </div>
        {% endfor %} 
  • The loop iterates over the contents of the Python content variable.
  • Calling the Jinja template is almost exactly the same as calling the formatted string

Here's the code that executes this template:

templates.py
''' CIS-15 templates.py 
Mike Matera 
 
This webapp presents the user with a pretty HTML page that's parameterizalbe. 
'''
 
from flask import Flask, render_template
 
app = Flask(__name__)
 
@app.route('/')
def index() :
    return render_template('index.html',
        page_title = "Another CIS-15 Example",
        project_name = "Mike's Awesome Website",
        main_heading = "Eggs, Milk and Sugar",
        main_text = "Making custard...",
        company_name = "M3, LLC.",
        content = [ 
          [{
            'title'   : 'On Toast.',
            'content' : 'Custard makes it French toast.'
            },
          {
            'title'   : 'On Rice.', 
            'content' : 'Custard on rice is rice pudding.'
          }
          ], 
          [{
            'title'   : 'On Broccoli.',
            'content' : 'Cheese sauce is custard without egg, so not really custard. But, quiche as broccoli and that\'s custard!'
            },
          {
            'title'   : 'Dim Sum.', 
            'content' : 'Custard cups are my favorite desert dim sum.'
          }
          ], 
        ]
      )
 
 
if __name__ == '__main__' :
    app.run(host='0.0.0.0', port=8080)
  • Notice that the HTML is still ugly (because HTML is always ugly)
  • The Python is nice and clean
  • The content is passed in using an list.
    • It's a list of lists of hashes.
    • Each list element is a list
      • The sub-list has two elements (left side and right side)
      • The left and right side are a dictionary
        • The dictionary has 'title' and 'content' keys
  • Jinja searches for templates in the ./templates directory
  • You can change where Flask looks for templates
    • But they must be in a directory called templates

Here's an example that shows how to change the templates location in Flask:

from flask import Flask, render_template
 
app = Flask(__name__, template_folder='/path/to/my/templates') # Must end in templates!

HTML Forms

  • An HTML form is an HTML document that takes input from the user.
  • When the user presses “Submit” the form data is sent to the web server
  • Flask can handle form data
  • If you're new to HTML forms read this tutorial with examples

Here's an example of a simple form that could have been used for Project 3:

<html>
    <head>
        <title>Simple Form</title>
    </head>
    <body>
        <form action="/result" method="post">
          Value for the variable a:<br>
          <input type="text" name="a_value"><br>
          Value for the variable b:<br>
          <input type="text" name="b_value"><br><br>
          <input type="submit" value="Submit">
        </form>
    </body>
</html>
  • Notice the following about the form:
    • It's action field is set to /result
      • The forms action will be directed to <the-form-url>/result
      • The application must have a route to this URL
    • The method is set to post.
      • Submitting this form will create an HTTP POST request.
    • The text inputs are named a_value and b_value. Those will appear in the Python program
    • The last input is the Submit button.

Handling Form Data

  • Forms are submitted with HTTP POST requests.
  • The browser embeds the form data into the HTML request
  • In a GET request the form data is in the URL. For example:
  • POST requests are usually preferred because they keep data private.
  • The route has to be altered to make a function take POST data.
@app.route('/form', methods=['GET', 'POST'])
def do_form() :
  if request.method == 'GET' : 
    # No form data here. 
  else:
    # Handle form data

The form from the previous section is handled by the following Python function:

@app.route('/result', methods=['POST'])
def do_table():
    a = float(request.form['a_value'])
    b = float(request.form['b_value'])
    data = []
    data.append(['a', a])
    data.append(['b', b])
    data.append([f'{a}+{b}', a+b])
    return render_template('basic_answer.html', table_data=data)
  • Notice the following.
    • The form field values are accessible by the variable request.form which is a dictionary
    • The @app.route decorator has an additional argument methods=['POST']
      • That allows the function to handle post data.
      • The form would be rejected otherwise.

You need a function to render the form. That's shown below:

@app.route('/', methods=['GET'])
def do_form():
    return render_template('basic_form.html')

The code simply renders the form template, which is not parameterizable.

Storing Data

  • Web applications need a place to store data
  • There are different types of data storage:
    • Permanent storage
    • Session-based storage
    • Temporary storage (also called cache)
  • The programming model for different storage types is different

If your program needs to cache data you can simply use global variables in Python:

from flask import Flask
 
app = Flask(__name__)
 
# Global variable used for caching. 
counter = 0 
 
@app.route('/')
def index() :
    global counter
    counter += 1 
    return "The counter reads {}".format(counter)
  • The application will keep count as long as the server is running.
  • High performance applications need multiple servers
    • Each server will have a different count
    • The count may change from page load to page load
  • Caching is the most efficient and easy way to use data

You can also store data in a cookie the client's browser.

  • Cookies are data that your browser keeps for websites
  • Websites can set cookies in your browser when they want to save data
  • Cookies for a site are only accessible to that site (other sites can't see them)
  • Cookies are how your browser is “logged in” to sites that require autentication
  • Cookies are also how sites track you.

Flask can set and read cookies using the session object.

from flask import Flask, session
 
app = Flask(__name__)
 
# You must have a secret key to use cookies.
app.secret_key = 'asjf98s0d7ad8dfvahusicvnakjcx 8ds9ghvas8dvhs'
 
@app.route('/')
def index() :
    # Retrieve the counter (if it exists) 
    counter = 0
    if 'counter' in session : 
        counter = int(session['counter'])
 
    counter += 1 
 
    # Store the updated counter
    session['counter'] = counter
    return "The counter reads {}".format(counter)

There are some important things to notice here:

  • To use cookies your app must have a secret key.
    • No one can know the key or your app is vulnerable.
    • The key protects your cookie from being maliciously altered by a user
    • It's an error to not have a key if you use session cookies.
  • When the user first visits your site the cookie won't be set
  • Setting session['counter']] sets the cookie when the browser receives the page
  • Subsequent calls to your app will have the cookie set.
  • Each different user (really browser) has its own count
  • Session cookies are very useful for storing data like
    • The username of a logged in user
    • User preferences
  • Watch out!
    • Cookies can be cleared by the user
    • They expire

When you need permanent storage you must upgrade to using a database.

  • Databases are fast and powerful
  • They're robust
    • They are designed to survive hardware failure
    • They are the safest way to store data
    • They can provide consistency guarantees
  • They're also the most expensive way to store data
    • Database are good for things like
      • Preferences
      • Application working data
      • User information
      • Records and history
    • Databases are not very good for
      • Data files like images and video
      • Data that doesn't need to be backed up
  • There are many databases to work with
    • SQL databases are popular for web applications
    • Newer technology database like Google Datastore are becoming more popular

Here's a program that permanently stores the counter in a Google database.

from flask import Flask
 
from google.appengine.ext import ndb 
 
app = Flask(__name__)
 
# Datastore stores Python classes 
class Counter(ndb.Model) :
    count = ndb.IntegerProperty()
 
@app.route('/')
def index() :
 
    counter_objects = Counter.query().fetch()
    print (counter_objects)
    if len(counter_objects) == 0 :
        # The counter isn't in our database 
        counter = Counter(count = 0) 
    else :
        # There may be multiple counters, take the first one.
        counter = counter_objects[0]
 
    # Store the updated counter
    counter.count += 1        
    counter.put()
 
    return "The counter reads {}".format(counter.count)

Notice:

  • The counter is the same for everyone
  • The counter will always be consistent
    • No two people will ever see the same count
  • The code is somewhat more complicated!

Other databases have a somewhat different API but the concepts are essentially the same.

Code

The code from the examples can be downloaded here: week15.zip