How to build a simple Blog site using Flask in Python

How to build a simple Blog site using Flask in Python

A beginner-friendly tutorial on building a blog site with responsive web design, routing, DB CRUD operations, user authentication & internet security

Flask is a well-documented lightweight Python framework which eases the process of building web applications. It has a variety of useful extendable features.

In this tutorial, we will learn how to write backend and frontend code for a simple blog website, practicing the following concepts as we do:

  • Responsive Web Design using HTML, CSS & Jinja

  • Routing

  • Database Management with SQLite and SQLAlchemy

  • Internet Security with Werkzeug

  • User Authentication & Authorization

  • Message Flashing

Note: Everything here will be locally hosted. We will not be able to send a link to others for them to write and view articles on the blog. Yet, proper documentation will enable us to share this project with the world, as seen here on the Ze Blog GitHub repo.

Prerequisites

This tutorial requires some familiarity with Python, HTML and CSS conventions. Expertise isn't a requirement, as we will remain beginner-friendly here.

Before starting this project, it is important to have the following set up:

Bonus: DB Browser for SQLite helps check what we have in the database, but it is not essential.

Initial Preparation

With the following steps, we will create a dedicated folder on the computer for our Flask project, and learn a thing or two about virtual environments as we do.

Create a Project Folder

It is good practice to have all files and folders properly organized on the system. For this blog project, we will create a dedicated folder on the computer. We can do this with the computer's file manager, VS Code or the terminal. Here's the terminal script to create a new directory (folder) and then move into it, using Ze Blog as an example:

$ mkdir ze_blog
$ cd ze_blog

Set Up a Virtual Environment

" Virtual environments are independent groups of Python libraries, one for each project. Packages installed for one project will not affect other projects or the operating system’s packages." - Flask Docs

Virtual environments keep installations and requirements unique to each project. It is recommended to create a new virtual environment for each project, as this prevents some compatibility issues and keeps things nicely organized. Virtual environments are easy to set up, using the venv module that comes bundled with Python. Any name can be chosen for the virtual environment. We will use env in this tutorial, but some programmers prefer to give it a similar name to the project.

Warning: Do not touch the virtual environment folder! Do not cd into it or manipulate files within. Most command-line virtual environment operations use what is already autogenerated by Python in the folder, rather than creating or deleting items. In other words, treat this as a mini system folder like __pycache__.

For simplicity, we will use the syntax for macOS/Linux in the next two code blocks. Windows users, please check out the applicable syntax from Flask Docs instead.

We start by creating a virtual environment named env:

$ python3 -m venv env

The next step is to activate our virtual environment with this command:

$ source env/bin/activate

Once activated, the Command Line Interface (CLI) will change to show the virtual environment's name. We will ignore this in future code blocks on this tutorial because all we will need to type is what comes after the $ sign. Still, here is a sample of how an activated virtual environment looks on an Oh My ZSH-flavoured terminal, with Git version control activated as well:

(env) ➜  ze_blog git:(main)

Note: When done with or taking a break from a project, it is good practice to deactivate its virtual environment before moving out of the directory or doing other things in the terminal. This might yet be the simplest possible command in Bash:

$ deactivate

We are still building the blog for now, so there is no deactivation just yet.

Install Flask

Pip is the package installer for Python. It is a very useful module that comes bundled with Python, so we will come across it often when installing Python libraries.

Within the activated environment, we will install Flask using this pip command:

$ pip install Flask

Backend Basics

We now have a Flask project up and running! Well, the backbone of a Flask project, but every forward step counts.

Next up, we will learn how to create our Flask app, prepare some database tables, and design the first few routes which will be used in the front end.

Create a Flask App

We will first create a .flaskenv file within the home directory of the project. This hidden file enables us to launch the blog with a flask run command in the terminal, and will have the following lines of code in plain text:

FLASK_ENV = development
FLASK_DEBUG = 1
FLASK_APP = app.py

