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 and auth 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 and Post from the models module. We will understand this when we populate the models 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 to auth.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 by Flask-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 the home() function, which returns a rendered HTML template containing a list of all posts.

  • @views.route("/new-post", methods=['GET', 'POST']): This route maps to the new_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 the edit_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 the delete_post() function, which allows a logged-in user to delete a post from the database.

  • @views.route("/posts/<username>"): This route maps to the posts() function, which displays all posts created by a specific user.

  • @views.route('/contact'): This route maps to the contact() function, which displays a "contact" page with contact information.

  • @views.route('/about'): This route maps to the about() 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, including Blueprint (which allows us to define routes and views in a modular way), render_template (which allows us to render HTML templates), redirect and url_for (which are used for URL redirection), request (which allows us to access data submitted in a form or URL parameters) and flash (which allows us to display flash messages to the user).

  • from . import db: This line imports the db 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 the User class from the models 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 the flask_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 and current_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 the werkzeug.security module, which is used for password hashing and verification. generate_password_hash is used to hash passwords and check_password_hash is used to verify hashed passwords.

  • 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.

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 rendered login.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 rendered sign_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 the db object from the current package.

  • from flask_login import UserMixin: This line imports the UserMixin class from the flask_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 the func object from the sqlalchemy.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 column

  • first_name: a string column with a maximum length of 255 characters that cannot be null

  • last_name: a string column with a maximum length of 255 characters that cannot be null

  • email: a string column with a maximum length of 255 characters that must be unique

  • username: a string column with a maximum length of 15 characters that must be unique

  • password: a string column with a maximum length of 30 characters

  • date_created: a datetime column with timezone information that defaults to the current time

  • posts: a relationship to the Post 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 column

  • title: a text column that cannot be null

  • text: a text column that cannot be null

  • date_created: a datetime column with timezone information that defaults to the current time

  • author: a foreign key column that references the id column of the User model, with the CASCADE option set for the ondelete 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.