How To Build A Blog App With Flask
Tags: webdev, flask, backend, python
Published: 2023-01-05 11:14:47

  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](https://flask.palletsprojects.com/en/2.1.x/)

## 1. Prerequisites
The following are the basic requirements before starting this project:
- [Python](https://www.python.org/downloads/)
- [Visual Studio Code](https://code.visualstudio.com/download)
- [Git bash](https://git-scm.com/downloads) or any other terminal
- [MySQL](https://www.mysql.com/downloads/)

## 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
```
**B. Create a virtual environment:** A [virtual environment](https://www.geeksforgeeks.org/python-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
```
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
```

**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__)
```
**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)
```

## 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
```
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
```
```
#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'))
```
You can check out [this article](https://docs.sqlalchemy.org/en/14/core/tutorial.html) 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)
```
then we you the following in your terminal
```
# create migrations folder
$ flask db init

# make changes
$ flask db migrate -m 'comment'
$ flask db upgrade
```
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)
```
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>
```
The **base.html** file contains the needed information for all the pages. It also contains some syntax known as [Jinja](https://jinja.palletsprojects.com/en/3.1.x/).
"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
```
**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))
```
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")
```
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)
```
**3. Post Operations:** [check this out](https://github.com/faozziyyah/flask-blog-app/tree/main/templates) 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)
```
**_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)

```
**_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)
```
**_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)
```
**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
```
Thanks for exploring this article with me!
For the source code: Kindly check out [Flask-blog-app](https://github.com/faozziyyah/flask-blog-app), a project I built for the [AltSchool Africa](https://www.altschoolafrica.com/) second semester examination.

Special Thanks to:
- John Elder for his tutorials on [Codemy.com](https://www.youtube.com/watch?v=0Qxtt4veJIc&list=PLCC34OHNcOtolz2Vd9ZSeSXWc8Bq23yEz)
- [AltSchool Africa](https://www.altschoolafrica.com/schools/engineering) for their lessons on backend engineering.