A simple flask blog

A simple flask blog

Introduction to flask

Flask is a lightweight web application framework written in Python, based on the Werkzeug WSGI toolkit and Jinja2 template engine. In this post, we will explore the development of a blog application that performs the CRUD operations: creating, reading, updating, and deleting posts. With Flask, you can quickly and easily create a powerful and dynamic web application that can be used to store and manage data.

How to set up a flask app

What you need to follow this guide

  • Python 3 installed on your computer

  • A basic understanding of how to use the command line (the terminal)

Let’s get started

Create a project directory

We’ll start off by creating the project directory and enter it.

mkdir my-first-flask-app

cd my-first-flask-app

Create a virtual environment

A virtual environment creates a separate container for your project. This lets you install Python libraries that are meant for the specific project only and nothing else, meaning that you can have different versions of for example Python installed in different environments without them being affected by each other.

To create your virtual environment you need to launch Python 3.x version and instruct it to run the built-in module venv. run the following:

# For windows users
py -3 -m venv myvirtualenvironment


# For Linux and Mac users
python3 -m venv myvirtualenvironment

In your project directory, you should now have a folder named myvirtualenvironment which contains the files needed for your virtual environment to work.

Time to activate your virtual environment. run the following:

# For windows users
. myvirtualenvironment/Scripts/activate


# For Linux and Mac users
. myvirtual environment/bin/activate

Install flask

To install Flask, run:

pip3 install Flask

When Flask is installed, create a python file in your project directory:

touch app.py

To run your app after writing your code, you need to let Flask know which file to use. Run the following:

# For windows users
set FLASK_APP=app.py


# For linux and mac users
export FLASK_APP=app.py

start flask

flask run

You should now see the below in your terminal:

$ flask run 
* Serving Flask app "app.py" (lazy loading)
* Environment: production
WARNING: This is a development server. 
Do not use it in a production deployment.
Use a production WSGI instead.
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 123–456–789
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

The project

Using both Bootstrap and CSS for the styling, this a common layout(base.html) for the blog app

<body>
 <nav class="navbar navbar-expand-lg bg-dark navbar-dark py-3">
 <div class="container">
 <! - The navbar brand →
 <a href="{{ url_for('home') }}"
 class="navbar-brand"
 ><img
 src="{{ url_for('static', filename='images/blloogg.png') }}"
 alt=""
 style="width: 5rem"
 /></a>
 <! - Bootstrap nav toggler →
 <button
 class="navbar-toggler"
 type="button"
 data-bs-toggle="collapse"
 data-bs-target="#navmenu"
 aria-controls="navmenu"
 aria-expanded="false"
 aria-label="Toggle navigation"
 >
 <span class="navbar-toggler-icon"></span>
 </button>
 <! - The nav links →
 <div class="collapse navbar-collapse" id="navmenu">
 {% if current_user.is_authenticated %}
 <! - If the user is logged in, this nav links appears →
 <ul class="navbar-nav ms-auto">
 <li class="nav-item">
 <a href="{{ url_for('home') }}" class="nav-link">Home</a>
 </li>
 <li class="nav-item">
 <a
 href="{{ url_for('profile', profile_id=current_user.id) }}"
 class="nav-link"
 >Profile</a
 >
 </li>
<li class="nav-item">
 <a href="{{ url_for('account') }}" class="nav-link">Post</a>
 </li>
 <li class="nav-item">
 <a href="{{ url_for('about') }}" class="nav-link">About</a>
 </li>
 <li class="nav-item">
 <a href="{{ url_for('contact') }}" class="nav-link">Contact</a>
 </li>
<li class="nav-item">
 <a href="{{ url_for('logout') }}" class="nav-link">Logout</a>
 </li>
 </ul>
{% else %}
 <! - If the user is not logged in, this nav links appears →
 <ul class="navbar-nav ms-auto">
 <li class="nav-item">
 <a href="{{ url_for('home') }}" class="nav-link">Home</a>
 </li>
 <li class="nav-item">
 <a href="{{ url_for('about') }}" class="nav-link">About</a>
 </li>
 <li class="nav-item">
 <a href="{{ url_for('contact') }}" class="nav-link">Contact</a>
 </li>
 <li class="nav-item">
 <a href="{{ url_for('login') }}" class="nav-link">Login</a>
 </li>
 <li class="nav-item">
 <a href="{{ url_for('register') }}" class="nav-link">Register</a>
 </li>
 </ul>
 {% endif %}
 </div>
 </div>
 </nav>
<! - The body section →
 <section class="container my-5 d-flex px-4 px-lg-5">
 <! - web content →
 <div class="w-100 this" id="thiss">
 {% block content %} 
 {% endblock content %}
 </div>
