Flask Webstack Framework

2 years ago | 29 December, 2021 | X minute read.

Introduction

This story series shares my framework for web applications using Python Flask as the backbone. It allows for advanced features to be added in as Python is very flexible.

An example of this would be for the Python application to be extended to scrape an external webpage for information or to get information from an API. With this data, the Flask web server can easily generate dynamic webpages using the included Jinja2 templating.

Another feature of my specific stack is the inclusion of Markdown editing of the static content on the webpages served. Both Jinja2 and markdown can be extended to provide useful tools (or macros) when creating the webpages. Examples of both are implemented in the main python file.

This first part describes the files needed to run the Flask website on your local machine. Part 2 extends the application by explaining how the application can be deployed to the cloud using Google App Engine (GAE).

Flask Development Web Stack Series: Part 1 of 2

You know this already?

Go To Part 2: Deployment to GAE

Page Contents

Directory Structure

The following shows the files we will be talking about and which together create the framework. Each file is described as we continue in this article.

app_directory/
  |-- main.py
  |-- assets/
      |-- css/
          |-- animate.min.css
          |-- bootstrap.min.css
          |-- custom.css
      |-- js/
          |-- bootstrap.bundle.min.js
          |-- clipboard.min.js
          |-- jquery-3.5.1.min.js
          |-- form.js
  |-- templates/
      |-- base.j2.html
      |-- home.j2.html
      |-- data.j2.html
      |-- article_01.j2.html
  |-- .gitignore

Download '.zip' of the files used in Part 1

Files and Code

The following files make up the directory structure above and are the basis for a new web project framework.

Main Python Flask Application

This is the brains of the whole operation. It is a Python script which uses a library called Flask to serve webpages. The code is well commented in terms of what each block does.

from flask import Flask, render_template, send_from_directory, send_file, request, redirect, url_for, jsonify, escape
from flaskext.markdown import Extension, Markdown, preprocessors
from datetime import datetime, timedelta
import os, re, sys
import requests

# THIS IS THE INFORMATION FOR WHERE TO ACCESS THE SITE WHEN RUNNING LOCALLY
host = "0.0.0.0" # represents 'this host'
port = 8880 

# USING THIS BASE PATH ENSURES THE ROOT FOR FILES IS THE APP ROOT
base_path = os.path.dirname(os.path.realpath(__file__))

# USEFUL FLAG WHEN BEHAVIOUR FOR LOCAL VS. DEPLOYMENT ON GOOGLE APP ENGINE
ON_GAE = bool(os.getenv('GAE_ENV', False))

# CREATE THE FLASK APP AND CONFIGURE IT
app = Flask(__name__)
app.config['TEMPLATES_AUTO_RELOAD'] = True # RELOAD TEMPLATES WHEN THEY CHANGE

# ADD FLASK REQUEST HANDLER
@app.route('/', defaults={'path': ''}, methods=['GET', 'POST'])
@app.route('/<path:path>', methods=['GET', 'POST'])
def index(path):

  # ----------------------------------------------------------------------------
  # PATHS WHICH RETURN TEMPLATED FILES (TEMPLATES IN DIRECTORY /templates/)
  # ----------------------------------------------------------------------------
  if path == '': # HOME PAGE
    return render_template('home.j2.html')

  if path.startswith("article/"): # ARTICLES: EG: /article/01
    return render_template(path.replace("article/","article_") +'.j2.html', article_id=path.replace("article/",""))

  if path.startswith("data"): # DATA DRIVEN PAGE: /data
    # Provide some data for the data page... this gets some data from a gist.
    json_gist = 'https://gist.githubusercontent.com/nasrulhazim/'
    json_gist += '54b659e43b1035215cd0ba1d4577ee80/raw/'
    json_gist += 'e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json'
    # PHEW! THATS A LONG URL!
    r = requests.get(json_gist)
    data = r.json()['quotes']
    return render_template("data.j2.html", data=data, source='https://gist.github.com/nasrulhazim/54b659e43b1035215cd0ba1d4577ee80')

  # ----------------------------------------------------------------------------
  # STATIC ASSET FILES (FILES IN DIRECTORY /assets/ ARE SERVED HERE)
  # ----------------------------------------------------------------------------
  if path.startswith("assets/"):
    # NOTE: THIS IS NEVER EXECUTED IN GAE DUE TO APP.YML CONFIG SERVING THIS 
    # OUTSIDE THE PYTHON APP
    return send_from_directory(base_path, path)

  # ----------------------------------------------------------------------------
  # FORM PROCESSOR
  # ----------------------------------------------------------------------------
  elif path == "api/form": # FORMS SHOULD POST JSON FORM DATA TO HERE
    # SEE FORM JAVASCRIPT FUNCTION
    data = request.form.to_dict()
    # DO SOMETHING WITH THE FORM DATA HERE.
    data["html-form-input-name-is-the-key"]
    resultStr = "success" if data["html-form-input-name-is-the-key"] else "fail"
    return json.dumps({'result':resultStr}), 200, {'Content-Type': 'application/json; charset=utf-8'}

  # ----------------------------------------------------------------------------
  # FALLBACK TO HOMEPAGE REDIRECT
  # ----------------------------------------------------------------------------
  return redirect('/') # REDIRECT TO HOME IF UNKNOWN PATH

