DEV Community

Cover image for How To Build A Blog App With Flask
faozziyyah
faozziyyah

Posted on

How To Build A Blog App With Flask

In this article, I'll take you through the process of building a blog app with flask and SQLAlchemy for database.
The features of this project include:

  • responsive website using bootstrap
  • add, read, edit and delete a post
  • adding comments and images to a post
  • authentication and authorization
  • message flashing
  • display error pages
  • database management with mysql and SQLAlchemy
  • using the rich text editor

Flask is a python micro-framework for building web applications and it is easy to learn. You can learn more about it by visiting this website

1. Prerequisites

The following are the basic requirements before starting this project:

2. Setting Up

A. Create a Project folder: navigate into the section where you want to have your project and type these commands to create a new directory or folder

// create a new directory(folder)
$ mkdir blog-app

// navigate into the folder
$ cd blog-app
Enter fullscreen mode Exit fullscreen mode

B. Create a virtual environment: A virtual environment is a tool that helps to keep dependencies required by different projects separate by creating isolated python virtual environments for them. This is one of the most important tools that most Python developers use.

// navigate into the folder
$ cd blog-app

// create a virtual environment
$ python3 -m venv venv

// activate the virtual environment
$ . venv/Scripts/activate
Enter fullscreen mode Exit fullscreen mode

once activated, the command line shows the name of your virtual environment (venv).

C. Install Flask and SQlAlchemy

$ pip install flask
$ pip install Flask-SQLAlchemy
$ pip install pymysql
$ pip install Flask-Login
$ pip install Flask-Migrate
Enter fullscreen mode Exit fullscreen mode

D. Create a Flask App: you can use this command in your terminal $ touch app.py to create the file for our flask app or create it manually in VS Code then write this code inside it

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
Enter fullscreen mode Exit fullscreen mode

E. Configure the Database: open your psql terminal (SQL shell) and create a new database. For this project, I use our_users as the database name. Thereafter, write this in your app.py file.

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:password of your MYSQL database@localhost/our_users'

app.config['SECRET_KEY'] = "secret"
db = SQLAlchemy(app)

// this should always be at the bottom of the page
if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

3. Database Models

our project data has three database models: Post, Comment and Users.
using the UserMixin, the Users model create a table of users, linking them to their posts while datetime is important to show the date each post was created.

from flask_login import UserMixin
from datetime import datetime
Enter fullscreen mode Exit fullscreen mode

then, we can build the Users, Post and Comment classes using this code:

#create users model
class Users(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True) 
    username = db.Column(db.String(20), nullable=False, unique=True)
    name = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), nullable=False, unique=True)
    favorite_color = db.Column(db.String(120))
    about_author = db.Column(db.String(500), nullable=True)
    date_added = db.Column(db.DateTime, default=datetime.utcnow)
    profile_pic = db.Column(db.String(200), nullable=True)
    password_hash = db.Column(db.String(128))
    posts = db.relationship('Post', backref='poster', lazy='dynamic')

def __repr__(self):
        return '<Name %r>' % self.name
Enter fullscreen mode Exit fullscreen mode
#create a blog post model
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255))
    content = db.Column(db.Text)
    author = db.Column(db.String(255))
    date_posted = db.Column(db.DateTime, default=datetime.utcnow)
    slug = db.Column(db.String(255))
    poster_id = db.Column(db.Integer, db.ForeignKey("users.id"))
    comments = db.relationship('Comment', backref='post', lazy='dynamic')
    post_pic = db.Column(db.String(200), nullable=True)

class Comment(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255))
    text = db.Column(db.Text())
    date = db.Column(db.DateTime())
    post_id = db.Column(db.Integer(), db.ForeignKey('post.id'))
Enter fullscreen mode Exit fullscreen mode

You can check out this article for better understanding on how the syntax works.

3. Database Migrations

Migrations are important to let MYSQL know that we've made changes to our database. First, we write this code in our app.py file.

from flask_migrate import Migrate

