Last time we connected a database to our app, and how to add records. We also added a backend interface to our app to allow owners to add and remove records. We configured various flask extensions and added some CSS
styling to our HTML
templates. Today we’re going to be looking at; managing users, adding a cart and implementing a checkout process.
Objectives;
- User management
- Using Blueprints
- Restricting access to owners
Creating a checkout processExperimenting
Say hello
to Blueprints
First we will start off by adding a Blueprint
for authentication.
What is a blueprint
? A blueprint is simply a collection of views and layouts.
Blueprints
simplify what would otherwise be a hodge-podge
of views and layouts.
It does this by adding structure to your app;
➜ app tree -L 3
.
├── assets
│ ├── js
│ │ └── main.coffee
│ └── scss
│ ├── _footer.scss
│ ├── main.scss
│ ├── _menugrid.scss
│ ├── _navigation.scss
│ └── _variables.scss
├── auth.py
├── create_db.py
├── favicon.ico
├── __init__.py
├── __init__.pyc
├── models.py
├── static
│ ├── css
│ │ └── main.css
│ ├── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ └── site.webmanifest
│ └── js
│ └── main.js
├── templates
│ ├── about.html
│ ├── auth
│ │ ├── assets
│ │ ├── login.html
│ │ └── signup.html
| |─── blog
│ | ├── create.html
│ | ├── index.html
│ | └── update.html
│ ├── base.html
│ ├── coupons.html
│ ├── home.html
│ ├── menu.html
│ ├── navigation.html
│ └── support.html
├── views.py
└── views.pyc
Structure
As you can see we have added quite a number of things to our app!
Below is a list of files we have added or modified;
auth.py
(added)__init__.py
(modified)models.py
(modified)templates/auth
(added)templates/blog
(added)views.py
(modified)
Let’s start from the top;
Getting started with Blueprints
First we need to define our Blueprint
in auth.py
;
import functools
from flask import ( Blueprint, flash, redirect, render_template, request, url_for, g)
bp = Blueprint('auth', __name__, url_prefix='/auth')
The __name__
refers to auth
because the blueprint needs to know where it is defined.
The url_prefix
as it’s name indicates, will be prepended to all URLs
associated with this Blueprint
.
The next step would be to import and register the Blueprint
with our app.
Our new app/__init__.py
from flask import Flask
raj = Flask(__name__)
raj.config.from_object('config')
from app import models, auth, views
raj.register_blueprint(auth.bp)
WOAH! Our new __init__.py
does not look like what we had previously from part 2!
So where did all the other code go?
Old app/__init__.py
1. from flask import Flask
2. from flask_scss import Scss # went to views.py
3. from flask_admin import Admin # went to views.py
4. from flask_sqlalchemy import SQLAlchemy # went to models.py
5.
6. raj = Flask(__name__)
7. raj.config.from_object('config')
8. db = SQLAlchemy(raj) # went to models.py
9.
10. Scss(raj, asset_dir='app/assets/scss', static_dir='app/static/css') # went to views.py
11. admin = Admin(raj) # went to models.py
12.
13. from app import models, views
Line 8
and line 11
both went to models.py
because that’s were they are used.
It makes sense to initialize the db
in our models.py
, much less so for admin
.
You could still import admin
in your models.py
like you did before - if you want.
Updated models.py
...
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from app import raj
db = SQLAlchemy(raj)
admin = Admin(raj)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
...
line 10
went to views.py
because they are primarily associated with views
.
Updated views.py
from flask import render_template, url_for
from flask_scss import Scss
from app import raj
app_name = "Raj's Restaurant App"
Scss(raj, asset_dir='app/assets/scss', static_dir='app/static/css')
@raj.route('/')
@raj.route('/index')
def index():
return render_template('index.html', title="Raj's Restaurant")
Our app should still be able to run as it did before since the only new thing we have added thus far is a Blueprint
.
Lets go ahead and run
our app to make sure everything still runs. It is recommended to fix any errors you get before going forward.
You should see something familiar when you run
your app;
(pyenv) ➜ raj ./run.py
* Serving Flask app "app" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://localhost:3000/ (Press CTRL+C to quit)
* Restarting with stat
[2019-09-24 12:39:21,361] INFO in flask_scss: Pyscss loaded!
* Debugger is active!
* Debugger PIN: 214-588-298
Back to our Blueprint
!
All we have done is register our auth
Blueprint
with our app.
The next step is to define the routes
for our authentication pages;
Sign Up
Log In
1. Sign Up
What is this route
going to do?
- Take in a user’s information
- Make sure the user is not already registered
- Add the user into our
database
What type of information are we taking from a new user?
- firstname
- Lastname
- user_name
- user_email
- user_password
So what is the problem???
If I remeber correctly, our User
model
only has three attributes;
app/models.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
Update models.py
Before we update our models.py
, let us close any databse
sessions;
$ (pyenv) python
>>> from app.models import db
>>> db.session.close()
>>> exit()
Ok, now we can go ahead and update our models.py
;
app/models.py
... # Other import statements
import bcrypt
class User(db.Model):
__tablename__ = 'users'
firstname = db.Column(db.String(127))
lastname = db.Column(db.String(127))
email = db.Column(db.String(120), unique=True, primary_key=True)
username = db.Column(db.String(80), unique=True)
password = db.Column(db.String(127), unique=True)
def __init__(self, username, email, password):
self.username = username
self.email = email.lower()
self.set_password(password)
def set_password(self, password):
self.password = bcrypt.hashpw(password, bcrypt.gensalt())
def check_password(self, password):
return bcrypt.checkpw(password, self.password)
def __repr__(self):
return '<Firstname: %r, Lastname: %r, Email: %r >' %\
(self.firstname, self.lastname, self.email)
... ### rest of the code
All the other code stays the same.
You will notice that we have also added a number of functions to our class,
__init__
set_password
check_password
__init__
This is simply a constructor for our User
.
It allows us to easily create users.
set_password
and check_password
When we create new User
a password hash is generated which is then stored as a password
.
This is also why we need bcrypt
.
I should also add that you should never store plain user passwords
in your app
.
You should also avoid storing hashed
passwords
unsalted.
If you’re curious, salting simply adds entropy
to your password before it is hashed
. This prevents rainbow
attacks.
Before we go any further, it is best if we deleted our old database
because we
have not set it up for migrations
so any updates to our models
are going to break SQL
queries to our db
because it is using previously defined models
.
~> psql postgres
~> drop raj_db
~> createdb raj_db
Time to Signup,
Ok, ok, so we can now define our signup
route.
We will need to import the database
(db
) to check if our new user has already signed up.
We also need the database to add
them if the are indeed a new user.
Remeber that we initialized our database in our models.py
app/auth.py
from app.models import User, db
...
db.create_all()
db.session.commit()
# Sign Up view
@bp.route('/signup', methods=('GET', 'POST'))
def signup():
if request.method == 'POST':
firstname = request.form['firstname']
lastname = request.form['lastname']
user_name = request.form['username']
user_email = request.form['email']
user_password = request.form['password']
error = None
if not user_name or firstname or lastname:
error = 'All names are required.'
elif not user_email:
error = 'Email is required.'
elif User.query.filter_by(email=user_email).first() is not None:
error = 'The User {} is already registered.'.format(username)
elif not user_password:
error = 'Password can not be empty!'
if error is None:
new_user = User(username=user_name, email=user_email, password=user_password)
new_user.firstname = firstname
new_user.lastname = lastname
db.session.add( new_user )
db.session.commit()
return redirect(url_for('auth.login'))
flash(error)
return render_template('auth/signup.html')
... # other code
We obtain user information via the request.form
and check to see if the form has been filled.
Then we check our db
for anyone with the same email
since this is defined as the primary key
in our User
model
.
This ensures that no two users can have the same email
address.
If there are no errors we create a User
and add them to our db
all in one line.
The password hasing is handled by the set_password
function of our User
model.
If everything goes well we redirect the user to the login
page, otherwise we render the
auth/signup.html
template.
Wait, where’s the auth/signup.html
template? We haven’t created it yet! Lets do so;
auth/signup.html
{% extends 'base.html' %}
{% block header %}
<h1 class="su-title">{% block title %}Sign Up{% endblock %}</h1>
{% endblock %}
{% block content %}
<div class="signup-form">
<form method="post">
<label for="firstname">First Name: </label>
<input name="firstname" id="firstname" required>
<label for="lastname">Last Name: </label>
<input name="lastname" id="lastname" required>
<label for="email">Email: </label>
<input name="email" id="email" required>
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Signup">
</form>
</div>
{% endblock %}
All the fields have to match what we ask for in the signup
route app/auth.py
.
2. Log In
Ok, so a user has signed up, how or where do they login? We need to define another route for this in our auth.py
;
app/auth.py
... # Sign up code
# Login view
@bp.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
user_name = request.form['username']
user_password = request.form['password']
error = None
user_u = User.query.filter_by(username=user_name).first()
if user_u is None:
error = 'Username does not exist.'
elif not user_u.check_password(user_password):
error = 'Incorrect password.'
if error is None:
# session.clear()
# session['user_id'] = user['id']
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
@bp.route('/logout')
def logout():
# session.clear()
db.session.close()
return redirect(url_for('index'))
@login_manager.user_loader
def load_user(email):
return User.query.get(str(email))
As before, we obtain our information via the request.form
.
We then make sure that the user actually exists before checking if their password is correct.
It is important to note that there are two matches being done here;
- Does this user exist.
- Is their password correct.
If the user exists we then use that user’s check_password
function as defined in our models to authenticate the plain-text
password we’ve obtained.
Now in our case, two users can have the same username
because of the way our signup
works.
This becomes dangerous because if one user gets information that he shares the same username with many other users, he can guess a correct password
and login to an account that does not belong to him.
However, no two users can have the same email
address. So the solution to our problem would be to take in a user’s email
address instead of their username
.
... # other code
def login():
if request.method == 'POST':
user_email = request.form['email']
user_password = request.form['password']
error = None
user_u = User.query.filter_by(email=user_email).first()
... # other code
Now a user either exists or they don’t because all User
emails are unique by definition.
Well of course if one user happens to know the email addresses of other users he can go to town and start guessing their passwords. (Good luck!)
Let’s get back to the other “good” users. If the login is successful, we redirect the user to the index page.
If the login is not successful, we render
the auth/login.html
template which does not exist.
Lets create it.
auth/login.html
{% extends 'templates/base.html' %}
{% block header %}
<h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Log In">
</form>
{% endblock %}
logout
@bp.route('/logout')
def logout():
# session.clear()
db.session.close()
return redirect(url_for('index'))
We end the current session and logout a user
The last functions of app/auth.py
load_user
@login_manager.user_loader
def load_user(email):
return User.query.get(str(email))
We simply return the user with an email that matches one we have provided but to do this we need a login manager.
app/auth.py
import functools
from app import raj
from app.models import User, db
from flask_login import LoginManager, login_user, logout_user, current_user, login_required
from flask import ( Blueprint, flash, redirect, render_template, request, url_for, g)
bp = Blueprint('auth', __name__, url_prefix='/auth')
login_manager = LoginManager()
login_manager.init_app(raj)
db.create_all()
db.session.commit()
... # rest of code
Who is a User anyway
If you look at an updated version of our base.html
;
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Raj's Restaurant" />
<meta name="keywords" content="HTML, CSS, JavaScript" />
<meta name="author" content="Lehbyte" />
<meta name="application-name" content="Raj's Application" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
<!-- <link rel="stylesheet" type="text/css" href="./static/css/main.css" /> -->
<link
rel="stylesheet"
type="text/css"
href="{{url_for('static',filename='css/main.css')}}"
/>
</head>
<!-- body -->
<body>
<main>
<!-- authentication -->
<div class="auth">
{% if user %}
<p class="loggedin">{{ user["username"] }}</p>
<button><a href="{{ url_for('auth.logout') }}">Log Out</a></button>
{% else %}
<div class="Signup">
<button><a href="{{ url_for('auth.signup') }}">SignUp</a></button>
</div>
<div class="login">
<form method="post">
<input
class="user"
type="text"
id="user"
placeholder="Username"
required
/>
<input
class="password"
type="password"
id="password"
placeholder="Password"
required
/>
<input type="submit" value="Log In" />
</form>
</div>
{% endif %}
</div>
<!-- Main content -->
<div class="container">
<header>
{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %} {% block content %}{% endblock %}
</div>
</main>
</body>
<!--body -->
<!-- footer -->
<footer>
<div class="footer-wrap">
<div class="footer-left">
<h3>Left section</h3>
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum."
</div>
<div class="footer-middle">
<h3>Middle section</h3>
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum."
</div>
<div class="footer-right">
<h3>Right Section</h3>
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum."
</div>
</div>
<p>Copyright Raj's Restaurant © 2019</p>
</footer>
</html>
Specifically under authentication;
You will see that we check to see if a user
exists in our app
’s context and then
render different html
code based on that.
Where does this user get passed to our base template? Lets take a look at our updated views.py
app/views.py
from flask import request, render_template, redirect, url_for, g
from auth import login_manager, current_user
from functools import wraps
from flask_scss import Scss
from app import raj
app_name = "Raj's Restaurant App"
Scss(raj, asset_dir='app/assets/scss', static_dir='app/static/css')
def templated(template=None):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
template_name = template
if template_name is None:
template_name = request.endpoint.replace('.','/') + '.html'
ctx = f(*args, **kwargs)
if ctx is None:
ctx = {}
elif not isinstance(ctx, dict):
return ctx
return render_template(template_name, **ctx)
return decorated_function
return decorator
@raj.before_request
def before_request():
g.user = current_user
@raj.route('/')
@raj.route('/home')
@raj.route('/index')
@templated()
def home():
user = g.user
return dict(title=app_name, user=user)
@raj.route('/about')
@templated('about.html')
def about():
return dict(title=app_name)
@raj.route('/coupons')
def coupons():
return render_template('coupons.html',title=app_name)
@raj.route('/blog')
def blog():
return render_template('blog/index.html',title=app_name)
@raj.route('/support', methods=['GET','POST'])
def support():
return render_template('support.html',title=app_name)
Whoah!
On line 2
we import a login_manager
and current_user
from auth
.
Then make sure that g.user
is the current_user
before any request.
We will work out the details in the next tutorial but for now all you need to know is that a guest is also a user within the app’s context.
templated - ed
This was just fancy code for simplifying template rendering, nothig too special.
Front matter
You may have noticed a change in the way we load static files
<link href="{{url_for('static',filename='css/main.css')}}"/>
Is better because it simplifies css
loading for Blueprints
.
If we tried to use the old method of loading our static files;
<link rel="stylesheet" type="text/css" href="./static/css/main.css" />
We would almost endup with a 404
resource not found error for the case of auth/signup.html
and auth/login.html
.
Why? Because our auth
Blueprint
prefixes every URL
with /auth
so a request for /static/css/main.css
would become /auth/static/css/main.css
which does not exist.
Fancy Foot Work!
We have also added a footer on our frontend.
assets/scss/_footer.scss
footer{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: normal;
font-size: 16px;
color: burlywood;// #e0b172;
background-color: black;
position: relative;
}
.footer-wrap{
display: flex;
flex-direction: row;
padding: 20px;
}
.footer-left,
.footer-middle,
.footer-right{
padding: 10px;
}
footer h3,
footer p{
color: snow;
padding: 10px;
text-align: center;
}
footer p{
padding: 10px;
}
@supports (display: flex){
@media screen and (max-width: 516px){
.footer-wrap{
flex-direction: column;
}
}
}
Do not forget to @import 'footer';
at the bottom of your assets/scss/main.scss
file.
Let’s also make our grid
layout work on smaller screens;
assets/scss/_menugrid.scss
... /* previous code */
@media screen and (max-width: 516px){
.food-grid{
grid-template-columns: 100%;
}
.food-item{
background-color: seashell;
}
}
You can view other style changes in this project’s repo.
Part 4
Next time, we will be looking at;
- Defining checkout process
- How to add images to our app
- Creating a
blog
Blueprint
Happy coding. 🙂