How to build a simple Blog site using Flask in Python
A beginner-friendly tutorial on building a blog site with responsive web design, routing, DB CRUD operations, user authentication & internet security
Flask is a well-documented lightweight Python framework which eases the process of building web applications. It has a variety of useful extendable features.
In this tutorial, we will learn how to write backend and frontend code for a simple blog website, practicing the following concepts as we do:
Responsive Web Design using HTML, CSS & Jinja
Routing
Database Management with SQLite and SQLAlchemy
Internet Security with Werkzeug
User Authentication & Authorization
Message Flashing
Note: Everything here will be locally hosted. We will not be able to send a link to others for them to write and view articles on the blog. Yet, proper documentation will enable us to share this project with the world, as seen here on the Ze Blog GitHub repo.
Prerequisites
This tutorial requires some familiarity with Python, HTML and CSS conventions. Expertise isn't a requirement, as we will remain beginner-friendly here.
Before starting this project, it is important to have the following set up:
Bonus: DB Browser for SQLite helps check what we have in the database, but it is not essential.
Initial Preparation
With the following steps, we will create a dedicated folder on the computer for our Flask project, and learn a thing or two about virtual environments as we do.
Create a Project Folder
It is good practice to have all files and folders properly organized on the system. For this blog project, we will create a dedicated folder on the computer. We can do this with the computer's file manager, VS Code or the terminal. Here's the terminal script to create a new directory (folder) and then move into it, using Ze Blog as an example:
$ mkdir ze_blog
$ cd ze_blog
Set Up a Virtual Environment
" Virtual environments are independent groups of Python libraries, one for each project. Packages installed for one project will not affect other projects or the operating system’s packages." - Flask Docs
Virtual environments keep installations and requirements unique to each project. It is recommended to create a new virtual environment for each project, as this prevents some compatibility issues and keeps things nicely organized. Virtual environments are easy to set up, using the venv
module that comes bundled with Python. Any name can be chosen for the virtual environment. We will use env
in this tutorial, but some programmers prefer to give it a similar name to the project.
Warning: Do not touch the virtual environment folder! Do not cd
into it or manipulate files within. Most command-line virtual environment operations use what is already autogenerated by Python in the folder, rather than creating or deleting items. In other words, treat this as a mini system folder like __pycache__
.
For simplicity, we will use the syntax for macOS/Linux in the next two code blocks. Windows users, please check out the applicable syntax from Flask Docs instead.
We start by creating a virtual environment named env
:
$ python3 -m venv env
The next step is to activate our virtual environment with this command:
$ source env/bin/activate
Once activated, the Command Line Interface (CLI) will change to show the virtual environment's name. We will ignore this in future code blocks on this tutorial because all we will need to type is what comes after the $ sign. Still, here is a sample of how an activated virtual environment looks on an Oh My ZSH-flavoured terminal, with Git version control activated as well:
(env) ➜ ze_blog git:(main)
Note: When done with or taking a break from a project, it is good practice to deactivate its virtual environment before moving out of the directory or doing other things in the terminal. This might yet be the simplest possible command in Bash:
$ deactivate
We are still building the blog for now, so there is no deactivation just yet.
Install Flask
Pip is the package installer for Python. It is a very useful module that comes bundled with Python, so we will come across it often when installing Python libraries.
Within the activated environment, we will install Flask using this pip command:
$ pip install Flask
Backend Basics
We now have a Flask project up and running! Well, the backbone of a Flask project, but every forward step counts.
Next up, we will learn how to create our Flask app, prepare some database tables, and design the first few routes which will be used in the front end.
Create a Flask App
We will first create a .flaskenv
file within the home directory of the project. This hidden file enables us to launch the blog with a flask run
command in the terminal, and will have the following lines of code in plain text:
FLASK_ENV = development
FLASK_DEBUG = 1
FLASK_APP = app.py
With these statements, we have told the computer that:
We want to run this Flask project in a development server
We are willing to debug the Flask app as it runs (1 is "True" in Boolean)
We want Flask to recognise
app.py
as the main Flask app file for this project
This brings us to app.py
, eventually the heaviest file in this project. Note that it is better to split the code we will have in app.py
into different Python files which carry out specific functions, but we are keeping things simple here.
Create app.py
within the project's home directory, then write some code within:
from flask import Flask
app = Flask(__name__)
We have now created a Flask instance in our main module. That's a complex way of saying we have told the Python interpreter that this is our Flask app's main Python file.
Prepare a Database
The articles, user details and other data on our blog need to be stored somewhere. This is where a database comes in, as it keeps all the required data in easily accessible tables. Learn more about databases and SQLAlchemy in Flask from Abdelhadi's tutorial on Digital Ocean. Here is a rather relevant snippet:
"SQLAlchemy is an SQL toolkit that provides efficient and high-performing database access for relational databases. It provides ways to interact with several database engines such as SQLite, MySQL, and PostgreSQL. It gives you access to the database’s SQL functionalities. It also gives you an Object Relational Mapper (ORM), which allows you to make queries and handle data using simple Python objects and methods. Flask-SQLAlchemy is a Flask extension that makes using SQLAlchemy with Flask easier, providing you tools and methods to interact with your database in your Flask applications through SQLAlchemy."
Okay, that is a lot of theory. Here is how we will use these tools in practice:
- Installation
We will first install Flask-SQLAlchemy with the following terminal code:
$ pip install Flask-SQLAlchemy
- Imports
We will now return to app.py
to import the inbuilt os
module for configuration purposes, and the SQLAlchemy
class for manipulating our database. Our Python file should look like this by now:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
- Configuration
In this step, we will construct a path for our SQLite database file. We will use some functions from the os
module to store the path of the base directory in a variable named base_dir
. We will then configure SQLAlchemy's URI and secret key, and turn off modification tracking to use less memory.
base_dir = os.path.dirname(os.path.realpath(__file__))
app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + \
os.path.join(base_dir, 'ze_blog.db')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = 'createacomplexsecretkeyhere'
This step can be a bit confusing and abstract, but that is fine for now. We do not need to memorize the syntax. Just understand what it is doing, and then it can be modified for each future Flask project.
Note: The secret key is an important security feature. It is standard practice to create a complex hexadecimal string for each project and hide it from the public.
- Instantiation
We will create a database object using the SQLAlchemy
class, passing the app instance to connect our Flask app with SQLAlchemy. We will store this database object in a variable called db
. We will then use this db
object to interact with our database.
db = SQLAlchemy(app)
db.init_app(app)
We now have our database configured. Time to prepare the tables within.
- Table Creation
We will store our project data with three database models: User
, Article
and Message
.
User Model
Our
User
model creates a table of users using UserMixin, links each user to their articles in the table, and then returns the username as its representation.To have access to
UserMixin
, we will first installFlask-Login
in the terminal:$ pip install Flask-Login
Next, we will import the
UserMixin
subclass from theFlask-Login
module:from flask_login import UserMixin
We can now build the
User
class using the following code:class User(db.Model, UserMixin): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), nullable=False, unique=True) first_name = db.Column(db.String(50), nullable=False) last_name = db.Column(db.String(50), nullable=False) email = db.Column(db.String(80), nullable=False, unique=True) password_hash = db.Column(db.Text, nullable=False) def __repr__(self): return f"User: <{self.username}>"
While some of this code is self-explanatory, please see this excellent article by Abdelhadi for more explanation of how the syntax works. There is even more detail in the SQLAlchemy Docs, which is beyond the scope of this tutorial.
Article Model
The
Article
model creates a table of articles, links each article to its author, and then returns the article's title as its representation.To be able to show the day each entry was posted, we will need to import from the
datetime
module that comes with Python:from datetime import datetime
The rest of the syntax is similar to that of the previous table, as seen below:
class Article(db.Model): __tablename__ = "articles" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80), nullable=False) content = db.Column(db.String, nullable=False) created_on = db.Column(db.DateTime, default=datetime.now()) user_id = db.Column(db.Integer, db.ForeignKey( "users.id"), unique=False, nullable=False) author = db.Column(db.String, nullable=False) def __repr__(self): return f"Article: <{self.title}>"
Message Model
The
Message
model creates a table of messages from site visitors and returns the message title as its representation.class Message(db.Model): __tablename__ = "messages" id = db.Column(db.Integer, primary_key=True) sender = db.Column(db.String(50), nullable=False) email = db.Column(db.String(80), nullable=False) title = db.Column(db.String(80), nullable=False) message = db.Column(db.String, nullable=False) priority = db.Column(db.String(20)) def __repr__(self): return f"Message: <{self.title}>"
Note: The import and configuration code will stay as it is until later in the project, so we will only see relevant code snippets in the next few steps. Here is how the top of our app.py
file should look by now, with some leading comments splashed in:
# Imports: load the source packages with `pip install -r requirements.txt`
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
import os
# Configurations: app, base directory and db
app = Flask(__name__)
base_dir = os.path.dirname(os.path.realpath(__file__))
app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + \
os.path.join(base_dir, 'ze_blog.db')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = 'createacomplexsecretkeyhere'
db = SQLAlchemy(app)
db.init_app(app)
- Initialization
The following syntax ensures that a database file (by the name we have assigned during configuration) is created the first time a route is requested in this Flask app, only that first time, and only if the ze_blog.db
file does not already exist:
@app.before_first_request
def create_tables():
db.create_all()
When we use flask run
in the terminal, a ze_blog.db
file will appear in the project's root directory if none previously existed. We can then use DB Browser for SQLite to see what is happening within the database, as VS Code does not read .db
files.
Design Basic Routes
Routing means mapping a URL to a specific Flask function that will handle the logic for that URL. Routing in Flask is done with the route()
decorator, and the URL is passed as an argument into the decorator. We will use @app.route()
in this tutorial.
We are keeping things simple for now, so we will start with the routes which bring the blog to life and make it user-friendly. Before this, we will need to add a few imports from Flask, leaving us with this statement for the Flask
imports line:
from flask import Flask, flash, render_template, url_for, request, redirect
These imports will become clearer as we go through their use cases.
- Homepage
The index route is always represented with just '/'
in routing. Since this is the homepage of a blog, we will make an index()
function to display all articles.
@app.route('/')
def index():
articles = Article.query.all()
context = {
"articles": articles
}
return render_template('index.html', **context)
Here, we have gotten all of the blog's articles by querying the Article
model in our database. The **context
keyword argument then tells Flask the variables to use when rendering the index.html
template. See more details about rendering templates explained by Flask Docs.
- About
This simple function renders the about.html
template as a webpage which tells the visitor about the blog site and its creator.
@app.route('/about')
def about():
return render_template('about.html')
- Contact
Our contact()
function collects feedback from users via a form in the contact.html
template, stores it on the "messages"
table in the database using the Message
model, and then redirects to the homepage after flashing a success message.
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
sender = request.form.get('name')
email = request.form.get('email')
title = request.form.get('title')
message = request.form.get('message')
priority = request.form.get('priority')
new_message = Message(sender=sender, email=email,
title=title, message=message,
priority=priority)
db.session.add(new_message)
db.session.commit()
flash("Message sent. Thanks for reaching out!")
return redirect(url_for('index'))
return render_template('contact.html')
Note: The 'GET'
method gets data from the backend to display to users, while the 'POST'
method allows a user to post data to the backend. 'GET'
is the default routing method, so it does not need to be passed as an argument when no other method is used. Read more about routing and its various methods on this nice article by Todd Birchard.
Note: The only way to view these messages on our blog project is with DB Browser, as sending emails with Flask is beyond the scope of this tutorial.
We have now set up the basic backend code needed to get the blog up and running. We will soon write blocks of code which will manipulate the User
and Article
tables, but that would be after designing the user interface.
Frontend with Flask
With these two steps, we will learn the standards for creating a templates
folder in Flask, practice some HTML and CSS, and learn how to manipulate HTML templates with Jinja.
Create HTML Templates, spiced with Jinja
"Jinja is a fast, expressive, extensible templating engine. Special placeholders in the template allow writing code similar to Python syntax. Then the template is passed data to render the final document." - Jinja Docs
Jinja lets us make our HTML files dynamic with syntax that resembles Python. Jinja code is written where needed within the HTML code, so it is not stored as a separate script.
We start this step by creating a templates
folder within the project's main folder or root directory. This folder is a standard in Flask development, as it is from here that all HTML files used in the project will be accessed by the Flask app. For example, here is the templates folder in Ze Blog.
We will create these HTML files for the blog: base.html
, index.html
, about.html
, contact.html
, signup.html
, login.html
, contribute.html
, edit.html
and article.html
.
For now, we will only fill in the first four files listed above, as we have created their relevant routes earlier in this tutorial. The remaining files are more technical and will be written along with the upcoming backend code. Let us start with base.html
, then.
"The most powerful part of Jinja is template inheritance. Template inheritance allows you to build a base “skeleton” template that contains all the common elements of your site and defines blocks that child templates can override." - Jinja Docs
The base template makes use of template inheritance, a very useful shortcut provided by Jinja. In the base.html
file, we can put every block of code that we would normally have to repeat in other HTML files. We will then inherit the base template into those child templates (the other HTML files) and only write the code that pertains to the functions of a particular HTML file within itself. Thus, we will adhere to the DRY principle and have a much cleaner program.
This file does the heavy lifting for the site's design, so it has some heft. Here is what was used in the sample blog: Ze Blog's base template. This tutorial assumes prior familiarity with the basics of HTML and CSS, so we will focus on Jinja here, with some code snippets taken from that sample file to illustrate certain concepts.
- Flask's url_for() Function
"Flask url_for is defined as a function that enables developers to build and generate URLs on a Flask application. As a best practice, it is the url_for function that is required to be used, as hard coding the URL in templates and view function of the Flask application tends to utilize more time during modification." - EDUCBA
<a href="{{url_for('index')}}">
<img
src="{{url_for('static',filename= 'Ze_Blog_Logo/site.png')}}"
alt="Logo"
width="120"
height="40"
/>
</a>
In this image-adding code block, we have used url_for
and Jinja syntax twice:
To create a link to the
index.html
templateTo create a link that instructs the interpreter to search the
static
folder for a file with a particular name
Note: The use of double curly brackets {{ sample code }}
is the default syntax for expressions in Jinja, as well as the use of quotation marks around the file/folder name with no .html
in the url_for()
function's argument.
- User Authentication and IF Statements
<div class="nav-item">
{% if current_user.is_authenticated %}
<a href="{{url_for('index')}}">Home</a>
<a href="{{url_for('about')}}">About</a>
<a href="{{url_for('contribute')}}">Contribute</a>
<a href="{{url_for('contact')}}">Contact</a>
<a href="{{url_for('logout')}}">Log Out</a>
{% else %}
<a href="{{url_for('index')}}">Home</a>
<a href="{{url_for('about')}}">About</a>
<a href="{{url_for('contact')}}">Contact</a>
<a href="{{url_for('register')}}">Sign Up</a>
<a href="{{url_for('login')}}">Log In</a>
{% endif %}
</div>
With current_user.is_authenticated
and an IF-ELSE
statement, we can ensure that only logged-in users will see a version of the navigation bar that lets them post an article or log out. Unauthorized users will see a navbar which lets them sign up or log in, but no option of contributing an article to the blog.
Note: Every statement block in Jinja must have an "end..." to close it (eg if
and endif
), and {% sample code %}
is the default syntax for statements in Jinja.
- Message Flashing and WITH Statements
Message flashing is provided by Flask as a simple way to give feedback to our users. We will explore the content of the messages when building the relevant routes, but our base template will have the following code block to allow a certain message to be printed when required:
{% with messages = get_flashed_messages() %} {% if messages %}
<div class="container">
{% for message in messages %}
<p>{{ message }} <a href="{{request.path}}">Dismiss</a></p>
{% endfor %}
</div>
{% endif %} {% endwith %}
Please read more about message flashing syntax on Flask Docs, with a lot more detail available there than what the scope of this tutorial allows.
Note: {{request.path}}
provides a URL which redirects to the current webpage. Thus, clicking the "Dismiss" link beside the message reloads the current page. This makes the flashed message disappear because flashed messages do not show up again after another request has been made. There are probably better ways to do this message dismissal - such as this jQuery answer on Stack Overflow - but {{request.path}}
works easily enough.
- Block Content
The base.html
template is just a skeleton upon which every endpoint is fleshed out. With this in mind, it is easy to understand why its <main>
element is not doing much, as this is where the specific job of each child template will come in. What little code we do have in the <main>
element is quite important, as the {% block content %}
statement allows each child template to inherit what is in base.html
and avoid repetition.
<main class="container">{% block content %}{% endblock content %}</main>
That's it. Yes, really. We open a content block and then close it immediately. We will then use it for inheritance in the next step.
Note: Any code we put before and after the {% block content %}{% endblock content %}
statement will apply to all child templates.
- Inheritance
The index.html
, about.html
and contact.html
files use basic HTML concepts and Jinja syntax standards that have been discussed earlier in this tutorial. Click each hyperlink to see the equivalent sample from Ze Blog.
This is the Jinja syntax for inheriting from the base template as extensions into any of the child templates:
{% extends 'base.html' %} {% block content %}
<p>Sample HTML code for the specific webpage</p>
{% endblock content %}
Beautify with CSS
Just like the templates
folder in the previous step, the static
folder is a standard for frontend development with Flask. A Flask project's static folder contains all assets used by the project's templates, including CSS files, JavaScript files, and images. Within the static
folder, we will create a css
folder, which will then have our main.css
file. For this beginner-friendly project, all styling will be done within main.css
.
The choice of blog design within main.css
is entirely up to the programmer. There are many ways of doing this - including the popular Bootstrap styling toolkit with many handy tutorials online - but here is the simple responsive Vanilla CSS used on Ze Blog.
Bonus: The Ze Blog logo was made for free with NameCheap's Logo Maker. As a reminder, this logo and any other image used in the project should be stored in the static
folder.
Further Preparation
With the basic frontend and backend code now covered, we will focus on the more specific functions that this blog site will be able to perform.
Note: As with previous sections of this tutorial, most of the syntax explanation for the next two major segments has been outsourced to more detailed online tutorials and official documentation, with sample code being available on Ze Blog.
Imports
We already have all the necessary installations, so the next step is to make a few more imports which will become clearer with their use cases. It is faster to just type them at once, so this is how the top of our Python file should now look:
# Imports: load the source packages with `pip install -r requirements.txt`
from flask import Flask, flash, render_template, url_for, request, redirect
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import current_user, login_user, logout_user, login_required, LoginManager, UserMixin
from datetime import datetime
import os
The werkzeug.security
module comes with Flask and provides password hashing for improved internet security, in case of a data breach. This means that we will never store the users' raw passwords, but rather a complex hashed form via generate_password_hash()
which will only be decoded by check_password_hash()
when a site visitor tries to log in.
Instantiation
To use the Flask-Login
module, we pass our app as an instance of LoginManager():
login_manager = LoginManager(app)
User Management
A major function of this blog will be handling users and their data. This involves user registration and authentication. We will explore the Python code used to achieve said objectives in this section of the tutorial. To see the HTML templates used in our sample project, check out signup.html
and login.html
on Ze Blog.
Register a User
Our register()
function collects user data from the registration form in the signup.html
template. If the username is already taken, we will flash an error and reload the registration page. If the given username is available, we will add this new user's data to the "users"
table in the database with a hashed password, using the User
model. We will then redirect them to the login page.
@app.route('/signup', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
first_name = request.form.get('first_name')
last_name = request.form.get('last_name')
email = request.form.get('email')
password = request.form.get('password')
username_exists = User.query.filter_by(
username=username).first()
if username_exists:
flash("This username already exists.")
return redirect(url_for('register'))
email_exists = User.query.filter_by(
email=email).first()
if email_exists:
flash("This email is already registered.")
return redirect(url_for('register'))
password_hash = generate_password_hash(password)
new_user = User(username=username, first_name=first_name,
last_name=last_name, email=email,
password_hash=password_hash)
db.session.add(new_user)
db.session.commit()
flash("You are now signed up.")
return redirect(url_for('login'))
return render_template('signup.html')
Log a User In
The login()
function checks if the user's input on the login.html
form matches any username and hashed password pair on the "users"
table in the database. It then redirects to the homepage if the user is successfully authenticated, but flashes an error and reloads the login page if the data is invalid.
@app.route('/login', methods=['GET', 'POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password_hash, password):
login_user(user)
flash("You are now logged in.")
return redirect(url_for('index'))
if (
user and check_password_hash(user.password_hash, password)
) == False:
flash("Please provide valid credentials.")
return redirect(url_for('login'))
return render_template('login.html')
We have now achieved user authentication, only allowing those who provide the correct name and password to be logged into the blog.
Log Out the Current User
The Flask-Login
module provides an easy logout_user()
function, which helps us revoke a user's access to protected routes once they click the logout link:
@app.route('/logout')
def logout():
logout_user()
flash("You have been logged out.")
return redirect(url_for('index'))
No HTML template is needed here, as the route simply logs the user out and redirects them to the homepage.
Note: For the rest of this tutorial, we will assume that a user is currently logged in.
Article Management
Another major feature of our blog will be handling articles on the site. This involves user authorization, as some features and routes should only be available to the author of the article. To see the relevant HTML templates used in our sample project, check out article.html
, contribute.html
and edit.html
on Ze Blog.
Display a Single Article
Let us start with an easy one. This article(id)
function takes the article's ID as an argument, gets the requested article from the database, and then displays said article via the article.html
template.
@app.route('/article/<int:id>/')
def article(id):
article = Article.query.get_or_404(id)
context = {
"article": article
}
return render_template('article.html', **context)
Just like the homepage, we do not need user authentication or authorization for this route, because every visitor to this blog will be allowed to read every article on it.
Post a New Article
A user has to be logged in to be able to post an article, as some user data is required to identify the author and later authorize them to edit or delete their articles. Only user authentication (not authorization) is needed for this step, as the protected data (article) doesn't exist until it has been created.
This is where we will use an easy authentication tool provided by Flask-Login
. The @login_required
decorator does all the heavy lifting for us, as it ensures that a protected page will only be rendered if the current site visitor is logged in as a user. If the visitor is not logged in, it will display an error message.
Our contribute()
function will enable authenticated users to post new entries to the "articles"
table in our database using the Article
model, after getting relevant data from the form in contribute.html
. We will then thank the user and redirect them to the homepage to see all articles. If the new article's title is taken, we will flash an error and reload the page instead.
@app.route('/contribute', methods=['GET', 'POST'])
@login_required
def contribute():
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
user_id = current_user.id
author = current_user.username
title_exists = Article.query.filter_by(title=title).first()
if title_exists:
flash("This article already exists. Please choose a new title.")
return redirect(url_for('contribute'))
new_article = Article(title=title, content=content,
user_id=user_id, author=author)
db.session.add(new_article)
db.session.commit()
flash("Thanks for sharing your thoughts.")
return redirect(url_for('index'))
return render_template('contribute.html')
User Authorization
We should only allow authorized users to manipulate the articles in our database. While creating routes for editing and deleting articles, we will practice the concept of user authorization by writing IF
statements which check that the currently logged in user is a particular article's author. We will see these IF
statements at work in our last two steps.
Edit an Article
The edit(id)
function enables article updates by the author. It takes the article's ID as an argument, then gets the article's author from the database to compare with the logged in user's username. If these are the same, it renders the edit.html
template for the user to edit their article, and then posts the user's changes to the database. The function then loads the article's page if it has been successfully edited, but flashes an error and redirects home if the user is unauthorized to make changes.
@app.route('/edit/<int:id>/', methods=['GET', 'POST'])
@login_required
def edit(id):
article_to_edit = Article.query.get_or_404(id)
if current_user.username == article_to_edit.author:
if request.method == 'POST':
article_to_edit.title = request.form.get('title')
article_to_edit.content = request.form.get('content')
db.session.commit()
flash("Your changes have been saved.")
return redirect(url_for('article', id=article_to_edit.id))
context = {
'article': article_to_edit
}
return render_template('edit.html', **context)
flash("You cannot edit another user's article.")
return redirect(url_for('index'))
Delete an Article
Deletion does not need an HTML file, as it is a route which does one action and then redirects home. Our delete(id)
function enables article deletion from the database by that article's author only. Just as above, the function first queries the database for the entry whose ID matches the article ID it has received as an argument, then it checks if the current user is the author of the chosen article before executing the db.session.delete()
function. We will then redirect to the homepage when the article is successfully deleted, or flash an error and redirect home if the user is unauthorized.
@app.route('/delete/<int:id>/', methods=['GET'])
@login_required
def delete(id):
article_to_delete = Article.query.get_or_404(id)
if current_user.username == article_to_delete.author:
db.session.delete(article_to_delete)
db.session.commit()
flash("That article is gone!")
return redirect(url_for('index'))
flash("You cannot delete another user's article.")
return redirect(url_for('index'))
We have completed this project by designing our blog site and adding all desired features with clean code. Congrats! But there is one more thing...
Finishing Touches
It is recommended practice to use a requirements.txt
file to store all the packages we have installed in this application:
pip freeze > requirements.txt
This file can then be accessed by anybody who wants to use our webapp, as they can install all our packages directly with one command:
pip install -r requirements.txt
And we are done! Our blog is ready for its many users and articles. Time to test it.
Taste the Magic
Let us see what we have created. This is done via flask run
in the terminal, from where we will open the generated link on a browser. Here is an example of how Ze Blog looks after a Sample User logs in:
To terminate the Flask process and return to the normal CLI, click Ctrl C on any OS. Remember to deactivate
this project's virtual environment when done with it.
Conclusion
Thanks for exploring the beautiful simplicity of Flask with me! 💜
We have learnt how to set up a Flask environment in VS Code, manipulate HTML templates with Jinja, and perform CRUD operations on a database with SQLite and SQLAlchemy. We have also practiced responsive web design with HTML and CSS, routing, password hashing with Werkzeug, message flashing, internet security, user authentication and user authorization.
Please share this tutorial with fellow WebDev enthusiasts, especially those interested in Backend Software Engineering with the Python Flask framework.
Check out Ze Blog, a project I built using these principles for the second-semester exam at AltSchool Africa.
There's a lot more to follow, here in Austin's Space. Follow me on Hashnode, GitHub and Twitter for more insights from my tech journey.
✨ Cheers to making more magic! ✨
Credits
Caleb's Flask lessons at AltSchool Africa's School of Engineering. See more lessons from him in my AltSchool Python repository
Abdelhadi's Flask and Flask-SQLAlchemy tutorials on Digital Ocean
The Art of Routing in Flask, by Todd Birchard
Didi's Technical Writing lectures at AltSchool