Introduction
So, I’ve been doing a lot of work on python framework and libraries as part of my first web app. This will be very much something that is cobbled together from a few tutorials, how to guides, my work on treehouse, and some interests of my own. I’m not expecting it to be a top quality product by any stroke of the imagination, and nor am I expecting it to have any relevance to anyone else besides myself. I am, however, going to document the work that I do along the way (both in this blog and via comments in the code) to help with the learning process, and if anyone else finds that of value, that’s an additional bonus for them.
So, in short, I want to build a web app that will allow me to do the following:
- To do list
- Shopping List
In addition, it will have the following features:
- User log in and authentication.
Tools that I am expecting to make use of:
- Python
- Flask
- Flask_login
- SQLAlchemy
Other resources:
Another point: it will be named Elsinore (after Hamlet’s castle).
Building my first web app – part one
The first part of my web app will be setting up the environment, creating the structure and creating the user authentication models. I will mostly be working from this tutorial: https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login by Anthony Herbert.
Setting Up
The is what the finished directory structure will look like.
Create a directory (elsinore_app)
Go to that directory
Create a virtual environment: python3 -m venv elsinore
Activate it: source elsinore/bin/activate
install the packages we need: pip install flask flask-sqlalchemy flask-login
Creating the Main App File
Next, we need to create the project directory: mkdir project.
The first file we will make is __init__.py
This file creates our database, and registers the blueprints.
We will set up SQLAlchemy here.
This is the code:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
# init SQLAlchemy so we can use it later in our models
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config[‘SECRET_KEY’] = ‘secret-key-goes-here’
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘sqlite:///db.sqlite’
db.init_app(app)
# blueprint for auth routes in our app
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
# blueprint for non-auth parts of app
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
Adding Routes
The next step are the routes. The routes are like the different paths used by pages to access templates.
The example has two blueprints. The first one will have home and profile pages. If a user tries to access the profile (AND OTHER?) pages without being logged in, they will be sent to the login route.
In the auth blueprint (i.e. not the main blueprint), we get the login and the sign up pages. We will also handle POST requests and the log out route.
Let’s create the first part of it first – main.py
This is the code:
from flask import Blueprint
from . import db
main = Blueprint(‘main’, __name__)
@main.route(‘/’)
def index():
return ‘Index’
@main.route(‘/profile’)
def profile():
return ‘Profile’
Now we need to create the auth.py file for the auth blueprint
from flask import Blueprint
from . import db
auth = Blueprint(‘auth’, __name__)
@auth.route(‘/login’)
def login():
return ‘Login’
@auth.route(‘/signup’)
def signup():
return ‘Signup’
@auth.route(‘/logout’)
def logout():
return ‘Logout’
Set the FLASK_APP and FLASK_DEBUG values;
export FLASK_APP=project
export FLASK_DEBUG=1
FLASK_APP – needs to point to where the create_app function is located. We will point it to the project directory.
FLASK_DEBUG is an environment variable. Setting it to 1 means that the debugger will run which will show errors in the browser.
To run the app we do flask run
TO check it is running, we can go to:
Creating Templates
The next step is to create the templates. These are like the dynamic pages that serve the content and implement the functionality. We will have templates for base, index, profile, login and signup.
Firstly, create a new folder in the project file called templates. We will create html files. Firstly, in base.html:
<!DOCTYPE html>
<html>
<head>
<meta charset=”utf-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
<meta name=”viewport” content=”width=device-width, initial-scale=1″>
<title>Flask Auth Example</title>
<link rel=”stylesheet” href=”https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css” />
</head>
<body>
<section class=”hero is-primary is-fullheight”>
<div class=”hero-head”>
<nav class=”navbar”>
<div class=”container”>
<div id=”navbarMenuHeroA” class=”navbar-menu”>
<div class=”navbar-end”>
<a href=”{{ url_for(‘main.index’) }}” class=”navbar-item”>
Home
</a>
<a href=”{{ url_for(‘main.profile’) }}” class=”navbar-item”>
Profile
</a>
<a href=”{{ url_for(‘auth.login’) }}” class=”navbar-item”>
Login
</a>
<a href=”{{ url_for(‘auth.signup’) }}” class=”navbar-item”>
Sign Up
</a>
<a href=”{{ url_for(‘auth.logout’) }}” class=”navbar-item”>
Logout
</a>
</div>
</div>
</div>
</nav>
</div>
<div class=”hero-body”>
<div class=”container has-text-centered”>
{% block content %}
{% endblock %}
</div>
</div>
</section>
</body>
</html>
Next, in the same directory, index.html
{% extends “base.html” %}
{% block content %}
<h1 class=”title”>
Flask Login Example
</h1>
<h2 class=”subtitle”>
Easy authentication and authorization in Flask.
</h2>
{% endblock %}
Next, create a login page (login.html):
{% extends “base.html” %}
{% block content %}
<div class=”column is-4 is-offset-4″>
<h3 class=”title”>Login</h3>
<div class=”box”>
<form method=”POST” action=”/login”>
<div class=”field”>
<div class=”control”>
<input class=”input is-large” type=”email” name=”email” placeholder=”Your Email” autofocus=””>
</div>
</div>
<div class=”field”>
<div class=”control”>
<input class=”input is-large” type=”password” name=”password” placeholder=”Your Password”>
</div>
</div>
<div class=”field”>
<label class=”checkbox”>
<input type=”checkbox”>
Remember me
</label>
</div>
<button class=”button is-block is-info is-large is-fullwidth”>Login</button>
</form>
</div>
</div>
{% endblock %}
Next, signup.html:
{% extends “base.html” %}
{% block content %}
<div class=”column is-4 is-offset-4″>
<h3 class=”title”>Sign Up</h3>
<div class=”box”>
<form method=”POST” action=”/signup”>
<div class=”field”>
<div class=”control”>
<input class=”input is-large” type=”email” name=”email” placeholder=”Email” autofocus=””>
</div>
</div>
<div class=”field”>
<div class=”control”>
<input class=”input is-large” type=”text” name=”name” placeholder=”Name” autofocus=””>
</div>
</div>
<div class=”field”>
<div class=”control”>
<input class=”input is-large” type=”password” name=”password” placeholder=”Password”>
</div>
</div>
<button class=”button is-block is-info is-large is-fullwidth”>Sign Up</button>
</form>
</div>
</div>
{% endblock %}
Finally, profile.html:
{% extends “base.html” %}
{% block content %}
<h1 class=”title”>
Welcome, Anthony!
</h1>
{% endblock %}
Now that we’ve created the pages, we can fix up the routes so that they point to these new pages:
from flask import Blueprint, render_template
…
@main.route(‘/’)
def index():
return render_template(‘index.html’)
@main.route(‘/profile’)
def profile():
return render_template(‘profile.html’)
We can also update auth.py in the same way:
from flask import Blueprint, render_template
…
@auth.route(‘/login’)
def login():
return render_template(‘login.html’)
@auth.route(‘/signup’)
def signup():
return render_template(‘signup.html’)
Creating User Models
The user mode. – represents what it means for the app to have a user.
It’s like the template for a database. It needs to include email address, password, name.
Could also add things like birthday, profile picture, location, user preferences.
HOW WOULD I ADD A PROFILE PICTURE?
Models in FLASK – classes that are then translated to tables in the databases.
Will create a file called models to store the class.
from . import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True) # primary keys are required by$
email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(100))
name = db.Column(db.String(1000))
location = db.Column(db.String(100))
Configure the Database
Using the SQLite database. Easiest option is to do this with FLASK-SQLAlchemy.
Path to the database is already in in the __init__.py file.
Just need to create the database in the Python REPL
To enter the REPL, python3. To exit: exit()
This is how to create the database:
from project import db, create_app
db.create_all(app=create_app()) # pass the create_app result so Flask-SQLAlchemy gets the configuration.
Can now see db.sqlite in the project directory.
This will have our user table in it.
Except, when I tried to look into it via DB Browser, I couldn’t see anything.
Setting up the Authorization Function
In the sign up function, we take the data from the form and add it to our database.
Need to check that the user doesn’t already exist. Also need to hash the password – don’t store passwords in plaintext.
This takes place via POST form data.
We will create a function (def) called signup_post(). This will be on the /signup route, but POST method, not get.
Code here:
from flask import Blueprint, render_template, redirect, url_for, request
from werkzeug.security import generate_password_hash, check_password_hash
from .models import User
from . import db
…
@auth.route(‘/signup’, methods=[‘POST’])
def signup_post():
email = request.form.get(’email’)
name = request.form.get(‘name’)
password = request.form.get(‘password’)
user = User.query.filter_by(email=email).first() # if this returns a user, then the email already exists in database
if user: # if a user is found, we want to redirect back to signup page so user can try again
return redirect(url_for(‘auth.signup’))
# create a new user with the form data. Hash the password so the plaintext version isn’t saved.
new_user = User(email=email, name=name, password=generate_password_hash(password, method=’sha256′))
# add the new user to the database
db.session.add(new_user)
db.session.commit()
return redirect(url_for(‘auth.login’))
Testing the Sign Up Method
Should be able to now create a new user. Do this using flask run and then using the form.
Can verify this in two ways – firstly, a user gets added to the table, can also check with the same email address too.
Okay, this didn’t actually work for me. In fact, I had to manually create the table using DB Browser (user, with the fields as defined in the model). After that it appeared to work fine, in the I could see the data being added.
Then we can add a flash message to indicate if the email address is already in use.
In auth.py:
from flask import Blueprint, render_template, redirect, url_for, request, flash
…
@auth.route(‘/signup’, methods=[‘POST’])
def signup_post():
…
if user: # if a user is found, we want to redirect back to signup page so user can try again
flash(‘Email address already exists’)
return redirect(url_for(‘auth.signup’))
And then in signup.html:
…
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class=”notification is-danger”>
{{ messages[0] }}. Go to <a href=”{{ url_for(‘auth.login’) }}”>login page</a>.
</div>
{% endif %}
{% endwith %}
<form method=”POST” action=”/signup”>
Adding the Login Method
The is similar to the sign up function. Need to take the user information, and then compare with the table in the database. If emails match, we can test the passwords (hashed).
Then we can log them into the system. We do this by using Flask-Login.
login_user -> creates a new session for the user that will persist for the length of the user’s session. This will allow the user to view protected pages.
We want these protected pages to be the to do list and the shopping list, right?
Create a new route for the posted data.
Once a user has successfully logged in, they go to the profile page.
On the profile page, I’ll put links to there pages?
This is the new route to add to auth.py
…
@auth.route(‘/login’, methods=[‘POST’])
def login_post():
email = request.form.get(’email’)
password = request.form.get(‘password’)
remember = True if request.form.get(‘remember’) else False
user = User.query.filter_by(email=email).first()
# check if the user actually exists
# take the user-supplied password, hash it, and compare it to the hashed password in the database
if not user or not check_password_hash(user.password, password):
flash(‘Please check your login details and try again.’)
return redirect(url_for(‘auth.login’)) # if the user doesn’t exist or password is wrong, reload the page
# if the above check passes, then we know the user has the right credentials
return redirect(url_for(‘main.profile’))
Now add in the block in the template (login.html)
…
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class=”notification is-danger”>
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<form method=”POST” action=”/login”>
Can now identify when a user has been logged in. Need to use the UserMixn to the Flask-Login to make it start to work.
In models.py
from flask_login import UserMixin
from . import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(100))
name = db.Column(db.String(1000))
Then we specify our user loader. Tells Flask-Login how to find a specific user from the ID stored in their session cookie. Can add this to our create_app function. in __init__.py
…
from flask_login import LoginManager
…
def create_app():
…
db.init_app(app)
login_manager = LoginManager()
login_manager.login_view = ‘auth.login’
login_manager.init_app(app)
from .models import User
@login_manager.user_loader
def load_user(user_id):
# since the user_id is just the primary key of our user table, use it in the query for the user
return User.query.get(int(user_id))
Then we can add the login_user function before we redirect to the profile page.
from flask_login import login_user
from .models import User
…
@auth.route(‘/login’, methods=[‘POST’])
def login_post():
…
# if the above check passes, then we know the user has the right credentials
login_user(user, remember=remember)
return redirect(url_for(‘main.profile’))
Protecting Pages
Now, let’s configure the profile page – so that it shows the name of the person logged in.
We can also set up pages so that they require the login required function – this prevents unlogged in users from seeing the route.
Inside login_required, we can use the current user. Can access attributes of this with dot notations.
Let’s make that work in main.py:
from flask_login import login_required, current_user
…
@main.route(‘/profile’)
@login_required
def profile():
return render_template(‘profile.html’, name=current_user.name)
Then in profile html, let’s make it work:
…
<h1 class=”title”>
Welcome, {{ name }}!
</h1>
Logout view
We can call the logout_user function in a route for logging out.
Also need to include login-required – doesn’t make sense to log out if you’re not logged in.
In auth.py:
from flask_login import login_user, logout_user, login_required
…
@auth.route(‘/logout’)
@login_required
def logout():
logout_user()
return redirect(url_for(‘main.index’))