Vicente Louvet III

Daily Feeling Tracker App Development Guide

A simple micro-blogging app meant to be used to be used anonymously

Table of Contents

Features:

Project Summary

The purpose of this project is to create an example application that is actually pretty useful.

While it is a toy application, lacking features that would be required to make it a product it includes all the components of a normal web application (Frontend presentation, Backend app logic, Database to store records).

This application is used in a separate blog post “Dev Setup Guide”

Directory Setup

We will be using a basic folder structure from the Flask web framework [“Link”] (LINK).

Python Library Setup

Create a requirements.txt file at the project root (/dft) and enter the following:

flask
flask-sqlalchemy
sqlalchemy_utils
gunicorn

Next, use pip to install the required libraries

python3 -m pip install -r requirements.txt

Python Application code breakdown

Now we’re ready to start creating our app.py file which will receive requests from NGINX.

Create a file ‘app.py’ at the project root (/dft) and enter the following:

The first section of the file specifies all the python libraries to be imported.

import os
import datetime
from flask import Flask, request, render_template, redirect, flash, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_utils import database_exists
from sqlalchemy import text

Next, we set up the Flask application configuration parameters. We define a secret key, and a SQL database.

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
# configure the database
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////app/database/app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# create the database
db = SQLAlchemy(app)

In this next section, we define some basic database tables.

Each table includes a name, and one or more columns. The columns must be defined as a specific data type.

  1. The first table is ‘form_entry_tags’ which associates the ‘form entries’ table to the ‘tags’ table.
  2. The most basic table is ‘form_entries’ which stores the blog posts made by a user.
  3. Tags are the second most basic table, tags allow a user to add short labels to the form entry before submitting.
    • Form entries can have many tags.
  4. Categories are the next table, the category table stores the different feelings a user can select when making a form entry.
    • Form entries can have one, and only one category.
form_entry_tags = db.Table(
    "form_entry_tags",
    db.Model.metadata,
    db.Column("form_entries", db.Integer, db.ForeignKey("form_entry.id")),
    db.Column("tags", db.Integer, db.ForeignKey("tag.id")),
)


class Tag(db.Model):
    __name__ = "tags"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100))


class FormEntry(db.Model):
    __name__ = "form_entries"
    id = db.Column(db.Integer, primary_key=True)
    uastring = db.Column(db.String(200))
    message = db.Column(db.String(200))
    timestamp = db.Column(db.DateTime)
    tags = db.relationship("Tag", secondary=form_entry_tags)
    image = db.Column(db.LargeBinary)
    category_id = db.Column(db.Integer, db.ForeignKey("category.id"))
    category = db.relationship("Category", backref="form_entries")


class Category(db.Model):
    __name__ = "Category"
    id = db.Column(db.Integer, primary_key=True)
    label = db.Column(db.Integer, nullable=False)
    description = db.Column(db.String(200))

Next, we define a function that initializes the database.

Taking care to not overwrite the database if one exists.

Once the database is created, seed values are entered for some tables.

At the end of this section we call the init_db function we defined above.

def init_db():
    with app.app_context():
        if not database_exists(db.engine.url):
            db.create_all()
            default_categories = [("Happy", "You are feeling happy")]
            for label, description in default_categories:
                category_obj = Category(label=label, description=description)
                db.session.add(category_obj)
            db.session.commit()


init_db()

In this code block we define our first Flask application route.

This route accepts both GET and POST HTTP requests.

If the user sends a GET request, a Jinja template ‘form.html’ is rendered.

When POSTing data, a new entry is added to the SQLite3 database, then the browser is re-directed to the entry form.


@app.route("/", methods=["GET", "POST"])
def form():
    if request.method == "POST":
        # Load categories table from the database
        category = Category.query.filter_by(label=request.form["category"]).first()
        # Create the form entry
        entry = FormEntry(
            category=category,
            message=request.form["message"],
            timestamp=datetime.datetime.now(),
            uastring=request.headers.get("User-Agent"),
        )
        entry.image = request.files["image"].read()

        # Create a new tag
        tag_name = request.form.get("tags")
        tag = Tag.query.filter_by(name=tag_name).first()
        if not tag:
            tag = Tag(name=tag_name)
            db.session.add(tag)
        # Add the tag to the FormEntry's tags list
        entry.tags.append(tag)
        # Save the form entry to the database
        db.session.add(entry)
        db.session.commit()
        flash(f"{request.form['tags']}")
        return redirect(url_for("form"))
    else:
        categories = Category.query.all()
        return render_template("form.html", categories=categories)

Last of all, this code block tells flask to run the application, binding to all available IP addresses on the host, using port 5001

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

Back to top

Complete app.py Python code

import os
import datetime
from flask import Flask, request, render_template, redirect, flash, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_utils import database_exists
from sqlalchemy import text
#initialize an application object
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
# configure the database
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////app/database/app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# create the database
db = SQLAlchemy(app)
#define the database tables
form_entry_tags = db.Table(
    "form_entry_tags",
    db.Model.metadata,
    db.Column("form_entries", db.Integer, db.ForeignKey("form_entry.id")),
    db.Column("tags", db.Integer, db.ForeignKey("tag.id")),
)


