faozziyyah
Posted on January 5, 2023
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:
- Python
- Visual Studio Code
- Git bash or any other terminal
- MySQL
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 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 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.
"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 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, a project I built for the AltSchool Africa second semester examination.
Special Thanks to:
- John Elder for his tutorials on Codemy.com
- AltSchool Africa for their lessons on backend engineering.
Posted on January 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.