<! - Side menu →
 <div
 class="card bg-dark d-xl-block d-lg-block d-none"
 style="width: 25rem; height: fit-content"
 >
 <div class="m-auto d-flex mt-3">
 <img
 src="{{ url_for('static', filename='images/owl.png') }}"
 class="card-img-top w-50"
 style="margin: auto"
 alt="…"
 />
 </div>
 <div class="card-body">
 <h5 class="card-title text-muted">LATEST BLOGS</h5>
 </div>
 <! - If the user is logged in →
 {% if current_user.is_authenticated %}
 <ul class="list-group list-group-flush">
 <! - Check if the user has a recent post →
 {% if current_user.posts[-1] %}
 <li class="list-group-item">
 <! - If yes, the title of the post stays at the top in the side menu recent blog list →
 {{ current_user.posts[-1].title.title() }}
 </li>
 <! - If no, This should occupy the space →
 {% else %}
 <li class="list-group-item">Unavailable</li>
 {% endif %} 
 <! - Check if the user had a post before the recent post →
 {% if current_user.posts[-2] %}
 <li class="list-group-item">
 <! - If yes, the title of the post occupies the second side menu recent blog list →
 {{ current_user.posts[-2].title.title() }}
 </li>
 <! - If no, This should occupy the space →
 {% else %}
 <li class="list-group-item">Unavailable</li>
 {% endif %} 
 <! - Check if the user had a second post before the recent post →
 {% if current_user.posts[-3] %}
 <li class="list-group-item">
 <! - If yes, the title of the post occupies the third side menu recent blog list →
 {{ current_user.posts[-3].title.title() }}
 </li>
 <! - If no, This should occupy the space →
 {% else %}
 <li class="list-group-item">Unavailable</li>
 {% endif %}
 </ul>
 <! - If a user is not logged indisplay this in the recent blog list →
 {% else %}
 <ul class="list-group list-group-flush">
 <li class="list-group-item">Recent Blog 1</li>
 <li class="list-group-item">Recent Blog 2</li>
 <li class="list-group-item">Recent Blog 3</li>
 </ul>
 {% endif %}
 </div>
 </section>
</body>

This is a common layout of the blog app. While the other HTML files inherit from this base layout, the only block contents (between {% block content %} and {% endblock content %}) differ from page to page.

The home page of my blog application displays all the posts stored in the database. This is accomplished by querying the database for all the posts and passing the retrieved information to the HTML template for rendering. The following is the Python code used to query the database for all the posts:

from flask import Flask, render_template
app = Flask(__name__)
# View function for the home page
@app.route('/')
@app.route('/home')
def home():
    posts = Post.query.all() # This get all the post in the post table in the database
    random.shuffle(posts) # Shuffles the post randomly
    return render_template('home.html', posts=posts) # and displays it randomly in the homepage whenever a refresh is done in the homepage

What was accomplished here was the creation of a route for the home page by importing Flask from the flask module, instantiating it, and creating routes for the respective functions. After creating the function, the first step was to query the database to retrieve all available posts, which were then sent to the HTML file for rendering.

This is how the homepage template looks like

<! - Extends from the base.html file →
{% extends 'base.html' %}
<! - The block for the title of this file →
{% block title %} Home {% endblock title %}
{% block content %}
<! - if a user is active →
{% if current_user.is_authenticated %}
<! - The scrolling text displays the user's first name →
<marquee behavior="smooth" direction="" class="move"
 >Hi! {{current_user.first_name.title()}}</marquee
>
{% endif %}
<h1 class="text-muted text-center h4">LATEST TRENDING POSTS</h1>
<hr class="mb-5" 
style="
border: 1px solid rgb(147, 139, 139);
"
/>
<! - If the →
{% if posts %}
{% for post in posts %}
<div
 class="card p-4 mt-4"
 style="
 box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
 border-left: 5px solid lightblue;
 "
>
 <div class="d-flex align-items-center">
 <img
 style="width: 4rem"
 src="{{ url_for('static', filename='images/user.png') }}"
 alt=""
 />
 <div class="mx-3 d-flex flex-column">
 <a
 href="{{ url_for('profile', profile_id=post.author.id) }}"
 class="text-decoration-none text-secondary"
 >
 <span class="h5 text-info"
 >{{ post.author.first_name.upper() }} {{ post.author.last_name.upper()
 }}
 </span>
 </a>
 <small class="text-muted fw-normal"
 >{{ post.date_posted.strftime('%Y-%h-%d') }}</small
 >
 </div>
 </div>
 <div class="d-flex align-items-center justify-content-between">
 <a
 href="{{ url_for('display_post', blog_id=post.id) }}"
 class="h5 text-decoration-none d-flex align-items-center text-dark card-title mt-4"
 style="width: fit-content"
 >
 {{ post.title.title() }}<i
 class="fa fas fas fas fa-external-link-alt d-xl-block d-lg-block d-none p-2 text-secondary mt-2 h6"
 style="opacity: 0.8; font-size: small"
 ></i>
 </a>
 </div>
 <hr />
 <p class="card-text">
 {{ post.content[:50] }}……………… <br /><br /><small
 class="text-muted"
 style="font-size: 0.7rem"
 >(click on title to read more)</small
 >
 </p>