With these statements, we have told the computer that:

  • We want to run this Flask project in a development server

  • We are willing to debug the Flask app as it runs (1 is "True" in Boolean)

  • We want Flask to recognise app.py as the main Flask app file for this project

This brings us to app.py, eventually the heaviest file in this project. Note that it is better to split the code we will have in app.py into different Python files which carry out specific functions, but we are keeping things simple here.

Create app.py within the project's home directory, then write some code within:

from flask import Flask
app = Flask(__name__)

We have now created a Flask instance in our main module. That's a complex way of saying we have told the Python interpreter that this is our Flask app's main Python file.

Prepare a Database

The articles, user details and other data on our blog need to be stored somewhere. This is where a database comes in, as it keeps all the required data in easily accessible tables. Learn more about databases and SQLAlchemy in Flask from Abdelhadi's tutorial on Digital Ocean. Here is a rather relevant snippet:

"SQLAlchemy is an SQL toolkit that provides efficient and high-performing database access for relational databases. It provides ways to interact with several database engines such as SQLite, MySQL, and PostgreSQL. It gives you access to the database’s SQL functionalities. It also gives you an Object Relational Mapper (ORM), which allows you to make queries and handle data using simple Python objects and methods. Flask-SQLAlchemy is a Flask extension that makes using SQLAlchemy with Flask easier, providing you tools and methods to interact with your database in your Flask applications through SQLAlchemy."

Okay, that is a lot of theory. Here is how we will use these tools in practice:

  • Installation

We will first install Flask-SQLAlchemy with the following terminal code:

$ pip install Flask-SQLAlchemy
  • Imports

We will now return to app.py to import the inbuilt os module for configuration purposes, and the SQLAlchemy class for manipulating our database. Our Python file should look like this by now:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)
  • Configuration

In this step, we will construct a path for our SQLite database file. We will use some functions from the os module to store the path of the base directory in a variable named base_dir. We will then configure SQLAlchemy's URI and secret key, and turn off modification tracking to use less memory.

base_dir = os.path.dirname(os.path.realpath(__file__))

app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + \
    os.path.join(base_dir, 'ze_blog.db')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = 'createacomplexsecretkeyhere'

This step can be a bit confusing and abstract, but that is fine for now. We do not need to memorize the syntax. Just understand what it is doing, and then it can be modified for each future Flask project.

Note: The secret key is an important security feature. It is standard practice to create a complex hexadecimal string for each project and hide it from the public.

  • Instantiation

We will create a database object using the SQLAlchemy class, passing the app instance to connect our Flask app with SQLAlchemy. We will store this database object in a variable called db. We will then use this db object to interact with our database.

db = SQLAlchemy(app)
db.init_app(app)

We now have our database configured. Time to prepare the tables within.

  • Table Creation

We will store our project data with three database models: User, Article and Message.

  1. User Model

    Our User model creates a table of users using UserMixin, links each user to their articles in the table, and then returns the username as its representation.

    To have access to UserMixin, we will first install Flask-Login in the terminal:

     $ pip install Flask-Login
    

    Next, we will import the UserMixin subclass from the Flask-Login module:

     from flask_login import UserMixin
    

    We can now build the User class using the following code:

     class User(db.Model, UserMixin):
         __tablename__ = "users"
         id = db.Column(db.Integer, primary_key=True)
         username = db.Column(db.String(50), nullable=False, unique=True)
         first_name = db.Column(db.String(50), nullable=False)
         last_name = db.Column(db.String(50), nullable=False)
         email = db.Column(db.String(80), nullable=False, unique=True)
         password_hash = db.Column(db.Text, nullable=False)
    
         def __repr__(self):
             return f"User: <{self.username}>"
    

    While some of this code is self-explanatory, please see this excellent article by Abdelhadi for more explanation of how the syntax works. There is even more detail in the SQLAlchemy Docs, which is beyond the scope of this tutorial.

  2. Article Model

    The Article model creates a table of articles, links each article to its author, and then returns the article's title as its representation.

    To be able to show the day each entry was posted, we will need to import from the datetime module that comes with Python:

     from datetime import datetime
    

    The rest of the syntax is similar to that of the previous table, as seen below:

     class Article(db.Model):
         __tablename__ = "articles"
         id = db.Column(db.Integer, primary_key=True)
         title = db.Column(db.String(80), nullable=False)
         content = db.Column(db.String, nullable=False)
         created_on = db.Column(db.DateTime, default=datetime.now())
         user_id = db.Column(db.Integer, db.ForeignKey(
             "users.id"), unique=False, nullable=False)
         author = db.Column(db.String, nullable=False)
    
         def __repr__(self):
             return f"Article: <{self.title}>"
    
  3. Message Model

    The Message model creates a table of messages from site visitors and returns the message title as its representation.

     class Message(db.Model):
         __tablename__ = "messages"
         id = db.Column(db.Integer, primary_key=True)
         sender = db.Column(db.String(50), nullable=False)
         email = db.Column(db.String(80), nullable=False)
         title = db.Column(db.String(80), nullable=False)
         message = db.Column(db.String, nullable=False)
         priority = db.Column(db.String(20))
    
         def __repr__(self):
             return f"Message: <{self.title}>"
    