# ENFORCE HTTPS (REDIRECT FROM HTTP) - ONLY ON APP ENGINE
@app.before_request
def force_https():
  if ON_GAE:
    if request.endpoint in app.view_functions and not request.is_secure:
      return redirect(request.url.replace('http://', 'https://')) # REDIRECT HANDLES HEADERS

# DATA PROVIDED TO ALL TEMPLATES
@app.context_processor
def inject_data():
  if ON_GAE:
    timeNow = datetime.now() + timedelta(hours=10)
    versionID = str(os.getenv('GAE_VERSION'))
    versionTimestamp = (datetime.fromtimestamp(int(int(os.getenv('GAE_DEPLOYMENT_ID'))/(2 << 27)))).strftime("%Y-%m-%d %H:%M")
  else:
    timeNow = datetime.now()
    versionID = "_DEV"
    versionTimestamp = (datetime.now() - timedelta(hours=10)).strftime("%Y-%m-%d %H:%M")
  return {
    'timeNow': timeNow,
    'pythonVers': "Python " + sys.version.split(" ")[0],
    'versionID': versionID,
    'versionTimestamp': versionTimestamp,
    'ON_GAE': ON_GAE
  }

# JINJA2 PROCESSOR
@app.context_processor
def alert_wrapper():
  def alert_formatter(content):
    # CAN BE USED TO PROVIDE DATA ON THE FLY TO A TEMPLATE.
    # FOR EXAMPLE SEE HOME TEMPLATE
    html = '<div class="alert alert-primary" role="alert">' + content + '</div>'
    return html
  return dict(alert_formatter=alert_formatter)

# MARKDOWN EXTENSION
class LineRefPreprocessor(preprocessors.Preprocessor):
  def run(self, lines):
    new_lines = []
    for line in lines:

      # [[now]] MACRO
      line = line.replace("[[now]]", datetime.now().strftime("%H:%M:%S, %d/%m/%Y"))

      new_lines.append(line)
    return new_lines
class SubstitutionExtension(Extension):
  def extendMarkdown(self, md, md_globals):
    # register
    md.preprocessors.register(LineRefPreprocessor(md),'line_subn', 0)

# INITIALISE MARKDOWN AS FLASK APP EXTENSION
mdengine = Markdown(app, extensions=[SubstitutionExtension(), 'fenced_code', 'attr_list', 'tables', 'toc'])

# START THE APP WHEN LOCAL (GAE LAUNCHES DIFFERENTLY)
if __name__ == '__main__':
  app.run(host=host, port=port, use_reloader=False, debug=False)

Jinja2 HTML Templating

Flask comes with the option to use the Jinja2 templating system to generate the HTML files on the fly. The advantage of this is that you can template the general format of the website, then build on that base template for each type of page.

The content can then be programmatically generated by the data that the Python Flask app provides. This means that any data can be used if you can create some logic for Python to access and work with it.

This is the base template:

<!doctype html>
<html lang="en">

  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!--  Favicons -->
    <link rel="icon" type="image/svg+xml" href="/assets/favicons/favicon.svg">
    <link rel="icon" type="image/png" href="/assets/favicons/favicon.png">

    <!-- Bootstrap CSS -->
    <link class="dark-light-css" rel="stylesheet" href="/assets/css/bootstrap.min.css">
    <!-- Animate CSS -->
    <link rel="stylesheet" href="/assets/css/animate.min.css"/>
    <!-- Custom CSS -->
    <link rel="stylesheet" href="/assets/css/custom.css">

    <!-- Page Title -->
    <title>{{title}}</title>

  </head>
  <body class="color-scheme-dark">

    <!-- header -->
    <header class="container py-4 border-bottom">
      <a href="/" title="Home"><img src="/assets/logoipsum.svg" /></a>
    </header>

    <!-- breadcrumbs navigation -->
    {%- if not type == "home" %}
    <div class="container py-3">
      <nav aria-label="breadcrumb">
        <small>
          <ol class="breadcrumb my-0">
            <li class="breadcrumb-item"><a href="/">Home</a></li>
            <li class="breadcrumb-item active" aria-current="page">{{title}}</li>
          </ol>
        </small>
      </nav>
    </div>
    {%- endif %}

    <!-- main page content -->
    <main class="py-3">
      <div class="container">

        <h1>Heading 1 in Base Template</h1>

        <div class="my-3" id="content">
        {% block page_content %}{% endblock %}
        </div>

      </div>
    </main>

    <!-- footer -->
    <footer class="container py-3 border-top">
      <p class="mb-1 small text-muted">
        Nullam libero orci, fringilla non lectus ut, aliquam laoreet libero. Phasellus eu consectetur enim. Morbi hendrerit in est eget condimentum.
      </p>
    </footer>

    <!-- jQuery -->
    <script src="/assets/js/jquery-3.5.1.min.js"></script>
    <!-- Boostrap JS Bundle -->
    <script src="/assets/js/bootstrap.bundle.min.js"></script>
    <!-- JQuery TOC Plugin -->
    <script src="/assets/js/jquery.toc.min.js"></script>
    <!-- clipboard.js Plugin -->
    <script src="/assets/js/clipboard.min.js"></script>

    <!-- form submission -->
    <script src="/assets/js/form.js"></script>
    <!-- Place following in template with form to use it -->
    <!-- <script>addFormProcessor("#formID")</script> -->

    {% block late_js %}{% endblock %}

  </body>
</html>

From the above base template, the following template extends (customises) it to generate a home page. The Flask app is setup to use this home template for home (/) requests.