</div>
{% endfor %} {% else %}
<h1 class="alert alert-danger text-center" id="th" style="transform: rotate(-10deg); margin-top: 10rem;">
 Post Unavailable
</h1>
{% endif %}
 {% endblock content %}

After querying the database for posts, we send them to the template file for rendering. In the home.html, we iterate over the posts received from the function and display each post in the database. But how do we get the information into the database? Let’s take it one step at a time.

Creating Models

A model is a class that represents a table or collection in our database, with each attribute of the class corresponding to a field of the table or collection. This provides a powerful way to interact with and manipulate data stored in the database, allowing for efficient and effective data management.

Now, let’s understand how models are created for this blog app using Flask-SQLAlchemy. To begin, we can install Flask-SQLAlchemy using the pip utility for a straightforward setup. This will allow us to create models that will interact with our database and provide the necessary data for our blog application.

pip3 install Flask Flask-Sqlalchemy

After installing Flask and Flask-SQLAlchemy, let’s create the user model. This model will provide the foundation for our application, allowing us to store and manage user data. With this model, we can create a secure and efficient system for managing user accounts.

from flask_sqlalchemy import SQLAlchemy
import os

base_dir = os.path.dirname(os.path.realpath(__file__))
# Instantiate the Flask imported from flask
app = Flask(__name__)

# The configuration for the URI of the database, the myblog.db is the name of this project's database
app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(base_dir, 'myblog.db')
# The configuration for the track modification
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# The configuration to set up a secret key to strengthen the security of your database
app.config["SECRET_KEY"] = '026b0eb800ec2934fb5cf2e7'

# Instantiate the SQLAlchemy to inherit from flask instantiation 'app'
db = SQLAlchemy(app)
# Initialize the database
db.init_app(app)


# creating the User table in the database
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(50), nullable=False)
    last_name = db.Column(db.String(50), nullable=False)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(70), unique=True, nullable=False)
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True, cascade="all, delete")
    # Define a representation with two attribute 'username' and 'email'

    def __repr__(self):
      return f"User('{self.username}', '{self.email}')"

First, we write the class to define the Model

db.Column is for defining a column.

db.Integer, db.String is for defining the Column type stored in the database.

primary_key=True, which means it should be autoincremented.

unique=True, means duplicate data cannot be entered.

nullable=True, means the column cannot be null

db.relationship creates a relationship between the user and the post model

backref is used to access the user who created a particular post

lazy=True determines how the related objects get loaded when querying through relationships

cascade=”all, delete” simply means when a user gets deleted, all the posts created by that user get deleted too.

Now that the Post Model has been linked with the User Model, let’s take a look at how the Post Model appears.

