OverIQ.com

Application Structure and Blueprint in Flask

Last updated on July 27, 2020


Up until now, our entire Flask application mainly resides in a single file main2.py. That's fine for small applications, but as the project grows it becomes difficult to manage. When we break a monolithic file into multiple files, the code in it becomes more maintainable and predictable.

Flask doesn't impose any restrictions on how you should structure the application. However, it does provides some guidelines to make the application modular.

In this course, we will use the following application structure.

1
2
3
4
5
6
7
8
/app_dir
    /app
        __init__.py
        /static
        /templates
        views.py
    config.py
    runner.py

Here is the rundown of what each file and folder does:

File Description
app_dir The app_dir is the root directory of your Flask project.
app The app directory is a Python package which holds views, templates, and static files.
__init__.py The __init__.py tells the Python that app directory is a Python package.
static The static directory contains the static files of the project.
templates The templates directory contains templates.
views.py The views.py contains routes and view functions.
config.py The config.py contains settings and configuration for your Flask application.
runner.py The entry point for your Flask application.

In the rest of the lesson, we will convert our project to conform to this directory structure. We will start by creating config.py.

Class-Based Configurations #

A software project usually runs in three different environments:

  1. Development.
  2. Testing.
  3. Production.

As the project evolves you will encounter a need to specify different configuration options for different environment. You will also notice that no matter which environment you are in some configurations always remains the same. We can implement such a configuration system using classes.

Start off by defining default configurations in the base class and then create environment-specific classes which inherit from the base class. The environment-specific classes can override or add environment-specific configurations.

Create a new file named config.py inside flask_app directory and add the following code in it:

flask_app/config.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os

app_dir = os.path.abspath(os.path.dirname(__file__))

class BaseConfig:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'A SECRET KEY'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    ##### Flask-Mail configurations #####
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'infooveriq@gmail.com'
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'password'
    MAIL_DEFAULT_SENDER = MAIL_USERNAME


class DevelopementConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEVELOPMENT_DATABASE_URI') or  \
        'mysql+pymysql://root:pass@localhost/flask_app_db'
    

class TestingConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URI') or \
                              'mysql+pymysql://root:pass@localhost/flask_app_db'    

class ProductionConfig(BaseConfig):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('PRODUCTION_DATABASE_URI') or  \
        'mysql+pymysql://root:pass@localhost/flask_app_db'

Note that for the first time we are reading values of some configurations from the environment variables. We are also providing default values in case environment variables are not set. This method is particularly useful when you have some sensitive data that you don't want to hardcode in the application itself.

To read configurations from a class use from_object() method as follows:

app.config.from_object('config.Create')

Creating Application Package #

Create a new directory app directory inside the flask_app directory and move all files and directories to this directory ( except env and migrations directories and our newly created config.py file ). Inside the app directory create __init__.py file with the following code:

flask_app/app/__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask
from flask_migrate import Migrate, MigrateCommand
from flask_mail import Mail, Message
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager, Command, Shell
from flask_login import LoginManager
import os, config

# create application instance
app = Flask(__name__)
app.config.from_object(os.environ.get('FLASK_ENV') or 'config.DevelopementConfig')

# initializes extensions
db = SQLAlchemy(app)
mail = Mail(app)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

# import views
from . import views
# from . import forum_views
# from . import admin_views

The __init__.py creates the application instance and initializes extensions. If FLASK_ENV environment variable is not set the Flask application will run in the debug mode ( i.e app.debug = True ). To put the application in production mode set the FLASK_ENV environment variable to config.ProductionConfig.

After initializing extensions, the import statement in line 21, imports all the views. This is necessary to connect the application instance to the view functions, otherwise, Flask will not be aware of your view functions.

Rename main2.py file to views.py and update it so that it only contains routes and view functions. Here is the complete code of updated views.py file.

flask_app/app/views.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
from app import app
from flask import render_template, request, redirect, url_for, flash, make_response, session
from flask_login import login_required, login_user,current_user, logout_user
from .models import User, Post, Category, Feedback, db
from .forms import ContactForm, LoginForm
from .utils import send_mail


@app.route('/')
def index():
    return render_template('index.html', name='Jerry')


@app.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)


@app.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)


@app.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('admin'))
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('login'))
    return render_template('login.html', form=form)


@app.route('/logout/')
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for('login'))