Note: The import and configuration code will stay as it is until later in the project, so we will only see relevant code snippets in the next few steps. Here is how the top of our app.py file should look by now, with some leading comments splashed in:

# Imports: load the source packages with `pip install -r requirements.txt`
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
import os

# Configurations: app, base directory and db
app = Flask(__name__)

base_dir = os.path.dirname(os.path.realpath(__file__))

app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + \
    os.path.join(base_dir, 'ze_blog.db')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = 'createacomplexsecretkeyhere'

db = SQLAlchemy(app)
db.init_app(app)
  • Initialization

The following syntax ensures that a database file (by the name we have assigned during configuration) is created the first time a route is requested in this Flask app, only that first time, and only if the ze_blog.db file does not already exist:

@app.before_first_request
def create_tables():
    db.create_all()

When we use flask run in the terminal, a ze_blog.db file will appear in the project's root directory if none previously existed. We can then use DB Browser for SQLite to see what is happening within the database, as VS Code does not read .db files.

Design Basic Routes

Routing means mapping a URL to a specific Flask function that will handle the logic for that URL. Routing in Flask is done with the route() decorator, and the URL is passed as an argument into the decorator. We will use @app.route() in this tutorial.

We are keeping things simple for now, so we will start with the routes which bring the blog to life and make it user-friendly. Before this, we will need to add a few imports from Flask, leaving us with this statement for the Flask imports line:

from flask import Flask, flash, render_template, url_for, request, redirect

These imports will become clearer as we go through their use cases.

  • Homepage

The index route is always represented with just '/' in routing. Since this is the homepage of a blog, we will make an index() function to display all articles.

@app.route('/')
def index():
    articles = Article.query.all()
    context = {
        "articles": articles
    }
    return render_template('index.html', **context)

Here, we have gotten all of the blog's articles by querying the Article model in our database. The **context keyword argument then tells Flask the variables to use when rendering the index.html template. See more details about rendering templates explained by Flask Docs.

  • About

This simple function renders the about.html template as a webpage which tells the visitor about the blog site and its creator.

@app.route('/about')
def about():
    return render_template('about.html')
  • Contact

Our contact() function collects feedback from users via a form in the contact.html template, stores it on the "messages" table in the database using the Message model, and then redirects to the homepage after flashing a success message.

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        sender = request.form.get('name')
        email = request.form.get('email')
        title = request.form.get('title')
        message = request.form.get('message')
        priority = request.form.get('priority')

        new_message = Message(sender=sender, email=email,
                              title=title, message=message,
                              priority=priority)
        db.session.add(new_message)
        db.session.commit()

        flash("Message sent. Thanks for reaching out!")
        return redirect(url_for('index'))

    return render_template('contact.html')