# creating the Post table in the database
class Post(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    # Define a representation with two attribute 'title' and 'date_posted'
    def __repr__(self):
      return f"Post('{self.title}', '{self.date_posted}')"

db.ForeignKey — The database’s ForeignKey attribute refers to the primary key of the user who created a particular post, allowing for the tracking of the post’s originator. This allows for a more efficient and organized system of post management.

To create the database using the models we just constructed, follow these steps:

Open your terminal and enter the python shell by typing python and hit the enter key

python

After that import everything from the python file where you wrote your code, let’s name the file app.py

from app import *

then run the db.create_all() command inside the app context

with app.app_context():
   db.create_all()

Hit enter button twice and your database gets created

Register a user

Now that the database has been created, we need to register a user on the blog platform. To ensure authorization and authentication, we must install the Flask-Login package. This package includes the login_user, current_user, logout_user, login_required, UserMixin, and LoginManager functions, which are essential for the registration process.

pip3 install flask-login

login_user is utilized to log in a user quickly and securely.current_user is used to get the currently authenticated user

logout_user facilitates the logging out of a user from their account, ensuring that their personal information remains secure. This function provides a secure and efficient way to terminate a user’s session, allowing them to log out of their account with confidence.

login_required is a decorator used to specify routes that can only be accessed by authenticated users. This ensures that only authorized personnel can access sensitive information, making it a vital security measure for any website or application.

UserMixin provides convenient default implementations for the methods that flask-login expects user objects to possess. This makes it easier for developers to quickly and efficiently create user-friendly applications that are secure and reliable.

LoginManager contains the code that enables your application and flask-login to work together, such as how to load a user from an ID, where to redirect users when they need to log in, and other related tasks. It is an essential component for ensuring a secure and efficient authentication process.

The most important part of an application that uses Flask-Login is the LoginManager class. You should create one for your application somewhere in your code, like this

from flask_login import LoginManager

login_manager = LoginManager()

The login manager contains the code that lets your application and Flask-Login work together, such as how to load a user from an ID, where to send users when they need to log in, and the like.

You will need to provide a user_loader callback. This callback is used to reload the user object from the user ID stored in the session. It should take the str id of a user, and return the corresponding user object. For example:

# The decorator that loads the user using the user’s id
@login_manager.user_loader
def user_loader(id):
   return User.query.get(int(id))

Now that we know what UserMixin is, we should modify our models to inherit from it

# creating the User table in the database
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(50), nullable=False)
    last_name = db.Column(db.String(50), nullable=False)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(70), unique=True, nullable=False)
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship(‘Post’, backref=’author’, lazy=True)

    # Define a representation with two attribute 'username' and 'email'
    def __repr__(self):
      return f"User('{self.username}', '{self.email}')"

# creating the Post table in the database
class Post(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False) 
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False) 
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    # Define a representation with two attribute 'title' and 'date_posted'
    def __repr__(self):
      return f"Post('{self.title}', '{self.date_posted}')"

We require a form to collect user registration data, and we will utilize Flask-WTF for this purpose. Let’s install Flask-WTF and get started!

pip3 install flask-wtf

Create a form.py file and write the code to generate the form fields. Ensure that the code is optimized for a professional, user-friendly experience. Make sure to include all necessary elements, such as labels, input fields, and submit buttons. Additionally, consider adding validation to ensure that the form is filled out correctly.

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
from wtforms.validators import DataRequired, Length, Email, EqualTo


