Flask Notebook

Last Update: 04.24.18

Notes from the Flask Mega-Tutorial

Chapter 1: Hello, World!

Installing Flask

We install Flask with pip

pip install flask

Virtual Environment

  • To create a virtual environment, the following command need to be executed in the root folder
  • Every time I want to get inside the virtual environment, need to execute
source venv/bin/activate

A "Hello, World" Flask Application

  • To launch an application
flask run

Package

  • Folder with an __init__.py file in it
  • The init file is read and define what is done with the package
  • Need to have an additional file at the top-level that defines the Flask application instance.
from app import app 
  • We need to set the FLASK_APP environment variable with the location of that file
(venv) $ export FLASK_APP=microblog.py

Ports

  • By default Flask use the port 5000
  • Applications deployed on production web servers typically listen on port 443
    • Sometimes 80 if they do not implement encryption,
    • Access to these ports require administration rights

Chapter 2: Templates

Mock

  • Creating mock objects is a useful technique that allows you to concentrate on one part of the application without having to worry about other parts of the system that don't exist yet. E.g.: mock a user:
user = {'username': 'Miguel'}

Templates

  • Help to separate the presentation and the business logic
  • Written as separate files, stored in a app/templates folder inside the application package.
  • We use from flask import render_template in the routes.py file in order to use those templates
  • The operation that converts a template into a complete HTML page is called rendering.
  • The render_template() function invokes the Jinja2 template engine that comes bundled with the Flask framework.
  • Resources: Flask Mega-Tutorial Part II

Conditional Statements

  • Templates support control statements, given inside {% ... %} blocks.
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog!</title>
{% endif %}

Loops

  • We define loops with {% for ... %} and {% endfor %}
{% for post in posts %}
<div><p>{{ post.author.username}} says:<b>{{ post.body }}</b></p></div>
{% endfor %}

Inheritance

  • If a or multiple elements repeat between pages we can create a level above
  • The page that need extensions will have {% extends "page.html" %} to precise what will extend it, and {% block nameOfBlock %} {% end nameOfBlock %} to show the extent of the addition
  • The extension page will have {% block nameOfBlock %} {% end nameOfBlock %}
  • With the extend statement, Jinja2 knows that when it is asked to render a page it needs to embed it inside another.
 <body>
        <div>Microblog: <a href="/index">Home</a></div>
        <hr>
        {% block content %}{% endblock %}

Chapter 3: Web Forms

Configuration

  • We can save configuration variable for the framework
  • A best practice is to set it in Class for ease of access, using a config.py file
import os

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
  • The SECRET_KEY configuration variable is very important as it's enable Flask to generate a cryptographic key
  • We call the config file from the __init__.py file with the app.config.from_object() method

User Login Form

  • Most Flask extensions use a flask_ naming convention for their top-level import symbol. In this case, Flask-WTF has all its symbols under flask_wtf
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password',vaildators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')  

User Login Template

  • Template expect the objects created before on the LoginForm class to be used as arguments
  • The following login view function will provide those
  • In the <form> element, the empty attribute action is used to submit the form to the current page
  • The method="post" attribute is preferred to get as the submission data go through the body of the request and not inside the URL, which would clutter it
  • To create a token we use form.hidden_tag(). Protect versus CSRF attacks.

Form Views

  • We need the to import the LoginForm class from forms.py, instantiate an object and send it to the template
  • My words. We created first a series of variable needed to login with app/forms.py. We instantiate a version of it (form). We use that instance and push it to our template also called form form = form
from flask import render_template
from app import app
from app.forms import LoginForm

# ...

@app.route('/login')
def login():
    form = LoginForm()
    return render_template('login.html', title='Sign In', form=form)

Receiving Form Data

  • Need to add a decorator to @app.route in the form of a method giving the ability to accept Get (default) and Post
  • Normally GET requests are those that return information to the client // POST requests are typically used when the browser submits form data to the server
  • form.validate_on_submit is the heavy load. When the page is initially requested, it receive a get from the browser. That function will reply false and the template with form will render normally.
  • But when, the user will POST, it's going to gather the data, check if it's ok with the validators and reply True.
  • The flash() function is used to show a message to the user
  • Tne redirect() functions instructs the browser where to go post-login.