@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data

        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()

        send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

        flash("Message Received", "success")
        return redirect(url_for('contact'))

    return render_template('contact.html', form=form)


@app.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res


@app.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res


@app.route('/article', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('article')
        return res, 302

    return render_template('article.html')


@app.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1
    else:
        session['visits'] = 1
    return "Total visits: {}".format(session.get('visits'))


@app.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'


@app.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res


@app.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')

The views.py file now only contains view functions. We have moved the code for models, form classes and utility functions to their respective files, as follows:

flask_app/app/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from app import db, login_manager
from datetime import datetime
from flask_login import (LoginManager, UserMixin, login_required,
                          login_user, current_user, logout_user)
from werkzeug.security import generate_password_hash, check_password_hash


class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)
    slug = db.Column(db.String(255), nullable=False, unique=True)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', backref='category', cascade='all,delete-orphan')

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)


post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'))
)


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    content = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)
    category_id = db.Column(db.Integer(), db.ForeignKey('categories.id'))

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.title[:10])


class Tag(db.Model):
    __tablename__ = 'tags'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', secondary=post_tags, backref='tags')

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)


class Feedback(db.Model):
    __tablename__ = 'feedbacks'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(1000), nullable=False)
    email = db.Column(db.String(100), nullable=False)
    message = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.name)


class Employee(db.Model):
    __tablename__ = 'employees'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    designation = db.Column(db.String(255), nullable=False)
    doj = db.Column(db.Date(), nullable=False)


@login_manager.user_loader
def load_user(user_id):
    return db.session.query(User).get(user_id)


class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(100))
    username = db.Column(db.String(50), nullable=False, unique=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    password_hash = db.Column(db.String(100), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id, self.username)

    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)

flask_app/app/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from flask_wtf import FlaskForm
from wtforms import Form, ValidationError
from wtforms import StringField, SubmitField, TextAreaField, BooleanField
from wtforms.validators import DataRequired, Email


class ContactForm(FlaskForm):
    name = StringField("Name: ", validators=[DataRequired()])
    email = StringField("Email: ", validators=[Email()])
    message = TextAreaField("Message", validators=[DataRequired()])
    submit = SubmitField()


class LoginForm(FlaskForm):
    username = StringField("Username", validators=[DataRequired()])
    password = StringField("Password", validators=[DataRequired()])
    remember = BooleanField("Remember Me")
    submit = SubmitField()

flask_app/app/utils.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from . import mail, db
from flask import render_template
from threading import Thread
from app import app
from flask_mail import Message


def async_send_mail(app, msg):
    with app.app_context():
        mail.send(msg)


def send_mail(subject, recipient, template, **kwargs):
    msg = Message(subject, sender=app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient])
    msg.html = render_template(template, **kwargs)
    thrd = Thread(target=async_send_mail, args=[app, msg])
    thrd.start()
    return thrd

Finally, to launch the application add the following code to runner.py file:

flask_app/runner.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import os
from app import app, db
from app.models import User, Post, Tag, Category, Employee, Feedback
from flask_script import Manager, Shell
from flask_migrate import MigrateCommand

manager = Manager(app)

# these names will be available inside the shell without explicit import
def make_shell_context():
    return dict(app=app,  db=db, User=User, Post=Post, Tag=Tag, Category=Category,
                Employee=Employee, Feedback=Feedback)

manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

The runner.py is the entry point of our project. The file starts by creating an instance of Manager() object. It then defines make_shell_context() function. The objects returned by make_shell_context() function will be available inside the shell without explicit import statements. At last, the run() method on the Manager instance is called to start the server.

Import Flow #

We have created quite a few files in this lesson and it is very easy to lose track of which file does what and the order in which files are executed. To make things clear this section explains shows how everything works together.

Things start with the execution of runner.py file. The second line of runner.py file imports app and db from the app package. When Python interpreter encounters this line, program control transfers to __init__.py to start its execution. In line 7, __init__.py imports the config module which transfers the program control to config.py. When the execution of config.py completes the program control again comes back to __init__.py. In line 21, __init__.py file imports views module which transfers the program control to views.py. The first line of views.py again imports application instance app from the app package. The application instance app is already in the memory so it will not be imported again. In line 4, 5 and 6, views.py imports models, forms and send_mail function respectively which transfers the program control temporarily to their respective files in turn. When the execution of views.py finishes the program control comes back to __init__.py. This completes the execution of __init__.py. The program control comes back to runner.py and starts the execution of the statement in line 3.