class RegistrationForm(FlaskForm):
    firstname = StringField('Username', validators=[DataRequired(), Length(min=2, max=30)])
    lastname = StringField('Username', validators=[DataRequired(), Length(min=2, max=30)])
    username = StringField('Username', validators=[DataRequired(), Length(min=2, max=25)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Sign Up')

StringField is essentially the same as a Text Field in an HTML form. It is a type of input field that allows users to enter text into a form

PasswordField is identical to the password field found in HTML forms. They provide a secure way for users to input sensitive information, such as passwords, without the risk of it being exposed to malicious actors.

TextAreaField is equivalent to a textarea field in an HTML form. It is a powerful tool for collecting user input, allowing users to enter large amounts of text in a single field

SubmitField is equivalent to the input type of “submit” in HTML forms. It is a convenient way to create a button that submits a form, allowing users to quickly and easily submit their information.

We must now send our registration form to the register.html file for rendering via our register route.

from form import RegistrationForm

# The view function for Register
@app.route('/register/')
def register():
   form = RegistrationForm()
   # This for a get request, if u click on the link that leads to the register page, this return statement get called upon
   return render_template('register.html', form=form)

After sending the form to our template file, let’s explore how to configure the form in the HTML file.

{% extends ‘base.html’ %}
{% block content %}

<form action="/register" method="post" class="w-100 p-5"
 style="
 box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
 border-top: 3px solid lightblue;
 ">
 <h1 class="text-muted">Register</h1>
 {{ form.hidden_tag() }}

<! - For First Name validations → 
{{ form.firstname(class="form-control p-3 mt-4", required=False, placeholder="Enter firstname") }}
<! - if there is error →
{% if form.firstname.errors %}
{% for error in form.firstname.errors %}
<! - display the error →
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %} 
{% endif %}

<! - For Last name validations →
{{ form.lastname(class="form-control p-3 mt-4", required=False, placeholder="Enter Last Name") }}
<! - if there is error →
{% if form.lastname.errors %} 
{% for error in form.lastname.errors %}
<! - display the error →
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For Username Validations → 
{{ form.username(class="form-control p-3 mt-4", required=False, placeholder="Enter Username") }}
<! - if there is error →
{% if form.username.errors %} 
{% for error in form.username.errors %}
<! - display the error →
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For Emails validations → 
{{ form.email(class="form-control p-3 mt-4", required=False, placeholder="Enter email") }} 
<! - if there is error →
{% if form.email.errors %}
{% for error in form.email.errors %}
<! - display the error →
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For Password validations →
{{ form.password(class="form-control p-3 mt-4 lead", required=False, placeholder='Enter password') }} 
<! - if there is error →
{% if form.password.errors %}
{% for error in form.password.errors %}
<! - display the error →
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For Confirm Password validations → 
{{ form.confirm_password(class="form-control p-3 mt-4 lead", required=False,
 placeholder='Confirm Password') }} 
<! - if there is error →
{% if form.confirm_password.errors %}
{% for error in form.confirm_password.errors %}
<! - display the error →
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For submit button →
 {{ form.submit(size=20, class='btn btn-success mt-4 w-100 p-2')}}
</form>
{% endblock content %}

We now have the form; let’s validate it after we fill in the fields with the necessary details. Upon clicking the submit button, a POST request will be sent to the registration route.

Now let’s modify the register route to accept post request


# The view function for Register
@app.route('/register/', methods=['GET', 'POST'])
def register():
    # If the logged-in user is trying to access the login url, redirects the user to the homepage
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    # Assign the RegistrationForm created in the form.py file to a variable 'form'
    form = RegistrationForm()
    # If the request is a post request and the form doesn't get validated, redirect the user to that same page
    if request.method == 'POST':
        if not form.validate_on_submit():
            return render_template('register.html', form=form)
        # If the form gets validated on submit
        else:
            # Check if the username already exist
            user = User.query.filter_by(username=form.username.data).first()
            # if the username exist
            if user:
                # redirect the user to that same page
                return redirect(url_for('register'))

            # Check if email exist
            existing_email = User.query.filter_by(email=form.email.data).first()
            # if the email exist
            if existing_email:
                # redirect the user to that same page
                return redirect(url_for('register'))
            # If both the username and email doesn't exist in the database, hash the password
            password_hash = generate_password_hash(form.password.data)
            # Create an instance of the User model, passing all the value of each column to the model and assign it to a
            # variable 'new_user'
            new_user = User(first_name=form.firstname.data, last_name=form.lastname.data, username=form.username.data,
                            email=form.email.data, password=password_hash)
            # Add the 'new_user'
            db.session.add(new_user)
            # Commit it to the database, the details gets sent to the database directly
            db.session.commit()
            # After committing, redirect the user to the login page so he/she can log in
            # into the newly created account
            return redirect(url_for('login'))
    # This for a get request, if u click on the link that leads to the register page, this return statement get called upon
    return render_template('register.html', form=form)

This route will validate the form and check if the username or email already exists in the database. If it does exist, it won’t proceed with the registration. If it does not exist, then add and commit the data to the database for successful registration.

Login a user

We can now register a user on the platform. Next, we create a login route that will handle the login.

Now let’s create a login form for the login page

class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember = BooleanField('Remember Me')
    submit = SubmitField('Login')

Create a login route and send the LoginForm to the template file

# The view function for the login
@app.route(‘/login/’)
def login():
   # Assign the LoginForm created in the form.py file to a variable ‘form’
   form = LoginForm()
   # This for a get request, if u click on the link that leads to the login page, this return statement get called upon
   return render_template(‘login.html’, form=form)

The template file looks like this

{% extends ‘base.html’ %}
{% block content %}

<form
 action=””
 method=”post”
 class=”w-100 p-5"
 style=
 box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
 border-top: 3px solid lightgreen;
 “
>
 <h1 class=”text-muted”>Login</h1>
 {{ form.hidden_tag() }}

<! - For Emails validations → 
{{ form.email(class="form-control p-3 mt-4", required=False, placeholder="Enter email") }}
{% if form.email.errors %} 
{% for error in form.email.errors %}
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For password validations →
{{ form.password(class="form-control p-3 mt-4 lead", required=False, placeholder='Enter password') }}
{% if form.password.errors %}
{% for error in form.password.errors %}
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - Rmember →
 <div class="d-flex align-items-center mt-4">
{{ form.remember(class="", required=False) }} 
{{ form.remember.label(class='mx-1 text-muted') }}
 </div>

<! - For submit button →
 {{ form.submit(size=20, class='btn btn-success mt-4 w-100 p-2')}}
</form>
{% endblock content %}

Sending the form for validation redirects it to the same login route for validation. The modified route looks like this

# The view function for the login
@app.route('/login/', methods=['GET', 'POST'])
def login():
    # If the logged-in user is trying to access the login url, redirects the user to the homepage
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    # Assign the LoginForm created in the form.py file to a variable 'form'
    form = LoginForm()
    # If the form gets validated on submit
    if form.validate_on_submit():
        # Query the User model and assign the queried data to the variable 'user'
        user = User.query.filter_by(email=form.email.data).first()
        # Check if the user exist in the database and if the inputted password is same with the one attached to the user on the database
        if user and check_password_hash(user.password, form.password.data):
            # If the check passed, login the user
            login_user(user, remember=form.remember.data)
            return redirect(url_for('home', id=user.id))
        else:
            # If the check failed
            flash('Check your Email / Password', 'danger')
            return render_template('login.html', form=form)
    # This for a get request, if u click on the link that leads to the login page, this return statement get called upon
    return render_template('login.html', form=form)

The page redirects the user to the homepage after validating the user and can have access to all the login_required routes

Create a post

Now that we have successfully logged into our account, let’s discuss how to create a post. To begin, let’s create a post form with title and content fields. This form will provide us with the necessary information to craft an engaging and informative post.

class PostForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(min=2, max=30)])
    post_content = TextAreaField('Post Content', validators=[DataRequired()])
    submit = SubmitField('Post')