@app.route('/login', methods=['GET','POST'])
def login():
	form = LoginForm()
	if form.validate_on_submit():
		flash('Login requested for user {}, remember_me={}'.format(form.username.data, form.remember_me.data))
		return redirect('/index')
	return render_template('login.html', title='Sign In', form=form)
  • To show the message in the base template, we use the construct with. I assign this way the result of calling get_flashed_messages() to a messages variable
{% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}

Improving Field Validation

  • To alert the user that their data is not valid, we add the flash messages error and their explanations in the app/login.html file.
<p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
  • One problem with writing links directly in templates and source files => reorganize your links, need to search and replace these links in your entire application.
  • For better control, Flask provides url_for()
  • url are more likely to change than function names which are internal
 <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('login') }}">Login</a>
    </div>
  • In the routes.py file
from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
    # ...

Source: (Flask Mega-Tutorial)

Chapter 4: Database

Databases in Flask

  • SQLAlchemy is an Object Relational Mapper or ORM.
  • ORMs allow applications to manage a database using high-level entities such as classes, objects and methods instead of tables and SQL.
  • The job of the ORM is to translate the high-level operations into database commands.
(venv) $ pip install flask-sqlalchemy

Database Migrations

  • Flask-Migrate Flask wrapper for Alembic, a database migration framework for SQLAlchemy
(venv) $ pip install flask-migrate

Flask-SQLAlchemy Configuration

  • SQLite databases are the most convenient choice for developing small applications, sometimes even not so small ones
  • Each database is stored in a single file on disk and there is no need to run a database server like MySQL and PostgreSQL
import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    # ...
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
  • Taking the database URL from the DATABASE_URL environment variable, and if that isn't defined, configuring a database named app.db
  • app.db located in the main directory of the application, which is stored in the basedir variable.
  • The database is going to be represented in the application by the database instance.
  • The database migration engine will also have an instance.
  • These are objects that need to be created after the application, in the app/__init__.py file
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

from app import routes, models
  • db object that represents the database.
  • Another object that represents the migration engine.
  • New module called models at the bottom. This module will define the structure of the database.

Database Models

  • id field mandatory for all models as the primary key
  • username, email, password_hash are varchar
  • password_hash to maintain security level
  • Models are setup in app/models.py file
from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))

    def __repr__(self):
        return '<User {}>'.format(self.username)    
  • Fields are created as instances of the db.Column class
  • The __repr__ method tells Python how to print objects of this class, which is going to be useful for debugging
>>> from app.models import User
>>> u = User(username='susan', email='susan@example.com')
>>> u
<User susan>

Creating The Migration Repository

  • Alembic (the migration framework used by Flask-Migrate) enables schema changes in a way that does not require the database to be recreated from scratch
  • It maintains a migration repository, a directory in which it stores its migration scripts
  • Each time a change is made to the database schema, a migration script is added to the repository with the details of the change
  • To apply the migrations to a database, these migration scripts are executed in the sequence they were created
  • The flask db sub-command is added by Flask-Migrate to manage everything related to database migrations
  • Create the migration repository for microblog by running flask db init:
(venv) $ flask db init
  Creating directory /home/miguel/microblog/migrations ... done
  Creating directory /home/miguel/microblog/migrations/versions ... done
  Generating /home/miguel/microblog/migrations/alembic.ini ... done
  Generating /home/miguel/microblog/migrations/env.py ... done
  Generating /home/miguel/microblog/migrations/README ... done
  Generating /home/miguel/microblog/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '/home/miguel/microblog/migrations/alembic.ini' before proceeding.
  • After you run this command, you will find a new migrations directory, with a few files and a versions sub-directory inside.
  • All these files should be added to source control.