The third line of runner.py imports the classes defined in the models.py module. Since models are already available from views.py, the models.py file will not be executed again.

Since we are running runner.py as the main module, the condition in line 17 evaluates to True and manager.run() starts the application.

Running Project #

We are now ready to run our project. In the terminal enter the following command to start the server.

1
2
3
4
5
(env) overiq@vm:~/flask_app$ python runner.py runserver
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 391-587-440
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

If FLASK_ENV environment variable is not set, the preceding command will start the application in the debug mode. Navigate to http://127.0.0.1:5000/ and you should see the home page which currently looks like this:

Browse the remaining pages of the application to make sure everything is working as expected.

Our application is now very flexible. It can pick up a completely different set of configurations just by reading an environment variable. For example, let's say we want to put our site in production mode. To do so, simply create an environment variable FLASK_ENV with the value config.ProductionConfig.

In the terminal, enter the following command to create FLASK_ENV environment variable:

(env) overiq@vm:~/flask_app$ export FLASK_ENV=config.ProductionConfig

This command creates an environment variable in Linux and Mac OS. Window user can use the following command:

(env) C:\Users\overiq\flask_app>set FLASK_ENV=config.ProductionConfig

Run the application again.

1
2
(env) overiq@vm:~/flask_app$ python runner.py runserver
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Now our application is running in production mode. At this point, if Python code raises an exception you will see a 500 Internal Server Error instead of a stack trace.

Because we are still in development, we have to delete FLASK_ENV environment variable. The FLASK_ENV will be deleted automatically as soon as you close the terminal. To manually delete it enter the following command:

(env) overiq@vm:~/flask_app$ unset FLASK_ENV

Window users can use this command:

(env) C:\Users\overiq\flask_app>set FLASK_ENV=

Our project is in much better shape. Now things are organized in a much more predictable way than it was possible before. The technique devised here is useful for small to medium-sized projects. However, Flask has some more tricks under its sleeve which can help you to become more productive.

Blueprints #

Blueprints are yet another way to organize the application. Blueprints provide separation of concerns at the level of views. Just like a Flask application, a Blueprint can have its own views, static files, and templates. We can also map blueprints with their own URIs. For example, let's say we are working on a blog and its admin panel. A blueprint for a blog would contain view function, templates and static assets specific to the blog only. Whereas the blueprint for admin panel would contain views, static files, and templates specific to the admin panel. Blueprints can be implemented either by using a module or a package.

It's time to add a blueprint to our project.

Creating Blueprint #

Create an directory named main inside the flask_app/app directory and move views.py and forms.py to this directory. Inside the main directory create __init__.py file with the following code:

flask_app/app/main/__init__.py

1
2
3
4
5
from flask import Blueprint

main = Blueprint('main', __name__)

from . import views

We are creating blueprint object using the Blueprint class. The Blueprint() constructor takes two arguments, the blueprint name and the name of the package where blueprint is located; for most applications passing __name__ to it will suffice.

By default, views functions in the blueprint will look for templates and static assets in the application's templates and static directories respectively.

We can change that by specifying the location of templates and static assets while creating Blueprint object as follows:

1
2
3
main = Blueprint('main', __name__
                template_folder='templates_dir')
                static_folder='static_dir')

In this case, Flask will look for templates and static assets in the templates_dir and static_dir directories inside the blueprint package.

The template path added by a blueprint has lower priority than the application's templates directory. That means if you have two templates of the same name in templates_dir and templates directories, Flask will use the template from the templates directory.

The following are some noteworthy points to remember about the blueprints:

  1. When using blueprints the routes are defined using the route decorator of the blueprint object rather than the application instance ( app ).

  2. To create URLs when using blueprints you have to prefix the endpoint with the name of the blueprint and a dot (.). This is true whether you are creating URLs in the Python code or in templates. For example:

    url_for("main.index")
    

    This will return the URL of the index route of the main blueprint.

    The name of the blueprint can be omitted, in case you are in the same blueprint that you want to create URL for. For example:

    url_for(".index")
    

    This will return the URL of the index route of the main blueprint assuming you are in the view function or template of the main blueprint.

To accommodate the changes we have to we have to update import statements, url_for() calls and routes in views.py file. Here is the updated version of views.py file.