Create a route for posting, import the PostForm from the form.py file, and render it to the template file.

# View function for account page, where you can create your post
@app.route(“/account/”)
# The login_required decorator indicates that this route can only be accessed when signed in
@login_required
def account():
   # Assign the PostForm created in the form.py file to a variable ‘form’
   form = PostForm()
   # This for a get request, if u click on the link that leads to the account page, this return statement get called upon
   return render_template(‘account.html’, form=form, user=current_user)

Create an account.html file

{% extends ‘base.html’ %}
{% block content %}

<div class=”w-100">
 <form action=”” method=”post”>
 <h1 class=”text-muted”>
 {{ user.first_name.upper() }} {{ user.last_name.upper() }}
 </h1>
 {{ form.hidden_tag() }}

<! - For title field → 
{{ form.title(class="form-control p-3 mt-4", required=False, placeholder="Enter title") }} 
{% if form.title.errors %}
{% for error in form.title.errors %}
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor % }
{% endif %}

<! - For content field →
{{ form.post_content(class="form-control p-3 mt-4 lead", required=False, placeholder='Enter post') }}
{% if form.post_content.errors %}
{% for error in form.post_content.errors %}
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For submit button →
{{ form.submit(size=20, class='btn btn-success mt-4 w-100 p-2')}}
 </form>
</div>
{% endblock content %}

Validation of the form on submit will send a post request to the account route, so let’s modify the route to accept the post request

# View function for account page, where you can create your post
@app.route("/account/", methods=['GET', 'POST'])
# The login_required decorator indicates that this route can only be accessed when signed in
@login_required
def account():
    # Assign the PostForm created in the form.py file to a variable 'form'
    form = PostForm()
    # If the form gets validated on submit
    if form.validate_on_submit():
        # Assign the data from the title field to a variable 'title'
        title = form.title.data
        # Assign the data from the post content field to a variable 'post_content'
        post_content = form.post_content.data
        # Create an instance of the Post model
        post = Post(title=title, content=post_content, author=current_user)
        # Add the datas to the Post table in the database
        db.session.add(post)
        # Commit
        db.session.commit()
        # redirect the user to that same page where the data gets displayed on the screen
        return redirect(url_for('account', id=id))
    # This for a get request, if u click on the link that leads to the account page, this return statement get called upon
    return render_template('account.html', form=form, user=current_user)

This validation will ensure that the data inputted is sent across to the database and that the author column records the user who created the post (i.e. the current_user). The HTML code to display the post details is as follows:

{% extends ‘base.html’ %} {% block head %}
<link
 rel=”stylesheet”
 href=”{{ url_for(‘static’, filename=’css/account.css’) }}”
/>
<script
 type=”text/javascript”
 src=”{{ url_for(‘static’, filename=’javascript/jquery-3.6.1.min.js’) }}”
></script>
<script
 src=”{{ url_for(‘static’, filename=’javascript/account.js’) }}”
 defer
></script>
{% endblock head %} 
{% block title %}{{ user.username.title() }}’s account{% endblock title %}
{% block content %}

<div class="w-100">
 <form action="" method="post">
 <h1 class="text-muted">
 {{ user.first_name.upper() }} {{ user.last_name.upper() }}
 </h1>
 {{ form.hidden_tag() }}

<! - For title field → 
{{ form.title(class="form-control p-3 mt-4", required=False, placeholder="Enter title") }} 
{% if form.title.errors %}
{% for error in form.title.errors %}
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor % }
{% endif %}

<! - For content field →
{{ form.post_content(class="form-control p-3 mt-4 lead", required=False, placeholder='Enter post') }}
{% if form.post_content.errors %}
{% for error in form.post_content.errors %}
 <small class="text-danger mt-1">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For submit button →
{{ form.submit(size=20, class='btn btn-success mt-4 w-100 p-2')}}