Note: The 'GET' method gets data from the backend to display to users, while the 'POST' method allows a user to post data to the backend. 'GET' is the default routing method, so it does not need to be passed as an argument when no other method is used. Read more about routing and its various methods on this nice article by Todd Birchard.

Note: The only way to view these messages on our blog project is with DB Browser, as sending emails with Flask is beyond the scope of this tutorial.

We have now set up the basic backend code needed to get the blog up and running. We will soon write blocks of code which will manipulate the User and Article tables, but that would be after designing the user interface.

Frontend with Flask

With these two steps, we will learn the standards for creating a templates folder in Flask, practice some HTML and CSS, and learn how to manipulate HTML templates with Jinja.

Create HTML Templates, spiced with Jinja

"Jinja is a fast, expressive, extensible templating engine. Special placeholders in the template allow writing code similar to Python syntax. Then the template is passed data to render the final document." - Jinja Docs

Jinja lets us make our HTML files dynamic with syntax that resembles Python. Jinja code is written where needed within the HTML code, so it is not stored as a separate script.

We start this step by creating a templates folder within the project's main folder or root directory. This folder is a standard in Flask development, as it is from here that all HTML files used in the project will be accessed by the Flask app. For example, here is the templates folder in Ze Blog.

We will create these HTML files for the blog: base.html, index.html, about.html, contact.html, signup.html, login.html, contribute.html, edit.html and article.html.

For now, we will only fill in the first four files listed above, as we have created their relevant routes earlier in this tutorial. The remaining files are more technical and will be written along with the upcoming backend code. Let us start with base.html, then.

"The most powerful part of Jinja is template inheritance. Template inheritance allows you to build a base “skeleton” template that contains all the common elements of your site and defines blocks that child templates can override." - Jinja Docs

The base template makes use of template inheritance, a very useful shortcut provided by Jinja. In the base.html file, we can put every block of code that we would normally have to repeat in other HTML files. We will then inherit the base template into those child templates (the other HTML files) and only write the code that pertains to the functions of a particular HTML file within itself. Thus, we will adhere to the DRY principle and have a much cleaner program.

This file does the heavy lifting for the site's design, so it has some heft. Here is what was used in the sample blog: Ze Blog's base template. This tutorial assumes prior familiarity with the basics of HTML and CSS, so we will focus on Jinja here, with some code snippets taken from that sample file to illustrate certain concepts.

  • Flask's url_for() Function

"Flask url_for is defined as a function that enables developers to build and generate URLs on a Flask application. As a best practice, it is the url_for function that is required to be used, as hard coding the URL in templates and view function of the Flask application tends to utilize more time during modification." - EDUCBA

<a href="{{url_for('index')}}">
  <img
    src="{{url_for('static',filename= 'Ze_Blog_Logo/site.png')}}"
    alt="Logo"
    width="120"
    height="40"
  />
</a>

In this image-adding code block, we have used url_for and Jinja syntax twice:

  1. To create a link to the index.html template

  2. To create a link that instructs the interpreter to search the static folder for a file with a particular name

Note: The use of double curly brackets {{ sample code }} is the default syntax for expressions in Jinja, as well as the use of quotation marks around the file/folder name with no .html in the url_for() function's argument.

  • User Authentication and IF Statements
<div class="nav-item">
  {% if current_user.is_authenticated %}
  <a href="{{url_for('index')}}">Home</a>
  <a href="{{url_for('about')}}">About</a>
  <a href="{{url_for('contribute')}}">Contribute</a>
  <a href="{{url_for('contact')}}">Contact</a>
  <a href="{{url_for('logout')}}">Log Out</a>
  {% else %}
  <a href="{{url_for('index')}}">Home</a>
  <a href="{{url_for('about')}}">About</a>
  <a href="{{url_for('contact')}}">Contact</a>
  <a href="{{url_for('register')}}">Sign Up</a>
  <a href="{{url_for('login')}}">Log In</a>
  {% endif %}
</div>