{% set type = "home" %}{# home / article  #}
{% set title = "Home" %}

{% extends "base.j2.html" %}
{% block page_content %}

<h2 class='mb-4'>Page: Home (Heading 2)</h2>

<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent viverra lorem ac scelerisque finibus. In hac habitasse platea dictumst. Praesent placerat, enim et faucibus ullamcorper, velit nunc congue enim, a venenatis dolor libero eu ante. Nulla vestibulum, libero nec eleifend tincidunt, felis ipsum ornare neque, tincidunt euismod urna nulla in lectus. Vestibulum dapibus in eros nec finibus.
</p>

<h3>Other Demo Pages</h3>

<ul>
  <li><a href="/data">Page: Data</a></li>
  <li><a href="/article/01">Page: Article 01</a></li>
</ul>

<p>The next paragraph is an alert generated by a jinja2 pre-processor.</p>

{{ alert_formatter("ALERT MACRO TEXT") | safe }}

{% filter markdown %}

---
## Markdown Section - Heading 2

This is a sample home page.

The next paragraph has a dynamically driven date using a markdown extension.

[[now]]

Pellentesque non justo nec nunc efficitur molestie. Donec vitae ultricies nisi. Aliquam erat volutpat. Phasellus quam diam, porttitor a tempus vel, convallis vitae purus. Mauris mattis erat nec quam tristique fermentum. Morbi sed lacus pharetra leo consequat vulputate nec sed nisl. Sed non lobortis turpis, sit amet efficitur justo. Sed a mauris ut nibh mattis feugiat eget sit amet dui. Praesent quam purus, elementum sit amet ipsum iaculis, consequat volutpat orci.

Nullam libero orci, fringilla non lectus ut, aliquam laoreet libero. Phasellus eu consectetur enim. Morbi hendrerit in est eget condimentum. Suspendisse elementum imperdiet ligula sed luctus. Donec quis diam ut leo scelerisque tempus at a enim. Sed a metus risus. Curabitur ut sodales orci, sollicitudin lobortis erat. Morbi aliquet massa sollicitudin justo aliquam, nec scelerisque est fringilla. Suspendisse mollis lobortis maximus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris tristique mollis sapien. Proin posuere metus urna, sit amet ullamcorper risus accumsan vel.

End of Markdown

---
{% endfilter %} 
{% endblock %}

The following template extends the base template to display some data in a table. The Flask app is setup to use this data template for /data requests.

{% set type = "article" %}{# home / article  #}
{% set title = "Article 01" %}

{% extends "base.j2.html" %}

{% block page_content %}

<h2 class='mb-4'>Page: Article 01</h2>

{% filter markdown %}

### Heading 3 in MARKDOWN

Blah Blah Blah.

---
{% endfilter %}

<h3>Heading 3 in HTML</h3>
<p>Blah Blah Blah.</p>

{% endblock %}

The following template extends the base template to display an article written in markdown. The Flask app is setup to use this article template for /article/01 requests.

{% set type = "article" %}{# home / article  #}
{% set title = "Page Title" %}

{% extends "base.j2.html" %}
{% block page_content %}

{% filter markdown %}{% endfilter %}

<h2 class='mb-4'>Page: Data</h2>

<p>Data from <a href='{{source}}'>this Gist</a>.</p>

<table class="table">
<thead>
<tr><th>Author</th><th>Quote</th></tr>
</thead>
<tbody>
  {% for item in data %}
    <tr><td>{{item.author}}</td><td>{{item.quote}}</td></tr>{% endfor %}
</tbody>
</table>

{% endblock %}

Static Assets

In the assets/ directory, CSS and Javascript files as well as logo images or favicons can be stored and served.

Part 2 describes how when the app is deployed to the cloud, App.yaml can be configured to serve the assets rather than the Python code.

The following are what I have in the blank framework and where they are from.
Alternatively, CDNs can be used for each rather than self-serving them from the assets directory.

Asset File Description Webpage Alternative CDN
assets/css/animate.min.css CSS Animations Link <link href="https://cdn.jsdelivr.net/npm/[email protected]/animate.min.css" rel="stylesheet" integrity="sha256-H+1wJGK9cAi4t7z8xI8ra5XMwxzIjVFTp33LcTE/Xmo=" crossorigin="anonymous">" />
assets/css/bootstrap.min.css Bootstrap 5 (CSS) Link <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
assets/js/bootstrap.bundle.min.js Bootstrap 5 (JS) Link <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
assets/js/clipboard.min.js Copy To Clipboard Link <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js" integrity="sha256-Eb6SfNpZyLYBnrvqg4KFxb6vIRg+pLg9vU5Pv5QTzko=" crossorigin="anonymous"></script>
assets/js/jquery-3.5.1.min.js jQuery Library Link <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

Custom JavaScript

This JavaScript is how form data can be captured using jquery and submitted to the api/form endpoint for processing by the Python / Flask app.
An example of how to recieve this form data is in the "form processor" part of main.py.

This might be used to process a contact form and send an email to the site owner via an external service such as MailJet.

// this should be triggered on a form submission event

function addFormProcessor(form_selector) {

  $(form_selector).submit(function( event ) {
    event.preventDefault();

    // this requires the form inputs to have "name" attributes set as they are 
    // used as the keys in the json object when sent to the API endpoint
    data = $(form_selector).serialize();

    $.ajax({
      type: "POST",
      url: "/api/form",
      data: data,
      dataType: "json"
    }).done(function(data) {
      if (data.result == "success") {
        console.log("Form data sent and processed.")
        // show it somehow to the user?
      } else {
        console.log("Form data sending failed.")
        // show it somehow to the user?
      }
    });
  });
}

Git (Change Management)

I recommend managing changes using Git. There is a lot of readily available information about this, so I won't go into it, however I basically just exclude my /archive/ directory in which I place superseded or removed files which I still like to be able to check from time to time. This technically wouldn't be needed with more proficient use of Git!

I also exclude all the standard things you need to for Python (see the comment in the example below).

archive/

# PYTHON GITIGNORE: I ADD THE SUGGESTED PYTHON RELATED GITIGNORE BELOW HERE. 
# AVAILABLE AT: https://github.com/github/gitignore/blob/main/Python.gitignore

Running the Webserver Locally

Download '.zip' of the files used in Part 1

  1. Ensure you are in the directory of the app (where main.py is).
  2. Start the Python app using the command python3 main.py.

Next Steps & Resources

Part 2 describes how the application can be deployed to the cloud (using Google App Engine).

  • Additional Jinja2 templating information is available here. Look especially at 'Macros' which are really cool.
  • Bootstrap 5 docs are available here.
  • Additional Markdown Extension resources are available here.
  • A good cheat sheet for using Git is available here from GitHub.



BOOTSTRAP, FLASK, FRAMEWORK, JINJA2, JQUERY, MARKDOWN, PYTHON, STACK, TEMPLATE, WEB