</form>
<hr class="mt-5" />
 {% if user %} {% for each in user.posts[::-1] %}
 <div
 class="card p-4 mt-4"
 style="
 box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
 border-left: 5px solid yellowgreen;
 "
 >
 <div class="d-flex align-items-center">
 <img
 style="width: 4rem"
 src="{{ url_for('static', filename='images/user.png') }}"
 alt=""
 />
 <div class="mx-3 d-flex flex-column">
 <span class="h5 text-info"
 >{{ user.first_name.upper() }} {{ user.last_name.upper() }}
 </span>
 <small class="text-muted fw-normal"
 >{{ each.date_posted.strftime('%Y-%m-%d') }}</small
 >
 </div>
 </div>
 <div class="d-flex align-items-center justify-content-between">
 <a
 href="{{ url_for('display_post', blog_id=each.id) }}"
 class="h5 text-decoration-none text-dark d-flex align-items-center card-title mt-4"
 style="width: fit-content"
 >
 {{ each.title.title() }}<i
 class="fa fas fas fas fa-external-link-alt d-xl-block d-lg-block d-none p-2 text-secondary mt-2 h6"
 style="opacity: 0.8; font-size: small"
 ></i>
 </a>
 <div class="d-flex align-items-center flex-row-reverse part">
 <i class="fa fas fa-ellipsis-h h3 toggler" style="cursor: pointer"></i>
 <div class="card p-3 mx-3 hide">
 <form
 action="{{ url_for('delete_post', post_id=each.id) }}"
 method="post"
 class="d-flex align-items-center text-danger justify-content-around"
 >
 <input
 type="submit"
 value="Delete"
 onclick="return confirm('Are you sure yo want to delete this post')"
 style="background: none; border: none"
 class="text-danger"
 /><i class="fa fas fa-trash-alt"></i>
 </form>
 <hr style="border: 1px solid white" />
 <a
 href="{{ url_for('update_post', post_id=each.id) }}"
 class="d-flex align-items-center text-danger justify-content-around text-decoration-none fw-normal text-light"
 >Update &nbsp; <i class="fa fa-pen-square text-light"></i
 ></a>
 </div>
 </div>
 </div>
 <hr />
 <p class="card-text">{{ each.content }}</p>
 </div>
 {% endfor %} {% endif %}
</div>
{% endblock content %}

After committing the data to the database, the route redirects the user to the same page and displays the post the user just created. If you take a closer look at the HTML code, you will notice the update and delete buttons, which are meant for updating and deleting the post respectively. Both buttons are inside the same div container, which can be displayed when the three dots icon at the right corner is clicked on but will be displayed with the help of a JavaScript code. To do this, create a static folder at the root folder, and inside the static folder, create a javascript folder and inside the javascript folder, create an account.js file.

$(document).ready(function(){
 $(‘.part’).on(“click”, function() {
 $(this).find(‘.hide’).toggleClass(‘active’);
 });
})

You will need to download the jQuery library and properly link it for the JavaScript to function correctly. To style the page, create a CSS folder inside the static folder and add an account.css file inside the CSS folder.

Inspect the {% block head %} in account.html to learn how to link the JavaScript and CSS files with Jinja2.

div.hide{
 transition: .6s;
 display: none;
}

div.hide.active{
 transition: .6s;
 display: block;
 position: absolute;
 top: 0;
 right: 30px;
 background: rgba(0, 0, 0, 0.818);
 font-weight: bold;
}

Update a post

As previously mentioned, every post has the option to be updated or deleted. In this section, we will discuss how to update a post. To begin, we need a form for this operation.

class UpdateForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(min=2, max=30)])
    post_content = TextAreaField('Post Content', validators=[DataRequired()])
    submit = SubmitField('Update Post')

Create a route to send the form to the template file for rendering through the route

# Import the UpdateForm class form form.py file
from form import UpdateForm


# View function for update post
@app.route("/post/<int:post_id>/update")
# The login_required decorator indicates that this route can only be accessed when signed in
@login_required
def update_post(post_id):
    # Assign the UpdateForm created in the form.py file to a variable 'form'
    form = UpdateForm()
    # Query a particular post from the Post table from the database
    post = Post.query.get_or_404(post_id)
    # For the get request, if the button that leads to the update page is being clicked, then redirect the user to the update page
    # then update the empty the value of both the title and content field with the queried data from database
    form.title.data = post.title
    form.post_content.data = post.content
    return render_template('update.html', form=form)