With current_user.is_authenticated and an IF-ELSE statement, we can ensure that only logged-in users will see a version of the navigation bar that lets them post an article or log out. Unauthorized users will see a navbar which lets them sign up or log in, but no option of contributing an article to the blog.

Note: Every statement block in Jinja must have an "end..." to close it (eg if and endif), and {% sample code %} is the default syntax for statements in Jinja.

  • Message Flashing and WITH Statements

Message flashing is provided by Flask as a simple way to give feedback to our users. We will explore the content of the messages when building the relevant routes, but our base template will have the following code block to allow a certain message to be printed when required:

{% with messages = get_flashed_messages() %} {% if messages %}
<div class="container">
  {% for message in messages %}
  <p>{{ message }} <a href="{{request.path}}">Dismiss</a></p>
  {% endfor %}
</div>
{% endif %} {% endwith %}

Please read more about message flashing syntax on Flask Docs, with a lot more detail available there than what the scope of this tutorial allows.

Note: {{request.path}} provides a URL which redirects to the current webpage. Thus, clicking the "Dismiss" link beside the message reloads the current page. This makes the flashed message disappear because flashed messages do not show up again after another request has been made. There are probably better ways to do this message dismissal - such as this jQuery answer on Stack Overflow - but {{request.path}} works easily enough.

  • Block Content

The base.html template is just a skeleton upon which every endpoint is fleshed out. With this in mind, it is easy to understand why its <main> element is not doing much, as this is where the specific job of each child template will come in. What little code we do have in the <main> element is quite important, as the {% block content %} statement allows each child template to inherit what is in base.html and avoid repetition.

<main class="container">{% block content %}{% endblock content %}</main>

That's it. Yes, really. We open a content block and then close it immediately. We will then use it for inheritance in the next step.

Note: Any code we put before and after the {% block content %}{% endblock content %} statement will apply to all child templates.

  • Inheritance

The index.html, about.html and contact.html files use basic HTML concepts and Jinja syntax standards that have been discussed earlier in this tutorial. Click each hyperlink to see the equivalent sample from Ze Blog.

This is the Jinja syntax for inheriting from the base template as extensions into any of the child templates:

{% extends 'base.html' %} {% block content %}
<p>Sample HTML code for the specific webpage</p>
{% endblock content %}

Beautify with CSS

Just like the templates folder in the previous step, the static folder is a standard for frontend development with Flask. A Flask project's static folder contains all assets used by the project's templates, including CSS files, JavaScript files, and images. Within the static folder, we will create a css folder, which will then have our main.css file. For this beginner-friendly project, all styling will be done within main.css.

The choice of blog design within main.css is entirely up to the programmer. There are many ways of doing this - including the popular Bootstrap styling toolkit with many handy tutorials online - but here is the simple responsive Vanilla CSS used on Ze Blog.

Bonus: The Ze Blog logo was made for free with NameCheap's Logo Maker. As a reminder, this logo and any other image used in the project should be stored in the static folder.

Further Preparation

With the basic frontend and backend code now covered, we will focus on the more specific functions that this blog site will be able to perform.

Note: As with previous sections of this tutorial, most of the syntax explanation for the next two major segments has been outsourced to more detailed online tutorials and official documentation, with sample code being available on Ze Blog.

Imports

We already have all the necessary installations, so the next step is to make a few more imports which will become clearer with their use cases. It is faster to just type them at once, so this is how the top of our Python file should now look:

# Imports: load the source packages with `pip install -r requirements.txt`
from flask import Flask, flash, render_template, url_for, request, redirect
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import current_user, login_user, logout_user, login_required, LoginManager, UserMixin
from datetime import datetime
import os

The werkzeug.security module comes with Flask and provides password hashing for improved internet security, in case of a data breach. This means that we will never store the users' raw passwords, but rather a complex hashed form via generate_password_hash() which will only be decoded by check_password_hash() when a site visitor tries to log in.

Instantiation

To use the Flask-Login module, we pass our app as an instance of LoginManager():

login_manager = LoginManager(app)

User Management