# write this after db = SQLAlchemy(app)
migrate = Migrate(app, db)
Enter fullscreen mode Exit fullscreen mode

then we you the following in your terminal

# create migrations folder
$ flask db init

# make changes
$ flask db migrate -m 'comment'
$ flask db upgrade
Enter fullscreen mode Exit fullscreen mode

c
Routing means mapping a URL to a specific Flask function that will handle the logic for that URL using @app.route(), but first, we need to import a few things from flask using this code
from flask import Flask, render_template, flash, request, redirect, url_for, current_app
1. Index or Home Page: this page shows all the posts created. This is the homepage of our blog app.

# all posts
@app.route('/')
def index():
    flash("Welcome to our website!")
    posts = Post.query.order_by(Post.date_posted)
    return render_template('index.html', post=posts)
Enter fullscreen mode Exit fullscreen mode

Before we can render our posts on the browser, we need to create an HTML page for that. But first, we need to create a folder called templates **in our app. Then, we create the **base.html and index.html inside the templates folder.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <script src="{{ url_for('static', filename='js/jquery.js') }}"></script>
    <script src="{{ url_for('static', filename='js/bootstrap.js') }}"></script>
    <script src="{{ url_for('static', filename='js/script.js') }}"></script>
    {% block title %}{% endblock %}
</head>
<body>

    <header>
        <!-- Fixed navbar -->
        <nav class="navbar navbar-expand-md navbar-dark">
          <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('index') }}"> 
              EDUBLOG
            </a>

            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
              <ul class="navbar-nav me-auto mb-2 mb-md-0">

                <li class="nav-item">
                  <a class="nav-link" aria-current="page" href="{{ url_for('add_post') }}">Add Blog Post</a>
                </li>

              </ul>

              <ul class="navbar-nav  navbar-right">   

                {% if current_user.is_authenticated %}

                <li class="nav-item">
                  <a class="nav-link" aria-current="page" href="{{ url_for('admin') }}">Admin</a>
                </li>

                <li class="nav-item">
                  <a class="nav-link" aria-current="page" href="{{ url_for('dashboard') }}">Dashboard</a>
                </li>

                <li class="nav-item">
                  <a class="nav-link" aria-current="page" href="{{ url_for('logout') }}">Logout</a>
                </li>

                {% else %}

                <li class="nav-item">
                  <a class="nav-link" aria-current="page" href="{{ url_for('add_user') }}">Register</a>
                </li>

                <li class="nav-item">
                  <a class="nav-link" aria-current="page" href="{{ url_for('login') }}">Login</a>
                </li>

                {% endif %}>

              </ul>

            </div>
          </div>
        </nav>
      </header>

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

    <footer class="">
          <span>2022. All rights reserved.</span>
      </footer>

      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The base.html file contains the needed information for all the pages. It also contains some syntax known as 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."

5. Other Features

A. Imports: These are necessary for the functionality of the blog app

from turtle import title
from flask import Flask, render_template, flash, request, redirect, url_for, current_app
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, BooleanField, ValidationError, TextAreaField
from wtforms.validators import DataRequired, EqualTo, Length
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from wtforms.widgets import TextArea
from flask_login import UserMixin, login_user, LoginManager, login_required, logout_user, current_user 
from flask_ckeditor import CKEditor
from flask_ckeditor import CKEditorField
from flask_wtf.file import FileField
from werkzeug.utils import secure_filename
import uuid as uuid
import os
Enter fullscreen mode Exit fullscreen mode

2. Authentication and Authorization: deals with the login, logout, register and admin features

# Flask_Login Stuff
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

@login_manager.user_loader
def load_user(user_id):
    return Users.query.get(int(user_id))
Enter fullscreen mode Exit fullscreen mode

First, we need to create forms which users can use to register and login