The First Database Migration

  • Two ways to create a database migration: manually or automatically.
  • To generate a migration automatically, Alembic compares the database schema as defined by the database models, against the actual database schema currently used in the database.
  • It then populates the migration script with the changes necessary to make the database schema match the application models
  • In this case, since there is no previous database, the automatic migration will add the entire User model to the migration script. The flask db migrate -m "users table" sub-command generates these automatic migrations
(venv) $ flask db migrate -m "users table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
  Generating /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done
  • The flask db migrate command does not make any changes to the database, it just generates the migration script.
  • To apply the changes to the database, the flask db upgrade command must be used.
(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> e517276bb1c2, users table
  • With SQLite, upgrade detects no db so create one
  • With MySQL or PostgreSQL, create database first before upgrade
  • Note: Flask-SQLAlchemy uses "snake case" naming convention

Database Upgrade and Downgrade Workflow

  • To upgrade, first create the new migrations scripts with flask db migrate after changing the model
  • Then run flask db upgrade
  • In prod, just update the migration scrips created in dev by flask db migrate then run the flask db upgrade command.
  • flask db downgrade exists also. After downgrade need to delete the migration script and generate a new one.

Database Relationship

  • Relational databases are good at storing relations between data items
from datetime import datetime
from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    posts = db.relationship('Post', backref='author', lazy='dynamic')

    def __repr__(self):
        return '<User {}>'.format(self.username)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.String(140))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    def __repr__(self):
        return '<Post {}>'.format(self.body)
  • Post class will represent posts written by users
  • timestamp is indexed to retrieve easily post in chronological order
    • In general, you will want to work with UTC dates and times in a server application.
  • user_id is a foreign key linked to user.id
  • the post field in the User class is not an actual database field
    • It's an high-level view of the relationship between users and posts
    • for example, if I have a user stored in u, the expression u.posts will run a database query that returns all the posts written by that user

Testing the database (Play Time)

  • Import the database instance and the models
>>> from app import db
>>> from app.models import User, Post
>>> u = User(username='john', email='john@example.com')
>>> db.session.add(u)
>>> db.session.commit()
  • Changes to a database are done in the context of a session
  • Sessions are accessed with db.session
  • You can do multiple changes in a session, that you commit with db.session.commit()
  • db.session.rollback() will abort the session and remove any changes stored in it
  • Sessions guarantee that the database will never be left in an inconsistent state.
>>> u = User(username='susan', email='susan@example.com')
>>> db.session.add(u)
>>> db.session.commit()
>>> users = User.query.all()
>>> users
[<User john>, <User susan>]
>>> for u in users:
...     print(u.id, u.username)
...
1 john
2 susan
  • The query attribute is the entry point to run database queries.
  • query.all() returns all elements of that class
>>> u = User.query.get(1)
>>> u
<User john>
  • With get and knowing the id, we can retrieve specific data
>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)
>>> db.session.add(p)
>>> db.session.commit()
  • I assign an author to a post using the author virtual field instead of having to deal with user IDs
  • SQLAlchemy is great it provides a high-level abstraction over relationships and foreign keys
  • Other examples
>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]

>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]

>>> # print post author and body for all posts 
>>> posts = Post.query.all()
>>> for p in posts:
...     print(p.id, p.author.username, p.body)
...
1 john my first post!

# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User susan>, <User john>]
>>> users = User.query.all()
>>> for u in users:
...     db.session.delete(u)
...
>>> posts = Post.query.all()
>>> for p in posts:
...     db.session.delete(p)
...
>>> db.session.commit()

Shell Context

  • While you work on your application, you will need to test things out in a Python shell very often
  • The shell command is the second "core" command implemented by Flask, after run
  • To start a Python in the interpreter in the context of the application
(venv) $ python
>>> app
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined
>>>

(venv) $ flask shell
>>> app
<Flask 'app'>
  • First command doesn't work because the app is not imported
  • flask shell pre-imports the application instance app
  • You can add a list of symbols to pre-import
  • In microblog.py:
from app import app, db
from app.models import User, Post

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post': Post}
  • After you add the shell context processor function you can work with database entities without having to import them:
(venv) $ flask shell
>>> db
<SQLAlchemy engine=sqlite:////Users/migu7781/Documents/dev/flask/microblog2/app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>

Chapter 5: User Logins

Password Hashing

  • The packages that implement password hashing is Werkzeug
    • Output of pip when you install Flask, since it is one of its core dependencies
>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'

The verification process is done with a second function from Werkzeug, as follows:

>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, 'foobar')
True
>>> check_password_hash(hash, 'barfoo')
False
  • We implement the hashing logic in the user model:
from werkzeug.security import generate_password_hash, check_password_hash

# ...

class User(db.Model):
    # ...

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
  • A user object is now able to do secure password verification, without the need to ever store original passwords
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('mypassword')
>>> u.check_password('anotherpassword')
False
>>> u.check_password('mypassword')
True

Introduction to Flask-Login

  • Flask-Login manages the user logged-in state
    • Users can log in to the application and then navigate to different pages while the application "remembers" that the user is logged in.
    • It also provides the "remember me" functionality that allows users to remain logged in even after closing the browser window
(venv) $ pip install flask-login

As with other extensions, Flask-Login needs to be created and initialized right after the application instance in app/__init__.py.

# ...
from flask_login import LoginManager

app = Flask(__name__)
# ...
login = LoginManager(app)

# ...

Preparing The User Model for Flask-Login

  • Flask-Login works with the app user model
  • Four required items:
    1. is_authenticated: boolean depending on valid credentials
    2. is_active: boolean depending on active user account or not
    3. is_anonymous: boolean
    4. get_id(): method that returns a unique identifier for the user as a string
  • A mixing class called UserMixin implement them in app/models.py
# ...
from flask_login import UserMixin

class User(UserMixin, db.Model):
    # ...

User Loader Function

  • Flask-Login keeps track of the logged in user by storing its unique identifier in Flask's user session
  • Because Flask-Login knows nothing about databases, it needs the application's help in loading a user.
    • The extension expects that the application will configure a user loader function, that can be called to load a user given the ID.
    • This function can be added in the app/models.py module:
from app import login
# ...

@login.user_loader
def load_user(id):
    return User.query.get(int(id))
  • The user loader is registered with Flask-Login with the @login.user_loader decorator.
    • The id that Flask-Login passes to the function as an argument is going to be a string, so databases that use numeric IDs need to convert the string to integer as you see above.

Logging Users In

  • Modify the app/routes.py page to have our login view function to take in account our user database and generate/verify hashes
# ...
from flask_login import current_user, login_user
from app.models import User

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title='Sign In', form=form)
  • The two first line in the login() function prevent an already logged user to see the login page.
    • The current_user variable comes from Flask-Login to obtain the user object that represents the client of the request
    • The first() method is another commonly used way to execute a query, when you only need to have one result.
    • the check_password() method will take the password hash stored with the user and determine if the password entered in the form matches the hash or not
    • If the username and password are both correct, then I call the login_user() function, which comes from Flask-Login.

Logging Users Out

  • Flask-Login has a logout_user() function that we can use in the routing file
# ...
from flask_login import logout_user

# ...

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))
  • We need to show the link to the user, to be able to logout when they are logged-in.
  • We will edit the base.html template with a conditional in that purpose
 <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        {% if current_user.is_anonymous %}
        <a href="{{ url_for('login') }}">Login</a>
        {% else %}
        <a href="{{ url_for('logout') }}">Logout</a>
        {% endif %}
    </div>

Requiring Users To Login

  • Flask-Login can forces users to log in before they can view certain pages
  • To set it up, Flask-Login needs to know the view function that handles login.
  • We can add that in app/__init__.py
# ...
login = LoginManager(app)
login.login_view = 'login'
  • The 'login' value above is the function (or endpoint) name for the login view.
    • In other words, the name you would use in a url_for() call to get the URL
  • To force login we add a decorator called @login_required.
    • You add this decorator to a view function below the @app.route decorators from Flask,
    • the function becomes protected and will not allow access to users that are not authenticated.
