This article features my early struggles in understanding how to work with multiple database users. Once I learnt how to work with joined table inheritance, I appreciated the support SQLAlchemy provides to solve this. Anyways, if you would like to read more on where I begun from, continue reading.
Imagine you have a database table of students and another database table of teachers. Let us imagine again that the application we are interested in building should be able to load all the students and all the teachers, separately. How do you go about it?
I struggled with this problem for quite a while during my initial days of learning Flask. I knew how to load all students, and say, all their posts. However, when the need to create a teachers table came up, I fambled, and failed. Several months down the line, I revisited this concept with the courage that I could figure it out. And this tutorial is the outcome of that effort.
- Create a simple flask application
- Add students to the application
- Add teachers to the application
- Load all students and teachers
I have already created a simple application. You can browse this GitHub repository to learn more. We will build on it moving forward.
Web forms are used to collect user information from a web application. We are going to create two types of users: students and teachers. This means that we will need to create a registration form for each user type. Upon successful registration, each user should be able to log into their own account.
Flask provides us with the flask-wtf
package that allows for the creation of webforms. We will need to install it in our virtual environment.
(venv) $ pip3 install flask-wtf
Following the principle of separation of concerns, we will create a forms
module which will hold all the forms our application will need. Inside app/
subfolder, create an empty form called forms.py
.
(venv) $ touch app/forms.py
We will begin by creating a registration form. This form will collect the same information from both students and teachers.
forms.py: Create a Registration Form
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo
class RegisterForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
Flask-wtf
provides several fields and validators that we have used to create the registration form. The first is the StringField
which is used to collect a string of characters. Same to the PasswordField
. The SubmitField
is used to submit the form. The EqualTo
validator is used to compare the value of the password field to the value of the confirm password field. DataRequired
ensures that the field is not empty, otherwise clicking the submit button won't work.
Within the app/templates
folder, we will create a register.html
file. This file will contain the registration form. I will use flask-bootstrap
to create a quick form.
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_context %}
<div class="row">
<div class="col-sm12">
<h1>{{ title }}</h1>
<p>
{{ wtf.quick_form(form) }}
</p>
</div>
</div>
{% endblock %}
In a single line of code, we are able to display the registration form. We have used the wtf.quick_form
macro to create the form. The wtf.quick_form
function takes in a form as an argument. We will use the form
variable to store the form. The good thing here is that our form has all the styles needed to look good, thanks to the Bootstrap framework.
To see this form, we will need to create a route that will render the registration form. First, we will create a registration route that handles the registration of a student. Then we will create a route that handles the registration of a teacher.
routes.py: Create a Registration Route
# ...
from app.forms import RegisterForm
# ...
@app.route('/register/student', methods=['GET', 'POST'])
def register_student():
form = RegisterForm()
return render_template(
'register.html',
title='Register Student',
form=form
)
To easily access this view function, we will need to update our navigation bar in base.html
to include a link to student registration.
base.html: Link to Student Registration
{% block navbar %}
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<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" href=" # ">Users</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('register_student') }} ">Register Student</a></li>
</ul>
</div>
</div>
</nav>
{% endblock %}
If you try to access the registration page at this point, you will be greeted with a RuntimeError
because we have not provided a secret key. All webforms require a secret key to prevent cross site scripting attacks. We will need to create a secret key in config.py
.
config.py: Create a Secret Key
import os
class Config(object):
# Form
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
We are sourcing the value of the SECRET_KEY
from an environment variable. If the variable is not set, we will use the string you-will-never-guess
. This is just a safe fallback to ensure that the application does not crash in case the SECRET_KEY
value is missing. To add this enviroment variable, we will do so in the .env
file. Create this file in the top-level directory of our application.
(venv) $ touch .env
Update the .env
file with the following content:
SECRET_KEY=b'\xa4\x87\xd9%U\xa5@$\x1frs\x125\x9f\xe4:'
As the name suggests, this value should be secret, and hard to guess. I was able to obtain this value by running the following command in the terminal:
(venv) $ python3 -c "import os; print(os.urandom(16))"
The next step will be to register this configuration module within the application instance.
__init__.py: Register the Configuration Module
# ...
from config import Config
# ...
app.config.from_object(Config)
# ...
Now, reloading the application in your browser will display the registration form.
To register a teacher, we will need to create a route that handles the registration of a teacher. We will use the same template as the student registration route. Why so? Can't we use the same route if the template is the same? That will be discussed in subsequent sections.
route.py: Teacher Registration Route
# ...
@app.route('/register/teacher', methods=['GET', 'POST'])
def register_teacher():
form = RegisterForm()
return render_template(
'register.html',
title='Register Teacher',
form=form
)
Add a link in the navigation bar to the teacher registration page.
base.html: Add a Link to Teacher Registration
<!-- Previous code -->
{% block navbar %}
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<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" href=" # ">Users</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('register_student') }} ">Register Student</a></li>
<li><a href=" {{ url_for('register_teacher') }} ">Register Teacher</a></li>
</ul>
</div>
</div>
</nav>
{% endblock %}
Once a student or a teacher has registered we will need to store that information in a database. This will allow the application to remember the user's information during their subsequent visit.
Let us begin by installing two packages that will allow us to work with a database.
(venv) $ pip3 install flask-sqlalchemy flask-migrate
Flask-sqlalchemy
is a popular Object Relational Mapper (ORM) for SqlAlchemy. Instead of using raw SQL commands to interact with the database, this package uses high-level classes, objects and methods.
Flask-migrate
, on the other hand, is used to manage the database. Should there be any change to the database tables, then flask-migrate
is responsible for creating and managing these migrations.
Just like the other packages, ensure that register them in the application instance.
__init__.py: Register the Database Packages
# ...
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
# ...
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# ...
from app import models # <--- update
Before we can create our database, let us create some configurations that will be needed by the application in order to work with our database of choice, SQLite.
config.py: Create the Database Configuration
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# Database
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or\
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLite is perfect for the needs of our application at the moment. It if convinient to use with applications that are small and simple. However, if we were to use this application with a larger scale application, we would want to use a database that is more suitable and robust for larger scale applications.
Now we can create a models.py
file in the app
sub-directory to define our database.
(venv) $ touch app/models.py
models.py: Define the Database Model
from app import db
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self):
return f'Student: {self.username}'
We have defined a Student
model with four columns. The first column is always an id
of type Integer. The other three columns are strings. It is recommended to not store a user's password in its original form. Rather, it should be stored as a hash, a representation of itself. This is an additional security measure in the event that the database is compromised and the users' information is expose.
Since we have just defined a new model, we need to apply this change by creating a migration script for the student. On our terminal, let us run the following commands:
(venv) $ flask db init
# Output
Creating directory /home/harry/software_development/python/current_projects/load_multiple_users/migrations ... done
Creating directory /home/harry/software_development/python/current_projects/load_multiple_users/migrations/versions
... done
Generating /home/harry/software_development/python/current_projects/load_multiple_users/migrations/script.py.mako ... done
Generating /home/harry/software_development/python/current_projects/load_multiple_users/migrations/alembic.ini ... done
Generating /home/harry/software_development/python/current_projects/load_multiple_users/migrations/README ... done
Generating /home/harry/software_development/python/current_projects/load_multiple_users/migrations/env.py ... done
Please edit configuration/connection/logging settings in
'/home/harry/software_development/python/current_projects/load_multiple_users/migrations/alembic.ini' before
proceeding.
This command will create a migrations folder in the application's top-level directory. All database migrations scripts will be stored in this folder.
Next is to create a migration script for the student. This script will contain all the details of the student table.
(venv) $ flask db migrate -m "student table"
# Output
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'student'
INFO [alembic.autogenerate.compare] Detected added index 'ix_student_email' on '['email']'
INFO [alembic.autogenerate.compare] Detected added index 'ix_student_username' on '['username']'
Generating /home/harry/software_development/python/current_projects/load_multiple_users/migrations/versions/92f4003438
7a_students_table.py ... done
You will notice that the file app.db
, defined in our configurations, is created in the top-levle directory. Inspecting the migrations folder, you will see a new sub-folder called versions. This folder will contain all the migration scripts that we will create. You will notice that a script called 92f4003438
7a_students_table.py has been added. The number 92f4003438
7a will be different in your case.
To apply these changes, we will need to run the following command:
(venv) $ flask db upgrade
# Output
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 92f40034387a, students table
(load_multiple_users) harry@harry:~/software_development/python/current_projects/lo
Remember that for you to use the command flask
, you need to be in the top-level directory of the application.
We need to do the same for the teacher table.
models.py: Teacher Model
# ...
class Teacher(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self):
return f'Teacher: {self.username}'
Since this is a change to our database, we need to create a migration script for the teacher table.
(venv) $ flask db migrate -m "teacher table"
# Output
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'teacher'
INFO [alembic.autogenerate.compare] Detected added index 'ix_teacher_email' on '['email']'
INFO [alembic.autogenerate.compare] Detected added index 'ix_teacher_username' on '['username']'
Generating /home/harry/software_development/python/current_projects/load_multiple_users/migrations/versions/07e7001b07
bd_teachers_table.py ... done
Then apply the changes to the database:
(venv) $ flask db upgrade
# Output
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 92f40034387a -> 07e7001b07bd, teachers table
That's it! We have now created a database with two tables, Student
and Teacher
.
Sometimes, you may want to test the application in the shell. This is where you activate the Python interpreter in the context of the application. Typically, to activate a Python interpreter, you will need to run python3
in the terminal. However, if you want to access the database objects for example, you will need to run flask shell
. This is made possible by the flask shell_context_processor
function.
multiple_users.py: Shell context processor
from app import app, db
from app.models import Student, Teacher
@app.shell_context_processor
def make_shell_context():
return dict(
db=db,
Student=Student,
Teacher=Teacher
)
To test the application in the shell, run the following command:
(venv) $ flask shell
# Output
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
App: app [development]
Instance: /home/harry/software_development/python/current_projects/load_multiple_users/instance
Try accessing the db
object:
>>> db
<SQLAlchemy engine=sqlite:////home/harry/software_development/python/current_projects/load_multiple_users/app.db>
In a normal Python interpreter, this would not be possible:
(venv) $ python3
# Output
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> db
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
Flask-login
is a flask package that allows us to manage user sessions. We need to install it in our application.
(venv) $ pip3 install flask-login
Create a flask-login
object in the application instance as was with the other packages.
__init__.py: Initialize Flask-Login
# ...
from flask_login import LoginManager
# ...
login = LoginManager(app)
login.login_view = 'login'
# ...
As mentioned earlier, it is not a good idea to store a user's password in the database. Instead, we will use a password hashing algorithm to store the password.
We will do a random test to see if we can create a password hash. In the terminal, activate the python shell in the context of the application:
(venv) $ flask shell
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> hash = generate_password_hash('test')
>>> hash
# Output
'pbkdf2:sha256:260000$QT9jZ6D3a9qmEUBp$a56b0251f63aee5ace44bed2f36e21570c0e95ede52711d4449d7d929ece3f28'
>>> check_password_hash(hash, 'test')
# Output
True
>>> check_password_hash(hash, 'wrong')
# Output
False
The entire password hashing logic can be implemented in the models.py
file as two functions for both Student
and Teacher
.
models.py: Password Hashing
# ...
from werkzeug.security import generate_password_hash, check_password_hash
# ...
class Student(db.Model):
# ...
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Teacher(db.Model):
# ...
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Flask-login
expects certain properties to be implemented. There are four required items that we need to implement:
is_authenticated
: This is a boolean property that indicates whether the user is authenticated (True
) or not(False
).is_active
: This is a boolean property that indicates whether the user is active (True
) or not(False
).is_anonymous
: This is a boolean property that indicates whether the user is anonymous (True
) or not(False
).get_id
: This is a method that returns the user's id.
Flask-login
provides a mixin class called UserMixin
that provides these properties. We will use this mixin class to implement the properties for our Student
and Teacher
classes.
models.py: User Mixin
# ...
from flask_login import UserMixin
class Student(UserMixin, db.Model):
# ...
class Teacher(UserMixin, db.Model):
# ...
At the moment, Flask-login
knows nothing about databases. We need to tell it how to load a user from the database. Given a user ID, each user can be loaded from the database.
models.py
# ...
from app import login
@login.user_loader
def load_student(id):
return Student.query.get(int(id))
@login.user_loader
def load_teacher(id):
return Teacher.query.get(int(id))
# ...
The ID passed to the function is string, so we need to convert it to an integer using the int()
function.
Before a user can log in, they need to register. Let us update our registration routes to store a user's information in the database.
routes.py: Add user to the database
from app import db
# ...
@app.route('/register/student', methods=['GET', 'POST'])
def register_student():
form = RegisterForm()
if form.validate_on_submit():
student = Student(
username=form.username.data,
email=form.email.data
)
student.set_password(form.password.data)
db.session.add(student)
db.session.commit()
flash('Congratulations, you are now a registered student!')
return redirect(url_for('login_student'))
return render_template(
'register.html',
title='Register Student',
form=form
)
@app.route('/register/teacher', methods=['GET', 'POST'])
def register_teacher():
form = RegisterForm()
if form.validate_on_submit():
teacher = Teacher(
username=form.username.data,
email=form.email.data
)
teacher.set_password(form.password.data)
db.session.add(teacher)
db.session.commit()
flash('Congratulations, you are now a registered teacher!')
return redirect(url_for('login_teacher'))
return render_template(
'register.html',
title='Register Teacher',
form=form
)
To begin, we will create login routes for both Student
and Teacher
.
routes.py: Login Routes
# ...
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user, login_required
from app.forms import LoginForm
from app.models import Student, Teacher
@app.route('/login/student')
def login_student():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
student = Student.query.filter_by(username=form.username.data).first()
if student is None or not student.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login_student'))
login_user(student, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template(
'login.html',
title='Login Student',
form=form
)
@app.route('/login/teacher')
def login_teacher():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
teacher = Teacher.query.filter_by(username=form.username.data).first()
if teacher is None or not teacher.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login_teacher'))
login_user(teacher, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template(
'login.html',
title='Login Teacher',
form=form
)
@app.route('/logout/student')
def logout_student():
logout_user()
return redirect(url_for('login_student'))
@app.route('/logout/teacher')
def logout_teacher():
logout_user()
return redirect(url_for('login_teacher'))
If the user is authenticated, he will automatically be redirected to the index
page. If not, the user will be required to fill in the correct credentials to access his account. I have used the flash()
function to display a friendly user feedback if the login attempt is unsuccessful.
Remember that we have used the login_required
decorator to protect the respective index pages of users. However, with all the changes we have made, we need to update the login.login_view
property in the application's instance to reflect the changes.
__init__.py: Update login.login_view
# ...
login.login_view = 'login_student'
login.login_view = 'login_teacher'
Our login form will be a simple form with two fields: username
and password
.
forms.py: Login Form
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Login')
Just like the registration template, we will use flask-bootstrap
's wtf.quick_form
to create a login form. First, create an empty login.html
file.
(venv) $ touch app/templates/login.html
Then update it with the following code:
login.html: Display Login Form
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_context %}
<div class="row">
<div class="col-sm12">
<h1>{{ title }}</h1>
<p>
{{ wtf.quick_form(form) }}
</p>
</div>
</div>
{% endblock %}
Add a login link for both the student and the teacher in the base.html
file.
base.html: Login Links
{% block navbar %}
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% if student %}
<a class="navbar-brand" href=" {{ url_for('index_student') }} ">Index Student</a>
{% elif teacher %}
<a class="navbar-brand" href=" {{ url_for('index_teacher') }} ">Index Teacher</a>
{% else %}
<a class="navbar-brand" href=" # ">Users</a>
{% endif %}
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
{% if student %}
<li><a href=" {{ url_for('logout_student') }} ">Logout</a></li>
{% elif teacher %}
<li><a href=" {{ url_for('logout_teacher') }} ">Logout</a></li>
{% endif %}
{% else %}
<li><a href=" {{ url_for('register_student') }} ">Register Student</a></li>
<li><a href=" {{ url_for('register_teacher') }} ">Register Teacher</a></li>
<li><a href=" {{ url_for('login_student') }} ">Login Student</a></li>
<li><a href=" {{ url_for('login_teacher') }} ">Login Teacher</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% endblock %}
Notice how we have used the if
statement to check if the user is a student or a teacher. If the user is a student, he will be redirected to the index_student
page. If the user is a teacher, he will be redirected to the index_teacher
page. If the user is not authenticated, he will be redirected to the login_student
or login_teacher
page.
To display the flash message seen earlier, we will update the block content
with the following code:
base.html: Flash Message
{% block content %}
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="row">
<div class="col-sm12">
{% for message in messages %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{% block app_context %}{% endblock %}
</div>
{% endblock %}
We retrieve the flash messages from get_flashed_messages()
function and save them in a messages
variable. We can then loop through the messages and display them.
At the moment, both the teacher and the student have access to the same home page. We would like to ensure that each user has access to the right home page, in the event that we may consider to use this page as a dashboard where only the current user's information is to be viewed.
Let us update our routes
module.
routes.py: Home pages
# ...
from flask_login import login_required
@app.route('/home/student')
@login_required
def index_student():
student = Student.query.first()
return render_template(
'index_student.html',
title='Home Student',
student=student
)
@app.route('/home/teacher')
@login_required
def index_teacher():
teacher = Teacher.query.first()
return render_template(
'index_teacher.html',
title='Home Teacher',
teacher=teacher
)
We have used the login_required
decorator to ensure that the user is authenticated before accessing the home page. Let us create these two templates.
(venv) $ touch app/templates/index_student.html app/templates/index_teacher.html
In these templates, we are going display some of the user's information. Add these information to both templates, changing where appropriate. We will show you how to display a student's information below.
index_student.html: Display Student Information
{% extends 'base.html' %}
{% block app_context %}
<div class="row">
<div class="col-sm12">
<h1>Hi, {{ student.username }}</h1>
<p>Email: {{ student.email }}</p>
</div>
</div>
{% endblock %}
<!-- Teacher template -->
<!-- Update the teacher's template accordingly -->
Remember to update all the redirect URLs from index
to index_student
or index_teacher
. You should be able to see this:
Try registering a new student and a new teacher. You should be able to see their information on the home page.
Our application has the errors
module. As it is, the redirect link will not work, since /index
does not exist any more. Do you have an idea how you can improve this?