Python Flask for Beginners: Build a CRUD Web App with Python and Flask Part. I
I’ve named the app Project Dream Team, and it will have the following features:
- Users will be able to register and login as employees
- The administrator will be able to create, update, and delete departments and roles
- The administrator will be able to assign employees to a department and assign them roles
- The administrator will be able to view all employees and their details
Part One will cover:
- Users will be able to register and login as employees
- The administrator will be able to create, update, and delete departments and roles
- The administrator will be able to assign employees to a department and assign them roles
- The administrator will be able to view all employees and their details
Ready? Here we go!
Prerequisites
This tutorial builds on my introductory tutorial, Getting Started With Flask, picking up where it left off. It assumes you have, to begin with, the following dependencies installed:
- Users will be able to register and login as employees
- The administrator will be able to create, update, and delete departments and roles
- The administrator will be able to assign employees to a department and assign them roles
- The administrator will be able to view all employees and their details
You should have a virtual environment set up and activated. You should also have the following file and directory structure:
├── dream-team
   ├── app
   │   ├── __init__.py
   │   ├── templates
   │   ├── models.py
  │   └── views.py
   ├── config.py
    ├── requirements.txt
   └── run.py
This project structure groups the similar components of the application together. The dream-team
directory houses all the project files. The app
directory is the application package, and houses different but interlinked modules of the application. All templates are stored in the templates
directory, all models are in the models.py
file, and all routes are in the views.py
file. The run.py
file is the application’s entry point, the config.py
file contains the application configurations, and the requirements.txt
file contains the software dependencies for the application.
If you don’t have these set up, please visit the introductory tutorial and catch up!
Database Setup
Flask has support for several relational database management systems, including SQLite, MySQL, and PostgreSQL. For this tutorial, we will be using MySQL. It’s popular and therefore has a lot of support, in addition to being scalable, secure, and rich in features.
We will install the following (remember to activate your virtual environment):
- Users will be able to register and login as employees
- The administrator will be able to create, update, and delete departments and roles
- The administrator will be able to assign employees to a department and assign them roles
The administrator will be able to view all employees and their details
$ pip install flask-sqlalchemy mysql-python
We’ll then create the MySQL database. Ensure you have MySQL installed and running, and then log in as the root user:
$ mysql -u root
mysql> CREATE USER 'dt_admin'@'localhost' IDENTIFIED BY 'dt2016';
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE DATABASE dreamteam_db;
Query OK, 1 row affected (0.00 sec)
mysql> GRANT ALL PRIVILEGES ON dreamteam_db . * TO 'dt_admin'@'localhost';
Query OK, 0 rows affected (0.00 sec)
We have now created a new user dt_admin
with the password dt2016
, created a new database dreamteam_db
, and granted the new user all database privileges.
Next, let’s edit the config.py
. Remove any exisiting code and add the following:
# config.py
class Config(object):
"""
Common configurations
"""
# Put any configurations here that are common across all environments
class DevelopmentConfig(Config):
"""
Development configurations
"""
DEBUG = True
SQLALCHEMY_ECHO = True
class ProductionConfig(Config):
"""
Production configurations
"""
DEBUG = False
app_config = {
'development': DevelopmentConfig,
'production': ProductionConfig
}
It is good practice to specify configurations for different environments. In the file above, we have specifed configurations for development, which we will use while building the app and running it locally, as well as production, which we will use when the app is deployed.
Some useful configuration variables are:
- Users will be able to register and login as employees
- The administrator will be able to create, update, and delete departments and roles
- The administrator will be able to assign employees to a department and assign them roles
- The administrator will be able to view all employees and their details
You can find more Flask configuration variables here and SQLAlchemy configuration variables here.
Next, create an instance
directory in the dream-team
directory, and then create a config.py
file inside it. We will put configuration variables here that will not be pushed to version control due to their sensitive nature. In this case, we put the secret key as well as the database URI which contains the database user password.
# instance/config.py
SECRET_KEY = 'p9Bv<3Eid9%$i01'
SQLALCHEMY_DATABASE_URI = 'mysql://dt_admin:dt2016@localhost/dreamteam_db'
Now, let’s edit the app/__init__.py
file. Remove any existing code and add the following:
# app/__init__.py
# third-party imports
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
# local imports
from config import app_config
# db variable initialization
db = SQLAlchemy()
def create_app(config_name):
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(app_config[config_name])
app.config.from_pyfile('config.py')
db.init_app(app)
return app
We’ve created a function, create_app
that, given a configuration name, loads the correct configuration from the config.py
file, as well as the configurations from the instance/config.py
file. We have also created a db
object which we will use to interact with the database.
Next, let’s edit the run.py
file:
# run.py
import os
from app import create_app
config_name = os.getenv('FLASK_CONFIG')
app = create_app(config_name)
if __name__ == '__main__':
app.run()
We create the app by running the create_app
function and passing in the configuration name. We get this from the OS environment variable FLASK_CONFIG
. Because we are in development, we should set the environment variable to development
.
Let’s run the app to ensure everything is working as expected. First, delete the app/views.py
file as well as the app/templates
directory as we will not be needing them going forward. Next, add a temporary route to the app/__init__.py
file as follows:
# app/__init__.py
# existing code remains
def create_app(config_name):
# existing code remains
# temporary route
@app.route('/')
def hello_world():
return 'Hello, World!'
return app
Make sure you set the FLASK_CONFIG
and FLASK_APP
environment variables before running the app:
$ export FLASK_CONFIG=development
$ export FLASK_APP=run.py
$ flask run
* Serving Flask app "run"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
We can see the “Hello, World” string we set in the route. The app is working well so far.
Models
Now to work on the models. Remember that a model is a representation of a database table in code. We’ll need three models: Employee
, Department
, and Role
.
But first, let’s install Flask-Login, which will help us with user management and handle logging in, logging out, and user sessions. The Employee
model will inherit from Flask-Login’s UserMixin
class which will make it easier for us to make use of its properties and methods.
$ pip install flask-login
To use Flask-Login, we need to create a LoginManager object and initialize it in the app/__init__.py
file. First, remove the route we added earlier, and then add the following:
# app/__init__.py
# after existing third-party imports
from flask_login import LoginManager
# after the db variable initialization
login_manager = LoginManager()
def create_app(config_name):
# existing code remains
login_manager.init_app(app)
login_manager.login_message = "You must be logged in to access this page."
login_manager.login_view = "auth.login"
return app
In addition to initializing the LoginManager object, we’ve also added a login_view
and login_message
to it. This way, if a user tries to access a page that they are not authorized to, it will redirect to the specified view and display the specified message. We haven’t created the auth.login
view yet, but we will when we get to authentication.
Now add the following code to the app/models.py
file:
# app/models.py
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login_manager
class Employee(UserMixin, db.Model):
"""
Create an Employee table
"""
# Ensures table will be named in plural and not in singular
# as is the name of the model
__tablename__ = 'employees'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(60), index=True, unique=True)
username = db.Column(db.String(60), index=True, unique=True)
first_name = db.Column(db.String(60), index=True)
last_name = db.Column(db.String(60), index=True)
password_hash = db.Column(db.String(128))
department_id = db.Column(db.Integer, db.ForeignKey('departments.id'))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
is_admin = db.Column(db.Boolean, default=False)
@property
def password(self):
"""
Prevent pasword from being accessed
"""
raise AttributeError('password is not a readable attribute.')
@password.setter
def password(self, password):
"""
Set password to a hashed password
"""
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
"""
Check if hashed password matches actual password
"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '<Employee: {}>'.format(self.username)
# Set up user_loader
@login_manager.user_loader
def load_user(user_id):
return Employee.query.get(int(user_id))
class Department(db.Model):
"""
Create a Department table
"""
__tablename__ = 'departments'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), unique=True)
description = db.Column(db.String(200))
employees = db.relationship('Employee', backref='department',
lazy='dynamic')
def __repr__(self):
return '<Department: {}>'.format(self.name)
class Role(db.Model):
"""
Create a Role table
"""
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), unique=True)
description = db.Column(db.String(200))
employees = db.relationship('Employee', backref='role',
lazy='dynamic')
def __repr__(self):
return '<Role: {}>'.format(self.name)
In the Employee
model, we make use of some of Werkzeug’s handy security helper methods, generate_password_hash
, which allows us to hash passwords, and check_password_hash
, which allows us ensure the hashed password matches the password. To enhance security, we have a password
method which ensures that the password can never be accessed; instead an error will be raised. We also have two foreign key fields, department_id
and role_id
, which refer to the ID’s of the department and role assigned to the employee.
Note that we have an is_admin
field which is set to False
by default. We will override this when creating the admin user. Just after the Employee
model, we have a user_loader
callback, which Flask-Login uses to reload the user object from the user ID stored in the session.
The Department
and Role
models are quite similar. Both have name
and description
fields. Additionally, both have a one-to-many relationship with the Employee
model (one department or role can have many employees). We define this in both models using the employees
field. backref
allows us to create a new property on the Employee
model such that we can use employee.department
or employee.role
to get the department or role assigned to that employee. lazy
defines how the data will be loaded from the database; in this case it will be loaded dynamically, which is ideal for managing large collections.
Migration
Migrations allow us to manage changes we make to the models, and propagate these changes in the database. For example, if later on we make a change to a field in one of the models, all we will need to do is create and apply a migration, and the database will reflect the change.
We’ll begin by installing Flask-Migrate, which will handle the database migrations using Alembic, a lightweight database migration tool. Alembic emits ALTER
statements to a database thus implememting changes made to the models. It also auto-generates minimalistic migration scripts, which may be complex to write.
$ pip install flask-migrate
We’ll need to edit the app/__init__.py
file:
# app/__init__.py
# after existing third-party imports
from flask_migrate import Migrate
# existing code remains
def create_app(config_name):
# existing code remains
migrate = Migrate(app, db)
from app import models
return app
We have created a migrate
object which will allow us to run migrations using Flask-Migrate. We have also imported the models from the app
package. Next, we’ll run the following command to create a migration repository:
$ flask db init
This creates a migrations
directory in the dream-team
directory:
└── migrations
├── README
├── alembic.ini
├── env.py
├── script.py.mako
└── versions
Next, we will create the first migration:
$ flask db migrate
Finally, we’ll apply the migration:
$ flask db upgrade
We’ve sucessfully created tables based on the models we wrote! Let’s check the MySQL database to confirm this:
$ mysql -u root
mysql> use dreamteam_db;
mysql> show tables;
+------------------------+
| Tables_in_dreamteam_db |
+------------------------+
| alembic_version |
| departments |
| employees |
| roles |
+------------------------+
4 rows in set (0.00 sec)
Blueprints
Blueprints are great for organising a flask app into components, each with its own views and forms. I find that blueprints make for a cleaner and more organised project structure because each blueprint is a distinct component that addresses a specific functionality of the app. Each blueprint can even have its own cutsom URL prefix or subdomain. Blueprints are particularly convenient for large applications.
We’re going to have three blueprints in this app:
- Users will be able to register and login as employees
- The administrator will be able to create, update, and delete departments and roles
- The administrator will be able to assign employees to a department and assign them roles
- The administrator will be able to view all employees and their details
Create the relevant files and directories so that your directory structure resembles this:
└── dream-team
├── app
│   ├── __init__.py
│   ├── admin
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── auth
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── home
│   │   ├── __init__.py
│   │   └── views.py
│   ├── models.py
│   ├── static
│   └── templates
├── config.py
├── instance
│   └── config.py
├── migrations
│   ├── README
│   ├── alembic.ini
│   ├── env.py
│   ├── script.py.mako
│   └── versions
│   └── a1a1d8b30202_.py
├── requirements.txt
└── run.py
I chose not to have static
and templates
directories for each blueprint, because all the application templates will inherit from the same base template and use the same CSS file. Instead, the templates
directory will have sub-directories for each blueprint so that blueprint templates can be grouped together.
In each blueprint’s __init__.py
file, we need to create a Blueprint object and initialize it with a name. We also need to import the views.
# app/admin/__init__.py
from flask import Blueprint
admin = Blueprint('admin', __name__)
from . import views
# app/auth/__init__.py
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
# app/home/__init__.py
from flask import Blueprint
home = Blueprint('home', __name__)
from . import views
Then, we can register the blueprints on the app in the app/__init__.py
file, like so:
# app/__init__.py
# existing code remains
def create_app(config_name):
# existing code remains
from app import models
from .admin import admin as admin_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/admin')
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
from .home import home as home_blueprint
app.register_blueprint(home_blueprint)
return app
We have imported each blueprint object and registered it. For the admin
blueprint, we have added a url prefix, /admin
. This means that all the views for this blueprint will be accessed in the browser with the url prefix admin
.
Home Blueprint
Time to work on fleshing out the blueprints! We’ll start with the home
blueprint, which will have the homepage as well as the dashboard.
# app/home/views.py
from flask import render_template
from flask_login import login_required
from . import home
@home.route('/')
def homepage():
"""
Render the homepage template on the / route
"""
return render_template('home/index.html', title="Welcome")
@home.route('/dashboard')
@login_required
def dashboard():
"""
Render the dashboard template on the /dashboard route
"""
return render_template('home/dashboard.html', title="Dashboard")
Each view function has a decorator, home.route
, which has a URL route as a parameter (remember that home
is the name of the blueprint as specified in the app/home/__init__.py
file). Each view handles requests to the specified URL.
The homepage
view renders the home template, while the dashboard
view renders the dashboard template. Note that the dashboard
view has a login_required
decorator, meaning that users must be logged in to access it.
Now to work on the base template, which all other templates will inherit from. Create a base.html
file in the app/templates
directory and add the following code:
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }} | Project Dream Team</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top topnav" role="navigation">
<div class="container topnav">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand topnav" href="{{ url_for('home.homepage') }}">Project Dream Team</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="#">Register</a></li>
<li><a href="#">Login</a></li>
</ul>
</div>
</div>
</nav>
<div class="wrapper">
{% block body %}
{% endblock %}
<div class="push"></div>
</div>
<footer>
<div class="container">
<div class="row">
<div class="col-lg-12">
<ul class="list-inline">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="#">Register</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="#">Login</a></li>
</ul>
<p class="copyright text-muted small">Copyright © 2016. All Rights Reserved</p>
</div>
</div>
</div>
</footer>
</body>
</html>
Note that we use #
for the Register and Login links. We will update this when we are working on the auth
blueprint.
Next, create a home
directory inside the app/templates
directory. The homepage template, index.html
, will go inside it:
<!-- app/templates/home/index.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block body %}
<div class="intro-header">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="intro-message">
<h1>Project Dream Team</h1>
<h3>The best company in the world!</h3>
<hr class="intro-divider">
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Inside the static
directory, add css
and img
directories. Add the following CSS file, style.css
, to your static/css
directory (note that you will need a background image, intro-bg.jpg
, as well as a favicon in your static/img
directory):
/* app/static/css/style.css */
body, html {
width: 100%;
height: 100%;
}
body, h1, h2, h3 {
font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 700;
}
a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a {
color: #aec251;
}
a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover {
color: #687430;
}
footer {
padding: 50px 0;
background-color: #f8f8f8;
}
p.copyright {
margin: 15px 0 0;
}
.alert-info {
width: 50%;
margin: auto;
color: #687430;
background-color: #e6ecca;
border-color: #aec251;
}
.btn-default {
border-color: #aec251;
color: #aec251;
}
.btn-default:hover {
background-color: #aec251;
}
.center {
margin: auto;
width: 50%;
padding: 10px;
}
.content-section {
padding: 50px 0;
border-top: 1px solid #e7e7e7;
}
.footer, .push {
clear: both;
height: 4em;
}
.intro-divider {
width: 400px;
border-top: 1px solid #f8f8f8;
border-bottom: 1px solid rgba(0,0,0,0.2);
}
.intro-header {
padding-top: 50px;
padding-bottom: 50px;
text-align: center;
color: #f8f8f8;
background: url(../img/intro-bg.jpg) no-repeat center center;
background-size: cover;
height: 100%;
}
.intro-message {
position: relative;
padding-top: 20%;
padding-bottom: 20%;
}
.intro-message > h1 {
margin: 0;
text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
font-size: 5em;
}
.intro-message > h3 {
text-shadow: 2px 2px 3px rgba(0,0,0,0.6);
}
.lead {
font-size: 18px;
font-weight: 400;
}
.topnav {
font-size: 14px;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -4em;
}
Run the app; you should be able to see the homepage now.
Auth Blueprint
For the auth
blueprint, we’ll begin by creating the registration and login forms. We’ll use Flask-WTF, which will allow us to create forms that are secure (thanks to CSRF protection and reCAPTCHA support).
pip install Flask-WTF
Now to write the code for the forms:
# app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField, ValidationError
from wtforms.validators import DataRequired, Email, EqualTo
from ..models import Employee
class RegistrationForm(FlaskForm):
"""
Form for users to create new account
"""
email = StringField('Email', validators=[DataRequired(), Email()])
username = StringField('Username', validators=[DataRequired()])
first_name = StringField('First Name', validators=[DataRequired()])
last_name = StringField('Last Name', validators=[DataRequired()])
password = PasswordField('Password', validators=[
DataRequired(),
EqualTo('confirm_password')
])
confirm_password = PasswordField('Confirm Password')
submit = SubmitField('Register')
def validate_email(self, field):
if Employee.query.filter_by(email=field.data).first():
raise ValidationError('Email is already in use.')
def validate_username(self, field):
if Employee.query.filter_by(username=field.data).first():
raise ValidationError('Username is already in use.')
class LoginForm(FlaskForm):
"""
Form for users to login
"""
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
Flask-WTF has a number of validators that make writing forms much easier. All the fields in the models have the DataRequired
validator, which means that users will be required to fill all of them in order to register or login.
For the registration form, we require users to fill in their email address, username, first name, last name, and their password twice. We use the Email
validator to ensure valid email formats are used (e.g some-name@some-domain.com
.) We use the EqualTo
validator to confirm that the password
and confirm_password
fields in the RegistrationForm
match. We also create methods (validate_email
and validate_username
) to ensure that the email and username entered have not been used before.
The submit
field in both forms will be represented as a button that users will be able to click to register and login respectively.
With the forms in place, we can write the views:
# app/auth/views.py
from flask import flash, redirect, render_template, url_for
from flask_login import login_required, login_user, logout_user
from . import auth
from forms import LoginForm, RegistrationForm
from .. import db
from ..models import Employee
@auth.route('/register', methods=['GET', 'POST'])
def register():
"""
Handle requests to the /register route
Add an employee to the database through the registration form
"""
form = RegistrationForm()
if form.validate_on_submit():
employee = Employee(email=form.email.data,
username=form.username.data,
first_name=form.first_name.data,
last_name=form.last_name.data,
password=form.password.data)
# add employee to the database
db.session.add(employee)
db.session.commit()
flash('You have successfully registered! You may now login.')
# redirect to the login page
return redirect(url_for('auth.login'))
# load registration template
return render_template('auth/register.html', form=form, title='Register')
@auth.route('/login', methods=['GET', 'POST'])
def login():
"""
Handle requests to the /login route
Log an employee in through the login form
"""
form = LoginForm()
if form.validate_on_submit():
# check whether employee exists in the database and whether
# the password entered matches the password in the database
employee = Employee.query.filter_by(email=form.email.data).first()
if employee is not None and employee.verify_password(
form.password.data):
# log employee in
login_user(employee)
# redirect to the dashboard page after login
return redirect(url_for('home.dashboard'))
# when login details are incorrect
else:
flash('Invalid email or password.')
# load login template
return render_template('auth/login.html', form=form, title='Login')
@auth.route('/logout')
@login_required
def logout():
"""
Handle requests to the /logout route
Log an employee out through the logout link
"""
logout_user()
flash('You have successfully been logged out.')
# redirect to the login page
return redirect(url_for('auth.login'))
Just like in the home
blueprint, each view here handles requests to the specified URL. The register
view creates an instance of the Employee
model class using the registration form data to populate the fields, and then adds it to the database. This esentially registers a new employee.
The login
view queries the database to check whether an employee exists with an email address that matches the email provided in the login form data. It then uses the verify_password
method to check that the password in the database for the employee matches the password provided in the login form data. If both of these are true, it proceeds to log the user in using the login_user
method provided by Flask-Login.
The logout
view has the login_required
decorator, which means that a user must be logged in to access it. It calles the logout_user
method provided by Flask-Login to log the user out.
Note the use of flash
method, which allows us to use Flask’s message flashing feature. This allows us to communicate feedback to the user, such as informing them of successful registration or unsuccessful login.
Finally, let’s work on the templates. First, we’ll install Flask-Bootstrap so we can use its wtf
and utils
libraries. The wtf
library will allow us to quickly generate forms in the templates based on the forms in the forms.py
file. The utils
library will allow us to display the flash messages we set earlier to give feedback to the user.
pip install flask-bootstrap
We need to edit the app/__init__.py
file to use Flask-Bootstrap:
# app/__init__.py
# after existing third-party imports
from flask_bootstrap import Bootstrap
# existing code remains
def create_app(config_name):
# existing code remains
Bootstrap(app)
from app import models
# blueprint registration remains here
return app
We’ve made quite a number of edits to the app/__init__.py
file. This is the final version of the file and how it should look at this point (note that I have re-arranged the imports and variables in alphabetical order):
# app/__init__.py
# third-party imports
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
# local imports
from config import app_config
db = SQLAlchemy()
login_manager = LoginManager()
def create_app(config_name):
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(app_config[config_name])
app.config.from_pyfile('config.py')
Bootstrap(app)
db.init_app(app)
login_manager.init_app(app)
login_manager.login_message = "You must be logged in to access this page."
login_manager.login_view = "auth.login"
migrate = Migrate(app, db)
from app import models
from .admin import admin as admin_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/admin')
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
from .home import home as home_blueprint
app.register_blueprint(home_blueprint)
return app
We need two templates for the auth
blueprint: register.html
and login.html
, which we’ll create in an auth
directory inside the templates
directory.
<!-- app/templates/auth/register.html -->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block body %}
<div class="content-section">
<div class="center">
<h1>Register for an account</h1>
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
<!-- app/templates/auth/login.html -->
{% import "bootstrap/utils.html" as utils %}
{% import "bootstrap/wtf.html" as wtf %}
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block body %}
<div class="content-section">
<br/>
{{ utils.flashed_messages() }}
<br/>
<div class="center">
<h1>Login to your account</h1>
<br/>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
The forms are loaded from the app/auth/views.py
file, where we specified which template files to display for each view. Remember the Register and Login links in the base template? Let’s update them now so we can access the pages from the menus:
<!-- app/templates/base.html -->
<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>
<!-- Modify footer menu -->
<ul class="list-inline">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
</ul>
Run the app again and click on the Register and Login menu links. You should see the templates loaded with the appropriate form.
Try to fill out the registration form; you should be able to register a new employee. After registration, you should be redirected to the login page, where you will see the flash message we configured in the app/auth/views.py
file, inviting you to login.
Logging in should be successful; however you should get a Template Not Found
error after logging in, because the dashboard.html
template has not been created yet. Let’s do that now:
<!-- app/templates/home/dashboard.html -->
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<div class="intro-header">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="intro-message">
<h1>The Dashboard</h1>
<h3>We made it here!</h3>
<hr class="intro-divider">
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Refresh the page. You’ll notice that the navigation menu still has the register and login links, even though we are already logged in. We’ll need to modify it to show a logout link when a user is already authenticated. We will also include a Hi, username!
message in the nav bar:
<!-- app/templates/base.html -->
<!-- In the head tag, include link to Font Awesome CSS so we can use icons -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Modify nav bar menu -->
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('home.dashboard') }}">Dashboard</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
<li><a><i class="fa fa-user"></i> Hi, {{ current_user.username }}!</a></li>
{% else %}
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% endif %}
</ul>
<!-- Modify footer menu -->
<ul class="list-inline">
<li><a href="{{ url_for('home.homepage') }}">Home</a></li>
<li class="footer-menu-divider">⋅</li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li class="footer-menu-divider">⋅</li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% endif %}
</ul>
Note how we use if-else
statements in the templates. Also, take note of the current_user
proxy provided by Flask-Login, which allows us to check whether the user is authenticated and to get the user’s username.
Logging out will take you back to the login page:
Attempting to access the dashboard page without logging in will redirect you to the login page and display the message we set in the app/__init__.py
file:
Notice that the URL is configured such that once you log in, you will be redirected to the page you initially attempted to access, which in this case is the dashboard.
Conclusion
That’s it for Part One! We’ve covered quite a lot: setting up a MySQL database, creating models, migrating the database, and handling registration, login, and logout. Good job for making it this far!
Watch this space for Part Two, which will cover the CRUD functionality of the app, allowing admin users to add, list, edit, and delete departments and roles, as well as assign them to employees.
Source : .