class Tag(db.Model):
    __name__ = "tags"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100))


class FormEntry(db.Model):
    __name__ = "form_entries"
    id = db.Column(db.Integer, primary_key=True)
    uastring = db.Column(db.String(200))
    message = db.Column(db.String(200))
    timestamp = db.Column(db.DateTime)
    tags = db.relationship("Tag", secondary=form_entry_tags)
    image = db.Column(db.LargeBinary)
    category_id = db.Column(db.Integer, db.ForeignKey("category.id"))
    category = db.relationship("Category", backref="form_entries")


class Category(db.Model):
    __name__ = "Category"
    id = db.Column(db.Integer, primary_key=True)
    label = db.Column(db.Integer, nullable=False)
    description = db.Column(db.String(200))

# define the database initialization function
def init_db():
    with app.app_context():
        if not database_exists(db.engine.url):
            db.create_all()
            default_categories = [("Happy", "You are feeling happy")]
            for label, description in default_categories:
                category_obj = Category(label=label, description=description)
                db.session.add(category_obj)
            db.session.commit()

# initialize the database
init_db()

# define the root application route
@app.route("/", methods=["GET", "POST"])
def form():
    if request.method == "POST":
        # Load categories table from the database
        category = Category.query.filter_by(label=request.form["category"]).first()
        # Create the form entry
        entry = FormEntry(
            category=category,
            message=request.form["message"],
            timestamp=datetime.datetime.now(),
            uastring=request.headers.get("User-Agent"),
        )
        entry.image = request.files["image"].read()

        # Create a new tag
        tag_name = request.form.get("tags")
        tag = Tag.query.filter_by(name=tag_name).first()
        if not tag:
            tag = Tag(name=tag_name)
            db.session.add(tag)
        # Add the tag to the FormEntry's tags list
        entry.tags.append(tag)
        # Save the form entry to the database
        db.session.add(entry)
        db.session.commit()
        flash(f"{request.form['tags']}")
        return redirect(url_for("form"))
    else:
        categories = Category.query.all()
        return render_template("form.html", categories=categories)

# run the application, bind to all addresses on port 5001
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

Back to top

Jinja templating

Flask supports a templating language called Jinja to render static content.

One feature that Flask and Jinja provide is extending templates. This allows us to define a page layout in a ‘base.html’ file, then import elsewhere.

Create a file ‘base.html’ in the templates folder folder example: /dft/templates/base.html

The contents of the base.html file should be as follows:

base.html

Create a file ‘form.html’ in the templates folder folder example: /dft/templates/form.html

The template file should contain the following code:

form.html

As you can see, Jinja allows passing and rendering variables from the python code in HTML which is then consumed by a web browser.

The full documentation on Jinja can be found here.

Back to top

Test run the app

In a linux terminal, within the project directory /dft/, run the following command to start the flask application server:

python3 -m flask run

The flask application should start running.

But how does this work you might ask? The Flask application server looks for a file in the current working directory named app.py by default.

If you got this far, stop the flask server by pressing CTRL-C in the terminal and proceed to the last section.

Back to top

deploying our code

Previously, we used the built-in web server functionality provided by Flask to run our app. During development it can be useful

to use the built-in wsgi/http server available in flask - but it has known limitations. In this section we cover how to deploy our app

directly to a linux host using NGINX and gunicorn.

In general, a request will be sent by a web browser, received by NGINX, forwarded to gunicorn as a WSGI request, then processed by our Flask application.

A response would then travel in reverse order through these services before finally arriving back at the web browser. Each ‘layer’ in the ‘stack’

NGINX configuration

On Debian based Linux distributions, Nginx can be installed using the following apt command:

sudo apt install nginx

Once installed, the nginx server must be configured to proxy requests from the web browser to our WSGI application server.

Locate, or create, the default nginx configuration file (/etc/nginx/conf/nginx.conf) and open it in a text editor.

Replace the contents with the following:

events {}

http {
  upstream flask_app {
    server app:5001;
  }

  server {
    listen 5000;
    server_name localhost;
    client_max_body_size 10M;
    location / {
      proxy_pass http://flask_app;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
  }
}

Keep in mind this nginx configuration assumes the nginx server will run on the same host as the Gunicorn WSGI application.

save the file and restart the Nginx server with the following command:

sudo systemctl restart nginx

Back to top

Gunicorn

Gunicorn is a production grade WSGI server that features caching, worker pooling, among other features.

For a full list of features offered by Gunicorn visit the official site here.

the gunicorn wsgi server was installed in a previous step, but we can confirm it is installed with the following command:

python3 -m pip install gunicorn

from the top level project directory (/dft/) run the following command to run the gunicorn WSGI server to host our app.py application.

gunicorn -w 4 -b 0.0.0.0:5001 app:app

Explanation of command options:

With this command running, our python application is being served on port 5001 and should be accessible using a web browser.

Where do we go from here?

This blog will expand on the functionality of the Flask application in a separate post.

If you are interested to learn how to dockerize, deploy, and maintain this application on a production server, visit our docker blog post here.

Back to top