Building A Simple Blog Application with Flask
Introduction
What is Flask?
In this tutorial, you will be introduced to Flask, a small and lightweight Python web framework that simplifies the process of creating web applications. This framework is especially useful for new developers as it is easy to use and can be built with just one Python file. Additionally, you will use the Bootstrap toolkit to enhance the visual appearance of your application and make it responsive for mobile browsers without needing to write your own HTML, CSS and JavaScript.
Flask utilizes the Jinja template engine, which enables you to create dynamic HTML pages using familiar Python concepts such as variables, loops and lists. You will be using these templates as part of this project.
This tutorial will guide you through building a small web blog using Flask, SQLAlchemy and Python 3. The application will allow users to sign up as a user and create, edit, delete and view posts in the database. This sums up what is called CRUD (Create, Read, Update, Delete) operations. These are the four basic operations a software application should be able to perform.
The Github link to the code in this tutorial will be available at the bottom.
Getting started
Before we begin, you're going to need the following:
A Python 3 environment running on your local machine
Working knowledge of Python programming language
Basic understanding of HTML and CSS
Once you have all three of the above prerequisites, you can proceed with the actual task of building the blog.
Let's begin.
Installing Flask and SQLAlchemy
Firstly, you need to set up a project directory. This directory will contain all the files and sub-directories related to your project. Since we are building a blog with Flask, let us name this directory flask_blog
.
mkdir flask_blog
Now it's time to set up a virtual environment in the project directory you just created. In case you are wondering what a virtual environment is and why this is necessary; a virtual environment is a specific Python setup, where the Python interpreter, libraries and scripts are isolated from those of other virtual environments and any libraries installed in the "system" Python, which comes either pre-installed or user-installed in the Operating System.
Create your virtual environment by navigating to your project directory flask_blog
:
python -m venv blogenv
In this instance, we named the virtual environment "blogenv". Feel free to use any name you like.
Now that your virtual environment has been created, let's activate it
source blogenv/bin/activate
Once your virtual environment is activated, install Flask using the pip install
command. Pip is the package installer for python that will be used to install all dependencies for this project.
pip install flask
Next, we install SQLAlchemy
SQLAlchemy is a powerful and versatile Python toolkit and Object Relational Mapper (ORM) that enables developers to take advantage of the full capabilities of SQL in their applications. In a nutshell, SQLAlchemy is a library that interacts with databases.
pip install flask-sqlalchemy
Setting up our files and directories
Let's create a file and a sub-directory in our flask_blog
directory.
touch app.py
mkdir website
Now that we have the file app.py
and website
directory in our main flask_blog
directory, let's go ahead and navigate into the website
directory and create some more files and another sub-directory.
Let's start with the __init__.py
file. This file is used to designate a directory as a Python package, making it possible to import its contents. The file must be present in every directory that contains code meant to be used as a package, including subdirectories of already established packages. The code in __init__.py
is executed first upon importing the package, allowing for special actions to be taken.
touch __init__.py
Let's create some more files that we will be making use of later.
touch auth.py
touch models.py
touch views.py
Finally, we create the sub-directory templates
. This directory is going to contain the front-end part of our application (i.e. HTML and CSS).
mkdir templates
Building the base application and creating the database (__init__.py
)
We will start by importing the required modules into the __init__.py
file which will be used for interacting with our database. The database will be stored in a file named database.db
which will be automatically created once the application is run.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from os import path
from flask_login import LoginManager
Next, we create a function create_app
. This will create an instance of our flask application.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from os import path
from flask_login import LoginManager
db = SQLAlchemy()
DB_NAME = "database.db"
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = "ragnarok"
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_NAME}'
db.init_app(app)
from .views import views
from .auth import auth
app.register_blueprint(views, url_prefix="/")
app.register_blueprint(auth, url_prefix="/")
from .models import User, Post
create_database(app)
login_manager = LoginManager()
login_manager.login_view = "auth.login"
login_manager.init_app(app)
@login_manager.user_loader
def load_user(id):
return User.query.get(int(id))
return app
def create_database(app):
if not path.exists("website/" + DB_NAME):
with app.app_context(): db.create_all()
print("Created database!")
A breakdown of the code in this function:
Creates an instance of the Flask class and sets the
__name__
as the name for the application.Adds a secret key and the database URI to the Flask configuration. The secret key is used to hash or encrypt session data while the database URI is used to specify the location of the database.
Initializes the database with the Flask app using
db.init_app(app)
.Registers the blueprints
views
andauth
which are the files we created earlier. These files will be responsible for handling the different parts of the application. The blueprints are registered with the prefix "/" and "/auth".Imports two classes
User
andPost
from themodels
module. We will understand this when we populate themodels
file.Calls the
create_database
function, passing the Flask app as an argument.Sets up a Login Manager which is a Flask-Login extension for handling user authentication. The
login_view
attribute is set toauth.login
to specify the endpoint that handles the login process. The Login Manager is then initialized with the Flask app.Defines the
load_user
function which is used byFlask-Login
to retrieve a user from the database. The function takes the user ID as an argument and returns a User object from the database.Returns the Flask app instance.
The create_database
function is used to create the database file if it doesn't exist. The function takes the Flask app as an argument and creates the database using db.create_all()
inside a context.
Once the script is executed, the create_app
function is called to create the Flask application.
The next file to work with is our app.py
file.
from website import create_app
if __name__ == "__main__":
app = create_app()
app.run(debug=True)
This Python script will create and run the Flask web application using the create_app
function from the website
module (remember the create_app
function in our __init__.py
file which is in the website
directory).
The if __name__ == "__main__":
statement is a common Python idiom that checks whether the script is being run as the main program (as opposed to being imported as a module) and if so, it creates an instance of the Flask application using the create_app
function and runs it in debug mode with app.run
(debug=True)
.
Defining our routes (views.py
)
Routing is the process of mapping a URL directly to the programming code that generates the web page. This technique improves the organization of the webpage and significantly enhances the website's performance, making it easy to implement changes or improvements in the future.
from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask_login import login_required, current_user
from .models import Post, User
from . import db
views = Blueprint("views", __name__)
@views.route("/")
@views.route("/home")
def home():
posts = Post.query.all()
return render_template("home.html", user=current_user, posts=posts)
@views.route("/new-post", methods=['GET', 'POST'])
@login_required
def new_post():
if request.method == "POST":
title = request.form.get('title')
text = request.form.get('text')
if not title:
flash('Both fields are compulsory', category='error')
elif not text:
flash('Both fields are compulsory', category='error')
else:
post = Post(text=text, title=title, author=current_user.id)
db.session.add(post)
db.session.commit()
flash('Post created!', category='success')
return redirect(url_for('views.home'))
return render_template('new_post.html', user=current_user)
@views.route("/edit-post/<id>", methods=['GET', 'POST'])
@login_required
def edit_post(id):
post = Post.query.filter_by(id=id).first()
if request.method == 'POST':
title = request.form['title']
text = request.form['text']
post.title = title
post.text = text
db.session.commit()
flash('Post updated.', category='success')
return redirect(url_for('views.home', id=id))
return render_template('edit_post.html', user=current_user, post=post)
@views.route("/delete-post/<id>")
@login_required
def delete_post(id):
post = Post.query.filter_by(id=id).first()
db.session.delete(post)
db.session.commit()
flash('Post deleted.', category='success')
return redirect(url_for('views.home'))
@views.route("/posts/<username>")
@login_required
def posts(username):
user = User.query.filter_by(username=username).first()
if not user:
flash('No user with that username exists.', category='error')
return redirect(url_for('views.home'))
posts = Post.query.filter_by(author=user.id).all()
return render_template("posts.html", user=current_user, posts=posts, username=username)
@views.route('/contact')
def contact():
return render_template('contact.html', user=current_user)
@views.route('/about')
def aboutt():
return render_template('about.html', user=current_user)
@views.route("/")
and@views.route("/home")
: This route maps to thehome()
function, which returns a rendered HTML template containing a list of all posts.@views.route("/new-post", methods=['GET', 'POST'])
: This route maps to thenew_post()
function, which allows a logged-in user to create a new post. If the HTTP request is a POST request, the form data is validated and added to the database.@views.route("/edit-post/<id>", methods=['GET', 'POST'])
: This route maps to theedit_post()
function, which allows a logged-in user to edit an existing post. If the HTTP request is a POST request, the updated data is validated and saved to the database.@views.route("/delete-post/<id>")
: This route maps to thedelete_post()
function, which allows a logged-in user to delete a post from the database.@views.route("/posts/<username>")
: This route maps to theposts()
function, which displays all posts created by a specific user.@views.route('/contact')
: This route maps to thecontact()
function, which displays a "contact" page with contact information.@views.route('/about')
: This route maps to theabout()
function, which displays an "about" page with information about the website.
Each route is decorated with the @views.route()
decorator, which specifies the URL endpoint that the route should be mapped to. Some of the routes are also decorated with the @login_required
decorator from the flask_login
module which ensures that the user is logged in before they can access certain pages. The functions use render_template()
to render HTML templates and request
to handle incoming HTTP requests. Finally, the db
object is used to interact with the database to add, update, or delete posts.
User authentication (auth.py
)
We are going to implement a basic user authentication system which will include user login, sign-up and logout functionality. Wwe will a Flask blueprint to organize the routes and follows best practices for security, such as storing hashed passwords instead of plain text..
Firstly, we import the necessary modules.
from flask import Blueprint, render_template, redirect, url_for, request, flash
from . import db
from .models import User
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
Code breakdown:
from flask import Blueprint, render_template, redirect, url_for, request, flash
: This line imports various modules from the Flask library, includingBlueprint
(which allows us to define routes and views in a modular way),render_template
(which allows us to render HTML templates),redirect
andurl_for
(which are used for URL redirection),request
(which allows us to access data submitted in a form or URL parameters) andflash
(which allows us to display flash messages to the user).from . import db
: This line imports thedb
module from the current package. The.
notation indicates that the module is in the same directory as the current file.from .models import User
: This line imports theUser
class from themodels
module in the current package.from flask_login import login_user, logout_user, login_required, current_user
: This line imports various functions and variables from theflask_login
module, which is used for user authentication and session management.login_user
is used to log a user in,logout_user
is used to log a user out,login_required
is used as a decorator to require authentication for certain routes andcurrent_user
is a variable that contains the currently logged in user.from
werkzeug.security
import generate_password_hash, check_password_hash
: This line imports functions from thewerkzeug.security
module, which is used for password hashing and verification.generate_password_hash
is used to hash passwords andcheck_password_hash
is used to verify hashed passwords.auth = Blueprint("auth", __name__)
: This line creates a newBlueprint
object named "auth". The first argument is the name of the blueprint and the second argument is the name of the module or package that the blueprint belongs to (in this case, it is the current package). TheBlueprint
object is used to define routes and views for the authentication blueprint.
Then we define our blueprint
auth = Blueprint("auth", __name__)
This line creates a new Blueprint
object named "auth". The first argument is the name of the blueprint and the second argument is the name of the module or package that the blueprint belongs to (in this case, it is the current package). The Blueprint
object is used to define routes and views for the authentication blueprint.
Next, we define our routes
@auth.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form.get("email")
password = request.form.get("password")
user = User.query.filter_by(email=email).first()
if user:
if check_password_hash(user.password, password):
flash("Logged in!", category="success")
login_user(user, remember=True)
return redirect(url_for("views.home"))
else:
flash("Password is incorrect.", category="error")
else:
flash("Email does not exist.", category="error")
return render_template("login.html", user=current_user)
@auth.route("/sign-up", methods=["GET", "POST"])
def sign_up():
if request.method == "POST":
first_name = request.form.get("first_name")
last_name = request.form.get("last_name")
email = request.form.get("email")
username = request.form.get("username")
password1 = request.form.get("password1")
password2 = request.form.get("password2")
email_exists = User.query.filter_by(email=email).first()
username_exists = User.query.filter_by(username=username).first()
if email_exists:
flash("Email is already in use.", category="error")
elif len(email) < 4:
flash("Email is invalid.", category="error")
elif username_exists:
flash("Username is already in use.", category="error")
elif password1 != password2:
flash("Passwords do not match!", category="error")
elif len(username) < 2:
flash("Username is too short.", category="error")
elif len(password1) < 5:
flash("Password is too short.", category="error")
else:
new_user = User(
first_name=first_name,
last_name=last_name,
email=email,
username=username,
password=generate_password_hash(password1, method="sha256"),
)
db.session.add(new_user)
db.session.commit()
login_user(new_user, remember=True)
flash("User created!")
return redirect(url_for("views.home"))
return render_template("sign_up.html", user=current_user)
@auth.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("views.home"))
The code defines three routes for the auth
blueprint:
auth.route("/login", methods=["GET", "POST"])
: This route handles user login. If the request method is POST, the code checks whether the user's email and password match what is stored in the database. If there is a match, the user is logged in and a success message is flashed. Otherwise, an error message is flashed. The function returns the renderedlogin.html
template.auth.route("/sign-up", methods=["GET", "POST"])
: This route handles user sign-up. If the request method is POST, the code checks if the email and username are available and valid. If they are, a new user is created in the database and the user is logged in. A success message is flashed and the function redirects to the home page. Otherwise, an error message is flashed. The function returns the renderedsign_up.html
template.auth.route("/logout")
: This route handles user logout. The user is logged out and the function redirects to the home page.
The @login_required
decorator is applied to the logout()
route, which ensures that the user must be authenticated to access the route.
Database Table Creation (models.py
)
As usual, we'll start with the imports
from . import db
from flask_login import UserMixin
from sqlalchemy.sql import func
Code breakdown:
from . import db
: This line is importing thedb
object from the current package.from flask_login import UserMixin
: This line imports theUserMixin
class from theflask_login
package.UserMixin
is a class that provides default implementations for some of the methods required by Flask-Login to manage user authentication and sessions.from sqlalchemy.sql import func
: This line imports thefunc
object from thesqlalchemy.sql
module.func
is a utility object provided by SQLAlchemy for creating SQL functions, expressions, and constructs.
Next, let's define the parameters of the database table
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(255), nullable=False)
last_name = db.Column(db.String(255), nullable=False)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(15), unique=True)
password = db.Column(db.String(30))
date_created = db.Column(db.DateTime(timezone=True), default=func.now())
posts = db.relationship('Post', backref='user', passive_deletes=True)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Text, nullable=False)
text = db.Column(db.Text, nullable=False)
date_created = db.Column(db.DateTime(timezone=True), default=func.now())
author = db.Column(db.Integer, db.ForeignKey(
'user.id', ondelete="CASCADE"), nullable=False)
These models define a database schema for the blogging application where each post is associated with a user. The User
model represents a user in the system, while the Post
model represents a blog post created by a user.
The User
model has the following attributes:
id
: an integer primary key columnfirst_name
: a string column with a maximum length of 255 characters that cannot be nulllast_name
: a string column with a maximum length of 255 characters that cannot be nullemail
: a string column with a maximum length of 255 characters that must be uniqueusername
: a string column with a maximum length of 15 characters that must be uniquepassword
: a string column with a maximum length of 30 charactersdate_created
: a datetime column with timezone information that defaults to the current timeposts
: a relationship to thePost
model, which will allow us to access all the posts created by a particular user.
The Post
model has the following attributes:
id
: an integer primary key columntitle
: a text column that cannot be nulltext
: a text column that cannot be nulldate_created
: a datetime column with timezone information that defaults to the current timeauthor
: a foreign key column that references theid
column of theUser
model, with theCASCADE
option set for theondelete
parameter, which means that if a user is deleted, all of their posts will be deleted as well.
Templates
As I mentioned earlier, the template directory will contain all the front-end code for our application. I will not be going in-depth into this because this is a back-end tutorial, however, you can check out the front-end code for this project on my github here.
The code for the entire project can be found here.