from flask_login import login_required

@app.route('/')
@app.route('/index')
@login_required
def index():
    # ...
  • We have to implement the redirect back from the successful login to the page the user wanted to access (so they don't need to login again)
  • When a user that is not logged in accesses a view function protected with the @login_required decorator, the decorator is going to redirect to the login page, but it is going to include some extra information in this redirect so that the application can then return to the first page.
    • If the user navigates to /index, for example, the @login_required decorator will intercept the request and respond with a redirect to /login, but it will add a query string argument to this URL, making the complete redirect URL /login?next=/index. The next query string argument is set to the original URL, so the application can use that to redirect back after login.
from flask import request
from werkzeug.urls import url_parse

@app.route('/login', methods=['GET', 'POST'])
def login():
    # ...
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)
    # ...
  • Once the user is logged in we retrieve next
  • Three possibles cases:
    1. No next arg -> index page
    2. next arg set to relative page -> page in next
    3. next arg set to domain name -> index page
      • To prevent URL attack to a malicious site. url_parse() check if netloc is set or not

Showing The Logged In User in Templates

  • We pass current_user instead of the previous mock user we created initially
{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}
  • Do not forget to delete the user hash and attribute in the route
@app.route('/')
@app.route('/index')
def index():
    # ...
    return render_template("index.html", title='Home Page', posts=posts)
  • To test with a user, we can add it directly to the database and commit it.
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()

User Registration

  • Create a registration form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User

# ...

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')
  • Second validator added to check email validity
  • Two password fields, the second checking if it's similar to the first
  • Two new methods, validate_username() and validate_email() to be sure that username and email don't already exist in the database
  • To display page we need an HTML template
{% extends "base.html" %}

{% block content %}
    <h1>Register</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}
  • Add a link to the registration form in the login page
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
  • Add view function route to register form
from app import db
from app.forms import RegistrationForm

# ...

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)
  • Check if user not already logged-in
  • Logic in if validate_on_submit() conditional to create a new user, write it to the database then redirect to login prompt

Sources: Flask Mega-tutorial Part V

Chapter 6: Profile Page and Avatars

User Profile Page

  • Write a view function that maps to the /user/ URL
@app.route('/user/<username>')
@login_required
def user(username):
    user = User.query.filter_by(username=username).first_or_404()
    posts = [
        {'author': user, 'body': 'Test post #1'},
        {'author': user, 'body': 'Test post #2'}
    ]
    return render_template('user.html', user=user, posts=posts)
  • Create an @app.route decorator with a dynamic component: <username>
  • first_or_404() used to send a 404 error back to client when no username
  • Create user.html template
{% extends "base.html" %}

{% block content %}
    <h1>User: {{ user.username }}</h1>
    <hr>
    {% for post in posts %}
    <p>
    {{ post.author.username }} says: <b>{{ post.body }}</b>
    </p>
    {% endfor %}
{% endblock %}
  • We modify base.html to add a link to the profile
<div>
      Microblog:
      <a href="{{ url_for('index') }}">Home</a>
      {% if current_user.is_anonymous %}
      <a href="{{ url_for('login') }}">Login</a>
      {% else %}
      <a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
      <a href="{{ url_for('logout') }}">Logout</a>
      {% endif %}
    </div>
  • url_for is called to generate the correct link to the profile

Avatar

  • We retrieve images from Gravatar with a MD5 hash
>>> from hashlib import md5
>>> 'https://www.gravatar.com/avatar/' + md5(b'john@example.com').hexdigest()
'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'
  • Default 80x80 pixels, s argument for precise size
https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35?s=128
  • Change default icon for non Gravatar registered users d
  • Add Gravatar to the User model
from hashlib import md5
# ...

class User(UserMixin, db.Model):
    # ...
    def avatar(self, size):
        digest = md5(self.email.lower().encode('utf-8')).hexdigest()
        return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
            digest, size)
  • avatar() method returns user's avatar image scaled to requested size
  • Users with no avatar will have an identical image generated
  • MD5 support in Python is in bytes. Convert string with encode().
  • Insert Gravatar in User profile
{% extends "base.html" %}