A major function of this blog will be handling users and their data. This involves user registration and authentication. We will explore the Python code used to achieve said objectives in this section of the tutorial. To see the HTML templates used in our sample project, check out signup.html and login.html on Ze Blog.

Register a User

Our register() function collects user data from the registration form in the signup.html template. If the username is already taken, we will flash an error and reload the registration page. If the given username is available, we will add this new user's data to the "users" table in the database with a hashed password, using the User model. We will then redirect them to the login page.

@app.route('/signup', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        first_name = request.form.get('first_name')
        last_name = request.form.get('last_name')
        email = request.form.get('email')
        password = request.form.get('password')

        username_exists = User.query.filter_by(
            username=username).first()
        if username_exists:
            flash("This username already exists.")
            return redirect(url_for('register'))

        email_exists = User.query.filter_by(
            email=email).first()
        if email_exists:
            flash("This email is already registered.")
            return redirect(url_for('register'))

        password_hash = generate_password_hash(password)

        new_user = User(username=username, first_name=first_name,
                        last_name=last_name, email=email,
                        password_hash=password_hash)
        db.session.add(new_user)
        db.session.commit()

        flash("You are now signed up.")
        return redirect(url_for('login'))

    return render_template('signup.html')

Log a User In

The login() function checks if the user's input on the login.html form matches any username and hashed password pair on the "users" table in the database. It then redirects to the homepage if the user is successfully authenticated, but flashes an error and reloads the login page if the data is invalid.

@app.route('/login', methods=['GET', 'POST'])
def login():
    username = request.form.get('username')
    password = request.form.get('password')

    user = User.query.filter_by(username=username).first()

    if user and check_password_hash(user.password_hash, password):
        login_user(user)
        flash("You are now logged in.")
        return redirect(url_for('index'))
    if (
        user and check_password_hash(user.password_hash, password)
        ) == False:
        flash("Please provide valid credentials.")
        return redirect(url_for('login'))

    return render_template('login.html')

We have now achieved user authentication, only allowing those who provide the correct name and password to be logged into the blog.

Log Out the Current User

The Flask-Login module provides an easy logout_user() function, which helps us revoke a user's access to protected routes once they click the logout link:

@app.route('/logout')
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for('index'))

No HTML template is needed here, as the route simply logs the user out and redirects them to the homepage.

Note: For the rest of this tutorial, we will assume that a user is currently logged in.

Article Management

Another major feature of our blog will be handling articles on the site. This involves user authorization, as some features and routes should only be available to the author of the article. To see the relevant HTML templates used in our sample project, check out article.html, contribute.html and edit.html on Ze Blog.

Display a Single Article

Let us start with an easy one. This article(id) function takes the article's ID as an argument, gets the requested article from the database, and then displays said article via the article.html template.

@app.route('/article/<int:id>/')
def article(id):
    article = Article.query.get_or_404(id)

    context = {
        "article": article
    }

    return render_template('article.html', **context)

Just like the homepage, we do not need user authentication or authorization for this route, because every visitor to this blog will be allowed to read every article on it.

Post a New Article

A user has to be logged in to be able to post an article, as some user data is required to identify the author and later authorize them to edit or delete their articles. Only user authentication (not authorization) is needed for this step, as the protected data (article) doesn't exist until it has been created.

This is where we will use an easy authentication tool provided by Flask-Login. The @login_required decorator does all the heavy lifting for us, as it ensures that a protected page will only be rendered if the current site visitor is logged in as a user. If the visitor is not logged in, it will display an error message.

Our contribute() function will enable authenticated users to post new entries to the "articles" table in our database using the Article model, after getting relevant data from the form in contribute.html. We will then thank the user and redirect them to the homepage to see all articles. If the new article's title is taken, we will flash an error and reload the page instead.