flask_app/app/main/views.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from app import app, db
from . import main
from flask import Flask, request, render_template, redirect, url_for, flash, make_response, session
from flask_login import login_required, login_user, current_user, logout_user
from app.models import User, Post, Category, Feedback, db
from .forms import ContactForm, LoginForm
from app.utils import send_mail


@main.route('/')
def index():
    return render_template('index.html', name='Jerry')


@main.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)


@main.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)


@main.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('.admin'))
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('.admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('.login'))
    return render_template('login.html', form=form)


@main.route('/logout/')
@login_required
def logout():
    logout_user()    
    flash("You have been logged out.")
    return redirect(url_for('.login'))


@main.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data
        print(name)
        print(email)
        print(message)
                
        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()
        
        send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)
        
        print("\nData received. Now redirecting ...")
        flash("Message Received", "success")
        return redirect(url_for('.contact'))

    return render_template('contact.html', form=form)


@main.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res


@main.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res


@main.route('/article/', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        print(request.form)
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('.article')
        return res, 302
    
    return render_template('article.html')


@main.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1  # reading and updating session data
    else:
        session['visits'] = 1 # setting session data
    return "Total visits: {}".format(session.get('visits'))


@main.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'


@main.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res


@main.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')

Notice that throughout the views.py file, we are creating URLs without specifying the blueprint name because we are inside the same blueprint for which we are creating URL for.

Also update the url_for() call in admin.html as follows:

flask_app/app/templates/admin.html

1
2
3
#...
<p><a href="{{ url_for('.logout') }}">Logout</a></p>
#...

The view functions in views.py are now associated with the main blueprint. Next, we have to register the blueprint on the Flask application. Open app/__init__.py and modify it as follows: (changes are highlighted):

flask_app/app/__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#...
# create application instance
app = Flask(__name__)
app.config.from_object(os.environ.get('FLASK_ENV') or 'config.DevelopementConfig')

# initializes extensions
db = SQLAlchemy(app)
mail = Mail(app)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'main.login'

# register blueprints
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)

#from .admin import main as admin_blueprint
#app.register_blueprint(admin_blueprint)

The register_blueprint() method of the application instance is used to register blueprint. We can register multiple blueprints by calling register_blueprint() for each blueprint. Notice that in line 11, we are assigning main.login to the login_manager.login_view. In this case, it is necessary to specify the blueprint name otherwise Flask wouldn't be able to tell which blueprint you are referring to.

At this point, the application structure should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
├── flask_app/
├── app/
│   ├── __init__.py
│   ├── main/
│   │   ├── forms.py
│   │   ├── __init__.py
│   │   └── views.py
│   ├── models.py
│   ├── static/
│   │   └── style.css
│   ├── templates/
│   │   ├── admin.html
│   │   ├── article.html
│   │   ├── contact.html
│   │   ├── index.html
│   │   ├── login.html
│   │   └── mail/
│   │       └── feedback.html
│   └── utils.py
├── migrations/
│   ├── alembic.ini
│   ├── env.py
│   ├── README
│   ├── script.py.mako
│   └── versions/
│       ├── 0f0002bf91cc_adding_users_table.py
│       ├── 6e059688f04e_adding_employees_table.py
├── runner.py
├── config.py
├── env/

The Application Factory #

We are already using packages and blueprints in our application. We can further improve our app by delegating the task of instantiating application instance to the Application Factory. Application Factory is simply a function which creates an object.

So what do we get by doing this:

  1. It makes testing easier because we can create application instance with different settings.
  2. We can run multiple instances of the same application in the same process. This is handy when you have load balancers distributing traffic across different servers.

Let's update app/__init__.py to implement an application factory as follows (changes are highlighted):

flask_app/app/__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from flask import Flask
from flask_migrate import Migrate, MigrateCommand
from flask_mail import Mail, Message
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager, Command, Shell
from flask_login import LoginManager
import os, config

db = SQLAlchemy()
mail = Mail()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = 'main.login'

# application factory
def create_app(config):
    
    # create application instance
    app = Flask(__name__)
    app.config.from_object(config)

    db.init_app(app)
    mail.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    #from .admin import main as admin_blueprint
    #app.register_blueprint(admin_blueprint)

    return app

We have delegated the task of creating application instance to the create_app() function. The create_app() function takes a single argument named config and returns an application instance.