{% block content %}
    <table>
        <tr valign="top">
            <td><img src="{{ user.avatar(128) }}"></td>
            <td><h1>User: {{ user.username }}</h1></td>
        </tr>
    </table>
    <hr>
    {% for post in posts %}
    <p>
    {{ post.author.username }} says: <b>{{ post.body }}</b>
    </p>
    {% endfor %}
{% endblock %}
  • To change avatar source, only need to rewrite the avatar() method
  • Add little avatars to each post
{% extends "base.html" %}

{% block content %}
    <table>
        <tr valign="top">
            <td><img src="{{ user.avatar(128) }}"></td>
            <td><h1>User: {{ user.username }}</h1></td>
        </tr>
    </table>
    <hr>
    {% for post in posts %}
    <table>
        <tr valign="top">
            <td><img src="{{ post.author.avatar(36) }}"></td>
            <td>{{ post.author.username }} says:<br>{{ post.body }}</td>
        </tr>
    </table>
    {% endfor %}
{% endblock %}

Using Jinja2 Sub-Templates

  • To repeat same snippets of code on different templates
  • We prefix the snippet with _. Example of _post.html:
    <table>
        <tr valign="top">
            <td><img src="{{ post.author.avatar(36) }}"></td>
            <td>{{ post.author.username }} says:<br>{{ post.body }}</td>
        </tr>
    </table>
  • We reference it with {% include ...}
{% extends "base.html" %}

{% block content %}
    <table>
        <tr valign="top">
            <td><img src="{{ user.avatar(128) }}"></td>
            <td><h1>User: {{ user.username }}</h1></td>
        </tr>
    </table>
    <hr>
    {% for post in posts %}
        {% include '_post.html' %}
    {% endfor %}
{% endblock %}

More Interesting Profiles

  • Add a free string field for users in the model
class User(UserMixin, db.Model):
    # ...
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)
  • Database is changed so need a new migration script
(venv) $ flask db migrate -m "new fields in user model"
  • Apply change to the database
(venv) $ flask db upgrade
  • Add new fields in user profile template
{% extends "base.html" %}

{% block content %}
    <table>
        <tr valign="top">
            <td><img src="{{ user.avatar(128) }}"></td>
            <td>
                <h1>User: {{ user.username }}</h1>
                {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
                {% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}
            </td>
        </tr>
    </table>
    ...
{% endblock %}

Recording The Last Visit Time For a User

  • Write current time whenever user sends a request in last_seen
  • Decorator @before_request to single-out the function to execute before the view function
from datetime import datetime

@app.before_request
def before_request():
    if current_user.is_authenticated:
        current_user.last_seen = datetime.utcnow()
        db.session.commit()
  • Good practice to use UTC time zone
  • When Flask-Login invoke the user, a database query will put it in a database session. No need to db.session.add()
  • During debug, delete necessary parts, commenting might not work with interpreters

Profile Editor

  • Form class for entry for users to enter their about me, in the Forms.py file
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length

# ...

class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
    submit = SubmitField('Submit')
  • TextAreaField is a multi-line box
  • Create an edit_profile.html template
{% extends "base.html" %}

{% block content %}
    <h1>Edit Profile</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.about_me.label }}<br>
            {{ form.about_me(cols=50, rows=4) }}<br>
            {% for error in form.about_me.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}
  • Edit the routes.py file to tie template, model and database
from app.forms import EditProfileForm

@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm()
    if form.validate_on_submit():
        current_user.username = form.username.data
        current_user.about_me = form.about_me.data
        db.session.commit()
        flash('Your changes have been saved.')
        return redirect(url_for('edit_profile'))
    elif request.method == 'GET':
        form.username.data = current_user.username
        form.about_me.data = current_user.about_me
    return render_template('edit_profile.html', title='Edit Profile', form=form)
  • validate_on_submit() can return false because
    • GET request was sent by the browser, so fill the form with the data available in database
    • POST was sent but data in invalid
    • To access it, link in the profile page
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
{% endif %}
  • Conditional so another user navigate a profile which is not its own don't see the edit button