@app.route('/contribute', methods=['GET', 'POST'])
@login_required
def contribute():
    if request.method == 'POST':
        title = request.form.get('title')
        content = request.form.get('content')
        user_id = current_user.id
        author = current_user.username

        title_exists = Article.query.filter_by(title=title).first()
        if title_exists:
            flash("This article already exists. Please choose a new title.")
            return redirect(url_for('contribute'))

        new_article = Article(title=title, content=content,
                              user_id=user_id, author=author)
        db.session.add(new_article)
        db.session.commit()

        flash("Thanks for sharing your thoughts.")
        return redirect(url_for('index'))

    return render_template('contribute.html')

User Authorization

We should only allow authorized users to manipulate the articles in our database. While creating routes for editing and deleting articles, we will practice the concept of user authorization by writing IF statements which check that the currently logged in user is a particular article's author. We will see these IF statements at work in our last two steps.

Edit an Article

The edit(id) function enables article updates by the author. It takes the article's ID as an argument, then gets the article's author from the database to compare with the logged in user's username. If these are the same, it renders the edit.html template for the user to edit their article, and then posts the user's changes to the database. The function then loads the article's page if it has been successfully edited, but flashes an error and redirects home if the user is unauthorized to make changes.

@app.route('/edit/<int:id>/', methods=['GET', 'POST'])
@login_required
def edit(id):
    article_to_edit = Article.query.get_or_404(id)

    if current_user.username == article_to_edit.author:
        if request.method == 'POST':
            article_to_edit.title = request.form.get('title')
            article_to_edit.content = request.form.get('content')

            db.session.commit()

            flash("Your changes have been saved.")
            return redirect(url_for('article', id=article_to_edit.id))

        context = {
            'article': article_to_edit
        }

        return render_template('edit.html', **context)

    flash("You cannot edit another user's article.")
    return redirect(url_for('index'))

Delete an Article

Deletion does not need an HTML file, as it is a route which does one action and then redirects home. Our delete(id) function enables article deletion from the database by that article's author only. Just as above, the function first queries the database for the entry whose ID matches the article ID it has received as an argument, then it checks if the current user is the author of the chosen article before executing the db.session.delete() function. We will then redirect to the homepage when the article is successfully deleted, or flash an error and redirect home if the user is unauthorized.

@app.route('/delete/<int:id>/', methods=['GET'])
@login_required
def delete(id):
    article_to_delete = Article.query.get_or_404(id)

    if current_user.username == article_to_delete.author:
        db.session.delete(article_to_delete)
        db.session.commit()
        flash("That article is gone!")
        return redirect(url_for('index'))

    flash("You cannot delete another user's article.")
    return redirect(url_for('index'))

We have completed this project by designing our blog site and adding all desired features with clean code. Congrats! But there is one more thing...

Finishing Touches

It is recommended practice to use a requirements.txt file to store all the packages we have installed in this application:

pip freeze > requirements.txt

This file can then be accessed by anybody who wants to use our webapp, as they can install all our packages directly with one command:

pip install -r requirements.txt

And we are done! Our blog is ready for its many users and articles. Time to test it.

Taste the Magic

Let us see what we have created. This is done via flask run in the terminal, from where we will open the generated link on a browser. Here is an example of how Ze Blog looks after a Sample User logs in:

To terminate the Flask process and return to the normal CLI, click Ctrl C on any OS. Remember to deactivate this project's virtual environment when done with it.


Conclusion

Thanks for exploring the beautiful simplicity of Flask with me! 💜

We have learnt how to set up a Flask environment in VS Code, manipulate HTML templates with Jinja, and perform CRUD operations on a database with SQLite and SQLAlchemy. We have also practiced responsive web design with HTML and CSS, routing, password hashing with Werkzeug, message flashing, internet security, user authentication and user authorization.

Please share this tutorial with fellow WebDev enthusiasts, especially those interested in Backend Software Engineering with the Python Flask framework.

Check out Ze Blog, a project I built using these principles for the second-semester exam at AltSchool Africa.

There's a lot more to follow, here in Austin's Space. Follow me on Hashnode, GitHub and Twitter for more insights from my tech journey.

✨ Cheers to making more magic! ✨

Credits