#create form class
class UserForm(FlaskForm):
    name = StringField("Name", validators=[DataRequired()])
    username = StringField("Username", validators=[DataRequired()])
    email = StringField("Email", validators=[DataRequired()])
    favorite_color = StringField("Favorite Color")
    about_author = TextAreaField("About Author")
    password_hash =PasswordField("Password", validators=[DataRequired(), EqualTo('password_hash2', message='Passwords must match')])
    password_hash2 = PasswordField("Confirm Password", validators=[DataRequired()])
    profile_pic = FileField("Profile_pic")
    submit = SubmitField("Submit")

class PasswordForm(FlaskForm):
    email = StringField("What's your email?", validators=[DataRequired()])
    password_hash = PasswordField("What's your password?", validators=[DataRequired()])
    submit = SubmitField("Submit")

# login form
class LoginForm(FlaskForm):
    username = StringField("username", validators=[DataRequired()])
    password = PasswordField("password", validators=[DataRequired()])
    submit = SubmitField("submit")
Enter fullscreen mode Exit fullscreen mode

then, we create the login and logout route

#login user
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = Users.query.filter_by(username=form.username.data).first()
        if user:
            if check_password_hash(user.password_hash, form.password.data):
                login_user(user)
                flash("Login successful!!")
                return redirect(url_for('dashboard'))
            else:
                flash("Wrong Password, Try Again!")
        else:
            flash("That user does not exist, Try Again!")

    return render_template('login.html', form=form)

# logout 
@app.route('/logout', methods=['GET', 'POST'])
@login_required
def logout():
    logout_user()
    flash("You have logged out!")
    return redirect(url_for('login'))

# delete user
@app.route('/delete/<int:id>')
@login_required
def delete(id):

    if id == current_user.id or id == 8:
        user_to_delete = Users.query.get_or_404(id)
        name = None
        form = UserForm()

        try:
            db.session.delete(user_to_delete)
            db.session.commit()
            flash("user deleted successfully!!")
            our_users = Users.query.order_by(Users.date_added)
            return render_template("add_user.html", form=form, name=name, our_users=our_users)

        except:
            flash("Whoops! Something went wrong, please try again later...")
            return render_template("add_user.html", form=form, name=name, our_users=our_users)
    else:
        flash("Sorry, you can't delete this user!")
        return redirect(url_for('dashboard'))

#update user record
@app.route('/update/<int:id>', methods=['GET', 'POST'])
@login_required
def update(id):
    form = UserForm()
    name_to_update = Users.query.get_or_404(id)
    if request.method == 'POST':
        name_to_update.name = request.form['name']
        name_to_update.email = request.form['email']
        name_to_update.favorite_color = request.form['favorite_color']
        name_to_update.username = request.form['username']
        name_to_update.about_author = request.form['about_author']

        #check for profile pic
        if request.files['profile_pic']:
            name_to_update.profile_pic = request.files['profile_pic']

        #grab image name
            pic_filename = secure_filename(name_to_update.profile_pic.filename)
            #set uuid
            pic_name = str(uuid.uuid1()) + "_" + pic_filename
            #save the image
            saver = request.files['profile_pic']
            #change it to string and save to db
            name_to_update.profile_pic = pic_name

            try:
                db.session.commit()
                saver.save(os.path.join(app.config['UPLOAD_FOLDER'], pic_name))
                flash("user updated successfully!")
                return render_template("update.html", form=form, name_to_update = name_to_update)
            except:
                flash("Error! try again later")
                return render_template("update.html", form=form, name_to_update = name_to_update)

        else:
            db.session.commit()
            flash("user updated successfully!")
            return render_template("update.html", form=form, name_to_update = name_to_update)
    else:
        return render_template("update.html", form=form, name_to_update = name_to_update, id = id)

# register user
@app.route('/user/add', methods=['GET', 'POST'])
def add_user():
    name = None
    form = UserForm()
    if form.validate_on_submit():
        user = Users.query.filter_by(email=form.email.data).first()
        if user is None:
            hashed_pw = generate_password_hash(form.password_hash.data, "sha256")
            user = Users(name=form.name.data, username=form.username.data, email=form.email.data, favorite_color=form.favorite_color.data, password_hash=hashed_pw)
            db.session.add(user)
            db.session.commit()
        name = form.name.data
        form.name.data = ''
        form.username.data = ''
        form.email.data = ''
        form.favorite_color.data = ''
        form.password_hash.data = ''

        flash("User Added successfully!")
    our_users = Users.query.order_by(Users.date_added)
    return render_template("add_user.html", form=form, name=name, our_users=our_users)
