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:
- Development.
- Testing.
- 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:
When using blueprints the routes are defined using the
route
decorator of the blueprint object rather than the application instance (app
).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 themain
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 themain
blueprint assuming you are in the view function or template of themain
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:
- It makes testing easier because we can create application instance with different settings.
- 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.
Load Comments