When you click the “Update” button of a particular post, it redirects to the “update_post” route. This route takes the post’s ID as a parameter, allowing you to make changes to the post.

<!this link is from the account.html →

<a href=”{{ url_for(‘update_post’, post_id=each.id) }}”
class=”d-flex align-items-center text-danger justify-content-around text-decoration-none fw-normal text-light”>Update &nbsp; <i class=”fa fa-pen-square text-light”></i></a>

If we examine the href attribute of the anchor tag, it redirects to the update_post route and also sends the value for the post_id parameter as the ID of each post. Upon redirection, the post is retrieved from the database using its ID, after which the title field is set to the post title and the content field is set to the post content.

{% extends ‘base.html’ %}
{% block content %}

<div class=”w-100">
 <form action=”” method=”post”>
 <h1 class=”text-muted”>Update Post</h1>
 {{ form.hidden_tag() }}

<! - For title field → 
{{ form.title(class="form-control p-3 mt-4", required=False, placeholder="Enter title") }}
{% if form.title.errors %} 
{% for error in form.title.errors %}
 <small class="text-danger">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For post field → 
{{ form.post_content(class="form-control p-3 mt-4 lead", required=False, placeholder='Enter post') }}
{% if form.post_content.errors %} 
{% for error in form.post_content.errors %}
 <small class="text-danger">{{ error }}</small>
{% endfor %}
{% endif %}

<! - For submit button →
{{ form.submit(size=20, class='btn btn-success mt-4 w-100 p-2')}}
 </form>
</div>
{% endblock content %}

Submitting the form for validation

# View function for update post
@app.route("/post/<int:post_id>/update", methods=['GET', 'POST'])
# The login_required decorator indicates that this route can only be accessed when signed in
@login_required
def update_post(post_id):
    # Assign the UpdateForm created in the form.py file to a variable 'form'
    form = UpdateForm()
    # Query a particular post from the Post table from the database
    post = Post.query.get_or_404(post_id)
    # If the user who created the post is not same with the current logged-in user, abort the process
    if post.author != current_user:
        abort(403)
    # If the request is a post method and the form gets validated on submit
    if request.method == 'POST':
        if form.validate_on_submit():
            # title of the post from database should be updated with the new(if True or False) title from the title field from the update form
            post.title = form.title.data
            # content of the post from database should be updated with the new(if True or False) title from the title field from the update form
            post.content = form.post_content.data
            # After the update, commit the changes to the database
            db.session.commit()
            # then redirect the user to the account page
            return redirect(url_for('account', post_id=post.id))
    # For the get request, if the button that leads to the update page is being clicked, then redirect the user to the update page
    # then update the empty the value of both the title and content field with the queried data from database
    form.title.data = post.title
    form.post_content.data = post.content
    return render_template('update.html', form=form)

Deleting a post

To delete a post, we don’t need a form; all we have to do is click the delete button and the post will be redirected to a delete route to process the deletion. Let’s extract the delete button from the account.html file for easy access.

<form action=”{{ url_for(‘delete_post’, post_id=each.id) }}” method=”post” class=”d-flex align-items-center text-danger justify-content-around”>
<input type=”submit” value=”Delete” onclick=”return confirm(‘Are you sure yo want to delete this post’)”
style=”background: none; border: noneclass=”text-danger”/><i class=”fa fas fa-trash-alt”></i>
</form>

Clicking the delete button will redirect to the delete_post route and send the post’s ID as the value for the parameter passed into the view function. Let’s take a look at what the delete route looks like.

# View function for delete_post, it only accepts a post request
@app.route("/post/<int:post_id>/delete", methods=['POST'])
@login_required
def delete_post(post_id):
    # Query the post you want to delete from the database using the post's id
    post = Post.query.get_or_404(post_id)
    # If the user who created the post is not the current user logged-in, abort the process with the status code
    if post.author != current_user:
        abort(403)
    # If the user who created the post is the current user logged-in, delete the post
    db.session.delete(post)
    db.session.commit()
    # After a successful delete, redirect the user back to the account page flash('Your post has been deleted!', 'success')
    return redirect(url_for('account'))

Logout user

Logging out a user is one of the simplest tasks; all you need to do is create a route for logging out and use the logout_user imported from flask-login to log out the user. It’s that easy!

# View function for the logout
@app.route('/logout/')
@login_required
def logout():
    # Logout_user() log out the user, it was imported from flask_login
    logout_user()
    # After a successful logout, redirect the user to the homepage
    return redirect(url_for('home'))

After a successful logout, you will be redirected to the home page, where you can peruse all the posts on the blog platform.

Below is the link to the project folder

Click here

The images used for this project are inside the image folder which is a sub folder inside the static folder