Enter fullscreen mode Exit fullscreen mode

3. Post Operations: check this out to view all the html pages for the blog.
To display a single post...

# individual post
@app.route('/posts/<int:id>', methods=['GET', 'POST'])
def post(id):
    post = Post.query.get_or_404(id)
    return render_template("post.html", post=post)
Enter fullscreen mode Exit fullscreen mode

to add a post...

# add post page
@app.route("/add-post", methods=["GET", "POST"])
@login_required
def add_post():
    form = PostForm()
    image_post = None
    if form.validate_on_submit():
        poster = current_user.id

        if request.files['post_pic']:
            #image_post = None
            post_pic = request.files['post_pic']
            pic_filename = secure_filename(post_pic.filename)
            #set uuid
            pic_name = str(uuid.uuid1()) + "_" + pic_filename
            saver = request.files['post_pic']
            #change it to string and save to db
            post_pic = pic_name

        post = Post(title=form.title.data, content=form.content.data, poster_id=poster, slug=form.slug.data, post_pic=post_pic)
        form.title.data = ''
        form.content.data = ''
        form.post_pic.data = ''
        form.slug.data = ''

        db.session.add(post)
        db.session.commit()
        saver.save(os.path.join(app.config['UPLOAD_FOLDER'], pic_name))
        flash("post submitted successfully!")

    return render_template("add_post.html", post_pic=image_post, form=form)

Enter fullscreen mode Exit fullscreen mode

to update/edit a post...

#update post
@app.route('/posts/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit_post(id):
    posts = Post.query.order_by(Post.date_posted)
    post = Post.query.get_or_404(id)
    form = PostForm()

    if form.validate_on_submit():
        post.title = form.title.data
        #post.author = form.author.data
        form.slug = form.slug.data
        form.content = form.content.data

        db.session.add(post)
        db.session.commit()
        flash("Post updated successfully!")
        return redirect(url_for('post', id=post.id))

    if current_user.id == post.poster_id:
        form.title.data = post.title
        #form.author.data = post.author
        form.slug.data = post.slug
        form.content.data = post.content
        return render_template("edit_post.html", form=form)

    else:
        flash("You aren't authorized to edit this post!")
        return render_template('index.html', post=posts)
Enter fullscreen mode Exit fullscreen mode

to delete a post...

# delete post
@app.route('/posts/delete/<int:id>')
@login_required
def delete_post(id):
    post_to_delete = Post.query.get_or_404(id)
    id = current_user.id

    if id == post_to_delete.poster.id or id == 8:

        try:
            db.session.delete(post_to_delete)
            db.session.commit()
            flash.info("Post deleted successfully")
            posts = Post.query.order_by(Post.date_posted)
            return render_template('index.html', post=posts)

        except:
            flash("Whoops! Something went wrong, please try again later")
            posts = Post.query.order_by(Post.date_posted)
            return render_template('index.html', post=posts)

    else:
        flash("You aren't authorized to delete this post!")
        posts = Post.query.order_by(Post.date_posted)
        return render_template('index.html', post=posts)
Enter fullscreen mode Exit fullscreen mode

4. Error Pages: this display the error on a page
invalid URL... and internal server error...

# invalid URL
@app.errorhandler(404)
def page_not_found(e):
    return render_template("404.html"), 404

# internal server error
@app.errorhandler(500)
def page_not_found(e):
    return render_template("500.html"), 500
Enter fullscreen mode Exit fullscreen mode

Thanks for exploring this article with me!
For the source code: Kindly check out Flask-blog-app, a project I built for the AltSchool Africa second semester examination.

Special Thanks to:

Top comments (0)