Chapter 7 : Error Handling

Error Handling in Flask

  • Stack trace shows sequence of calls to the error line
  • User don't see any details

Debug Mode

  • Access to a nice debugger directly in browser
(venv) $ export FLASK_DEBUG=1
  • Ability to expand each stack frame and see the source code
  • Can open a Python prompt on any frame to execute expressions
    • A PIN number is necessary as an additional security feature
  • The application is restarting every time the source code is changed

Custom Error Pages

  • @errorhandler decorator
  • Dedicated app/errors.py module to manage them
from flask import render_template
from app import app, db

@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template('500.html'), 500
  • Similar to view functions
  • Second value returned is the error code number
  • Rollback forced in case of error 500 which are database errors
  • Creation of a 404 page
{% extends "base.html" %}

{% block content %}
    <h1>File Not Found</h1>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}
  • Creation of a 500 page
{% extends "base.html" %}

{% block content %}
    <h1>An unexpected error has occurred</h1>
    <p>The administrator has been notified. Sorry for the inconvenience!</p>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

Sending Errors by Email

  • Add email server details to the config.py configuration file
class Config(object):
    # ...
    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    ADMINS = ['your-email@example.com']
  • logging package is used to write logs and send them by mail
  • Needed to add a SMTPHandle to the logger instance app.logger
import logging
from logging.handlers import SMTPHandler

# ...

if not app.debug:
    if app.config['MAIL_SERVER']:
        auth = None
        if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
            auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
        secure = None
        if app.config['MAIL_USE_TLS']:
            secure = ()
        mail_handler = SMTPHandler(
            mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
            fromaddr='no-reply@' + app.config['MAIL_SERVER'],
            toaddrs=app.config['ADMINS'], subject='Microblog Failure',
            credentials=auth, secure=secure)
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)
  • Email logger activated only out of debug mode
  • Test with SMTP debugging server from Python. Fake server which accepts emails, print them to console
(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025
  • Variables to export MAIL_SERVER=localhost and MAIL_PORT=8025
  • The mock email containing the error message is printed to the console
  • To use a real email server, i.e. Gmail
export MAIL_SERVER=smtp.googlemail.com
export MAIL_PORT=587
export MAIL_USE_TLS=1
export MAIL_USERNAME=<your-gmail-username>
export MAIL_PASSWORD=<your-gmail-password>
  • Note: the username and not the full email address

Logging to a File

  • Import another handler, RotatingFileHandler to the application logger
  • Similar to the email handler
# ...
from logging.handlers import RotatingFileHandler
import os

# ...

if not app.debug:
    # ...

    if not os.path.exists('logs'):
        os.mkdir('logs')
    file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
                                       backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)

    app.logger.setLevel(logging.INFO)
    app.logger.info('Microblog startup')
  • RotatingFileHandler assures that we have the last 10 log messages, 10KB maximum.
  • logging.Formatter to custom the log messages. lineno is the line number
  • Level lowered to INFO category to be useful.
    • Categories are DEBUG, INFO, WARNING, ERROR and CRITICAL by severity

Fixing the Duplicate Username Bug

  • Make sure that the username entered in the form does not exists in the database
  • Difference with registration is that if the original username is untouched, then the validation should allow it
class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
    submit = SubmitField('Submit')

    def __init__(self, original_username, *args, **kwargs):
        super(EditProfileForm, self).__init__(*args, **kwargs)
        self.original_username = original_username

    def validate_username(self, username):
        if username.data != self.original_username:
            user = User.query.filter_by(username=self.username.data).first()
            if user is not None:
                raise ValidationError('Please use a different username.')
  • Overloaded construction that accept the original username

  • The username is saved as an instance variable and checked in the validate_username() method

  • To use this method, add original username argument in the view function

@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm(current_user.username)
    # ...
  • Duplicates will be prevented in most cases