Application factory separates the instantiation of the extensions from their configurations. The instantiation occurs before create_app() is called and configuration occurs inside the create_app() function using the init_app() method.

Next, update runner.py to use applicaton factory as follows:

flask_app/runner.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import os
from app import db, create_app
from app.models import User, Post, Tag, Category, Employee, Feedback
from flask_script import Manager, Shell
from flask_migrate import MigrateCommand

app = create_app(os.getenv('FLASK_ENV') or 'config.DevelopementConfig')
manager = Manager(app)

def make_shell_context():
    return dict(app=app,  db=db, User=User, Post=Post, Tag=Tag, Category=Category,
                Employee=Employee, Feedback=Feedback)

manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

It is important to note that when using application factories, we no longer have access to the application instance in the blueprint at the import time. To access application inside a blueprint use current_app proxy from the flask package. Let's update our project to use current_app variable as follows:

flask_app/app/main/views.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from app import db
from . import main
from flask import (render_template, request, redirect, url_for, flash,
                   make_response, session, current_app)
from flask_login import login_required, login_user, current_user, logout_user
from app.models import User, Feedback
from app.utils import send_mail
from .forms import ContactForm, LoginForm


@main.route('/')
def index():
    return render_template('index.html', name='Jerry')


@main.route('/user/<int:user_id>/')
def user_profile(user_id):
    return "Profile page of user #{}".format(user_id)


@main.route('/books/<genre>/')
def books(genre):
    return "All Books in {} category".format(genre)


@main.route('/login/', methods=['post', 'get'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('.admin'))
    form = LoginForm()
    if form.validate_on_submit():
        user = db.session.query(User).filter(User.username == form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('.admin'))

        flash("Invalid username/password", 'error')
        return redirect(url_for('.login'))
    return render_template('login.html', form=form)


@main.route('/logout/')
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for('.login'))


@main.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data

        # db logic goes here
        feedback = Feedback(name=name, email=email, message=message)
        db.session.add(feedback)
        db.session.commit()

        send_mail("New Feedback", current_app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

        flash("Message Received", "success")
        return redirect(url_for('.contact'))

    return render_template('contact.html', form=form)


@main.route('/cookie/')
def cookie():
    if not request.cookies.get('foo'):
        res = make_response("Setting a cookie")
        res.set_cookie('foo', 'bar', max_age=60*60*24*365*2)
    else:
        res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo')))
    return res


@main.route('/delete-cookie/')
def delete_cookie():
    res = make_response("Cookie Removed")
    res.set_cookie('foo', 'bar', max_age=0)
    return res


@main.route('/article', methods=['POST', 'GET'])
def article():
    if request.method == 'POST':
        res = make_response("")
        res.set_cookie("font", request.form.get('font'), 60*60*24*15)
        res.headers['location'] = url_for('.article')
        return res, 302

    return render_template('article.html')


@main.route('/visits-counter/')
def visits():
    if 'visits' in session:
        session['visits'] = session.get('visits') + 1
    else:
        session['visits'] = 1
    return "Total visits: {}".format(session.get('visits'))


@main.route('/delete-visits/')
def delete_visits():
    session.pop('visits', None) # delete visits
    return 'Visits deleted'


@main.route('/session/')
def updating_session():
    res = str(session.items())

    cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'}
    if 'cart_item' in session:
        session['cart_item']['pineapples'] = '100'
        session.modified = True
    else:
        session['cart_item'] = cart_item

    return res


@main.route('/admin/')
@login_required
def admin():
    return render_template('admin.html')

flask_app/app/utils.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from . import mail, db
from flask import render_template, current_app
from threading import Thread
from flask_mail import Message


def async_send_mail(app, msg):
    with app.app_context():
        mail.send(msg)


def send_mail(subject, recipient, template, **kwargs):
    msg = Message(subject, sender=current_app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient])
    msg.html = render_template(template, **kwargs)
    thr = Thread(target=async_send_mail, args=[current_app._get_current_object(), msg])
    thr.start()
    return thr

Now you should have a have a solid understanding of Flask, its different components and how everything fits together. Believe it or not, we have explored quite a lot. In the next part of this course, we will use what we have learned to create a Delicious clone named Flask-Marks. Delicious is a social bookmarking site launched in 2003. In 2017, it was acquired by Pinboard and since then it is running in read-only mode. Flip the page and let's get started.