Table of contents
- 1. Introduction
- 2. Prerequisites/Packages
- 3. Getting Started
- 4. Creating Models and Database
- 5. Using WTForms (Web Forms)
- 6. Using Jinja Templates
- 7. Using the Bootstrap toolkit
- 8. Performing CRUD Operations on the Database
- 12. Using Blueprints and Project Restructure
- 10. Authentication and Authorization
- 11. Conclusion
- 12. Credits
1. Introduction
Flask is a lightweight WSGI python framework, that provides many powerful tools, libraries and technologies for creating quick and easy web applications, with the ability to scale up to complex web applications. It depends on the Werkzeug WSGI toolkit and Jinja template engine.
WSGI is the Web Server Gateway Interface. It is a specification that describes how a web server communicates with web applications, and how web applications can be chained together to process one request.
Jinja is a fast, expressive, extensible templating engine, which allows writing codes similar to Python syntax.
In this beginner-friendly tutorial, you will be introduced to some basic python concepts (such as IF statement, For loops, lists, variables, functions, classes etc.), different flask libraries, modules and packages, the basic use of the Bootstrap toolkit (to style and visually enhance your application) and use of Jinja templates (to dynamically build HTML pages).
By the end of this tutorial, you will be able to build a blog site using the following concepts/features:
virtual environment
routing in flask
environment variables
webforms and models
database operations (CRUD) with SQLAlchemy ORM
flask blueprints
authentication/authorization
flask database migrations
comments, likes, search functionalities
flask pagination and more.
You can check the source code for this project on GitHub.
2. Prerequisites/Packages
Python/Python 3: installed, set up and running on your machine. For this tutorial, I’m using a Windows Machine. See the tutorial on how to install and check Python 3 version on your machine.
VS Code Editor or any other IDE of your choice.
Basic knowledge in Frontend development (HTML, CSS & JavaScript) and Python, such as functions, classes, conditional statements, for loops, lists, variables, data types etc.
3. Getting Started
Create and Activate Your Virtual Environment
A Virtual Environment is like a container that holds and keeps all the dependencies required by a particular project isolated from other projects. This is an effective way to contain your installed libraries, modules and packages, thus managing your application requirements and avoiding installing them system-wide.
Create your project folder and open it on VS Code.
Either use the windows terminal (press the Window key and type CMD, then press Enter to open it.). Type the command lines below.
$ mkdir my-blog-app # to create the project folder $ cd my-blog-app # to enter into the project folder $ code # to open it in VS Code
Or simply use your PC's File Explorer. Navigate to where you want to locate your project and create the project folder (Ctrl + N). and name it. Then right-click and select open with Code. Then continue in the terminal on VS Code.
Note: If the terminal is not opened, click on the Terminal menu and select New Terminal (or use the shortcut 'Ctrl + Shift + ').
Create your virtual environment. Use the command
$ python -m venv [virtual-environment-name]
. This creates a folder with the virtual environment name in your project folder. See a sample below.$ python -m venv venv
Activate the virtual environment to localize all your future installations for the project.
On Windows OS, using Windows Terminal, Powershell:
$ venv/Scripts/activate
On Mac OS, using bash shell:
$ source venv/bin/activate
Once you activate it, the virtual environment name is closed in brackets in front of the current working directory, for example, (venv) my-blog-app>
.
Note*: do not touch or modify any file or folder in the* venv folder.
Install Flask
On your terminal, use the command below to install flask into the virtual environment. This will install all the Flask packages as shown below.
$ pip3 install flask
The below command works too.
$ pip install flask
Create Requirements.txt File
The Requirements.txt file is a file that stores the list and versions of all the libraries, modules, and packages installed and used while building a particular project. In other words, it stores all the dependencies for a particular project to run successfully. This file is localized (or resides) in the root directory of your project.
This file helps to solve all compatibility issues that might arise in the future when you or someone else tries to run the project on another machine.
Use the command below in your terminal to create a requirements.txt file.
pip freeze > requirements.txt
So each time we install a package for our project, ensure to run this command to update the requirements.txt file.
Create a Simple Routing
Let's create our first route.
On the Explorer section of the VS Code, right-click on a clear space to add a new folder called "app" in your project root directory and also create a file called __init__.py inside it (this makes the app folder behave as a package folder with modules). Also create another file named main.py, which opens in the Editor section.
Import Flask and instantiate it.
# Start by importing Flask into the main.py file: from flask import Flask # create an instance of the Flask as app app = Flask(__name__)
Create a simple route that says "Hello World!!!"
@app.route("/") def hello(): return "Hello World!!!"
@app.route is the decorator that handles the route function
def hello()
Then an if statement checks the app instance and runs the app.
if __name__ == "__main__": app.run(debug=True)
debug=True to enable the server to detect and debug changes in our code and restart while updating the server with the changes.
This last part of the code enables the server to start and run the app code in the main.py file.
Then we will start our server in the terminal with the below command:
$ python app\main.py
Once the server starts, it serves you the address and port where it is running: http://127.0.0.1:5000, which is also the same as your local machine (localhost:5000).
Copy the server address and launch it on your Internet Browser (client-side). Remember, this is what we asked the route function
def hello()
to return.
Rendering HTML templates
Let's progress further to make our route render an HTML template.
First, import "render_template" from flask, as an additional module.
from flask import Flask, render_template
Add the below codes to the main.py file, below the
def hello()
route.# a route rendering an HTML template (index.html) @app.route("/home") def index(): return render_template("index.html")
Your main.py file should look like this...
from flask import Flask, render_template app = Flask(__name__) # a simple route @app.route("/") def hello(): return "Hello World!!!" # rendering a template @app.route("/home") def index(): return render_template("index.html") if __name__ == "__main__": app.run(debug=True)
Create a folder called templates in the app folder.
Note*: this is where all the HTML template files should be stored.*
Also create the HTML file named index.html inside the template folder, just the same way you created the main.py file. Then, write simple HTML code as shown below:
<!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"> <title>My Blog App</title> </head> <body> <h1>My Blog App</h1> <p>Hi, welcome to my new blog</p> </body> </html>
Ensure that your server is still running or use the command
python app\main.py
in your terminal to start your server.Then add /home to your initial server address to go to the route
def index()
we created on our main.py file, i.e http://127.0.0.1:5000/home
Set Environment Variables
Environment Variables are variables that are globally defined throughout the project environment, and they are used to hide sensitive information about the application, such as the SECRET_KEY, DATABASE_URI etc.
Create a file called ".env" (or ".flaskenv") file in the root directory.
Then install the package "python-dotenv" using the terminal, which helps to read the .env files.
pip install python-dotenv
Define below variables in the .env file.
# .env file FLASK_APP = main FLASK_ENV = development FLASK_DEBUG = True
In your main.py file, comment out or remove the line of code below.
# if __name__ == "__main__": # app.run(debug=True)
Now, instead of starting your server with
python app\main.py
, you can now useflask run
in your terminal.
4. Creating Models and Database
Next, we will need to define our database tables/models to create a database to store our blog data. First, we will need to install the flask-sqlalchemy package to successfully create our database and also later interact with it.
Flask-SQLAlchemy is an extension that provides and simplifes the functionality for SQLALchemy to our flask application. SQLAlchemy is the Python SQL toolkit and Object Relational Mapper (ORM) that gives application developers the full power and flexibility of SQL.
Since we are building a blog, we will need the Article model for the articles table to start with. To do this, we need to:
Create Article Model
first, install flask-sqlalchemy, using the terminal.
$ pip3 install flask-sqlalchemy
Create models.py file in the app folder for all the models we will create.
and import SQLAlchemy from flask_sqlalchemy.
from flask_sqlalchemy import SQLAlchemy
Instantiate the SQLAlchemy class.
db = SQLAlchemy()
Then begin to define the Article model class using the db.Model class as the parent class. Note: also import datetime from datetime, for the datetime function.
# models.py file from flask_sqlalchemy import SQLAlchemy from datetime import datetime #imported for date_posted # Instantiating the SQLAlchemy Class db = SQLAlchemy() # ARTICLE MODEL/TABLE class Article(db.Model): __tablename__ = "articles" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) date_posted = db.Column(db.DateTime, default=datetime.utcnow) def __repr__(self): return f"Article: <{self.title}>"
__tablename__ defines the table name.
db.Column defines the table columns and their attributes.
db.Integer, db.Strings, db.Text, db.Datetime and db.Boolean defines the column datatype. db.Strings can take character length.
primary_key=True for marking a column or columns as Primary Key, usually used for "id" column.
nullable=False meaning the column cannot have a null value.
Configure and Create Database
Add the below variables to the .env file
# SQLALCHEMY CONFIG FOR THE DATABASE SECRET_KEY = enter-your-super-secret-key SQLALCHEMY_DATABASE_URI = "sqlite:///my_blog.db" SQLALCHEMY_TRACK_MODIFICATIONS = True
Create a config.py file and from
dotenv
importload_dotenv
. Theload_dotenv
help to locate the .env file and load its content into theos
. Also, importos
, and use itsgetenv
method to get the environment variable into your application within the Settings class and instantiate the class as sett, as seen below.```python from dotenv import load_dotenv import os
load_dotenv()
class Settings: environment = os.getenv("FLASK_ENV") debug = os.getenv("FLASK_DEBUG")
secret_key = os.getenv("SECRET_KEY")
database_uri = os.getenv("SQLALCHEMY_DATABASE_URI")
track_modifications = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS")
#Instantiate the Settings class sett = Settings()
3. After we instantiate the "Settings" class as "**sett**", import it into **app/\_\_init\_\_.py** file. This makes "**sett**" available as a module in "**app" package folder**, instead of from **app/article folder**, so we can import it directly from **app** as a module. For example, we can now do this in main.py, `from app import sett`, instead of `from app.config import sett`
```python
# __init__.py file in "app" folder
from .config import sett
So on the main.py file, instead of using the variable directly like this...
# SQLALCHEMY CONFIG FOR THE DATABASE app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///my_blog.db" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECRET_KEY"] = "enter-your-super-secret-key"
We will do this instead...
from app import sett # SQLALCHEMY CONFIG FOR THE DATABASE app.config["SQLALCHEMY_DATABASE_URI"] = sett.database_uri app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = sett.track_modifications app.config["SECRET_KEY"] = sett.secret_key
On the main.py file, import the db instance from models.py into main.py. Then, add below to your code after the
app = Flask(__name__)
.# SQLALCHEMY CONFIG FOR THE DATABASE app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///my_blog.db" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECRET_KEY"] = "enter-your-super-secret-key" # INITIALIZING DB APP db.init_app(app) # CREATING ALL DB TABLES with app.app_context(): db.create_all()
The main.py file should now look like this...
# main.py file from flask import Flask, render_template from models import db app = Flask(__name__) # SQLALCHEMY CONFIG FOR THE DATABASE app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///my_blog.db" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECRET_KEY"] = "enter-your-super-secret-key" # INITIALIZING DB db.init_app(app) # CREATING ALL DB TABLES with app.app_context(): db.create_all() # CREATING ROUTES # simple hello route @app.route("/") def hello(): return "Hello World!!!" # a route rendering an HTML template (index.html) @app.route("/home") def index(): return render_template("index.html")
Then start the server using
flask run
in the terminal. Once the server starts, it creates the database file (my_blog.db) in a folder called "instance". Now your DB table is created.
5. Using WTForms (Web Forms)
Web Forms are tools used for collecting data from users which are stores in the database, and also allow users to send data to the application. WTForms is a flexible tool used for validating and rendering forms in Python web development.
Flask-WTF is an integration tool of Flask and WTForms.
To use the Web Forms in Flask...
Install Flask-WTF using our terminal. You can read the Flask-WTF documentation to know more about using it.
Note: Always make sure your virtual environment is still active before any installation.
pip install flask-wtf
Create webforms.py in the app folder and import FlaskForm class and the needed Field types which helps to define the form fields atributes in the class ArticleForm with FlaskForm as the parent class, as shown below.
# webforms.py from flask_wtf import FlaskForm from wtforms import StringField, TextField, SubmitField # ARTICLE FORM class ArticleForm(FlaskForm): title = StringField(label="Title", validators=[InputRequired()]) content = TextField(label="Content", validators=[InputRequired()]) submit = SubmitField(label="Submit")
The form fields "title" and "content" will allow us to collect data and store it in the title and content columns in our database's articles table, but the id and date_posted columns will be auto-generated by the database since we set the
primary_key=True
for the id column and the date_posted column defaulted to current datetime usingdefault=datetime.utcnow
Import the created ArticleForm class from webforms into the main.py file and modify the index route to include the form as shown below.
# add to main.py file from webforms import ArticleForm @app.route("/home") def index(): form = ArticleForm() return render_template("index.html", form=form)
Set up our index.html using Jinja templating to add the form into our HTML code.
Note: we will look briefly into Jinja template engine in the next chapter of this tutorial.
<!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"> <title>My Blog App</title> </head> <body> <h1>My Blog App</h1> <p>Hi, welcome to my new blog</p> <div> <h2>Create An Article</h2> <div> <form action="{{ url_for('index') }}" method="POST"> <div>{{ form.hidden_tag() }}</div> <div>{{ form.title.label }}</div> <div>{{ form.title }}</div> <div>{{ form.content.label }}</div> <div>{{ form.content }}</div> <div>{{ form.submit }}</div> </form> </div> </body> </html>
The web page should display like this...
6. Using Jinja Templates
In this section, we are going to briefly talk about a few concepts of Jinja templates needed in this project. Read more about templates here.
A Template contains variables and/or expressions, which get replaced with values when a template is rendered; and tags, which control the logic of the template, such as HTML templates which are rendered dynamically.
Jinja Delimiters are used to insert jinja codes into other types of code, such as HTML codes in a web page. The default Jinja delimiters are configured as follows:
{% ... %}
for statements such asd ifs, loops etc.{{ ... }}
for expressions to print to the template output{# ... #}
for comments not included in the template output
Template Inheritance allows us to build a base "skeleton" template that contains all the common sections or elements of your web application and also defines blocks that child templates can be rendered into.
{% extends %}
tags are used to tell a child template that it should extend another template, usually the base template. It is the starting code for the child templates. For example,{% extends base.html %}
.{% block %} ... {% endblock %}
provides a placeholder to fill in both the parent and child templates, and also defines the content that fills it in the parent template. Examples are:{% block title %} ... {% endblock title %}
used within the<title>
tag{% block content %} ... {% endblock content %}
used mostly within the<div>
tag.{% block body %} ... {% endblock body %}
used within the<body>
tag.{% block header %} ... {% endblock header %}
used within the<header>
tag.{% block footer %} ... {% endblock footer %}
used within the<footer>
tag.
Now let's start creating our template HTML files in the templates folder while implementing the Jinja template concepts mentioned above.
The base.html file:
The base.html file contains 3 sections within the <body>
tag, which will appear on every other HTML page. These sections are namely:
Header Section*,* having the header area and navigation menu bars
Main Content Section, this is where other HTML file content will be inserted into the base.html. Other HTMl pages are inserted in between the
{% block content %}
and{% endblock content %}
.Footer Section.
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
<title>My Blog App</title>
</head>
<body>
<!-- HEADER SECTION -->
<nav class="navbar navbar-light fixed-top text-lg-center bg-primary border border-primary">
<div class="container container-sm">
<!-- HEADING/LOGO AREA -->
<div class="container">
<a class="navbar-brand" href="{{url_for('index')}}">
<h1 class="text-white"><b>Blogger's Zone</b></h1>
</a>
</div>
<!-- NAVIGATION MENU BAR -->
<div class="container">
<ul class="nav navbar-dark nav-tabs nav-justified">
<li class="nav-item">
<a class="nav-link active" href="{{url_for('index')}}"><b>Home</b></a>
</li>
</ul>
</div>
</div>
</nav>
<!-- MAIN CONTENT SECTION -->
<main>
<div><br><br><br><br><br><br><br></div>
<div class="container">
{% block content %}
{% endblock content %}
</div>
<div><br><br><br></div>
</main>
<!-- FOOTER SECTION -->
<div class="text-md-center text-muted bg-primary border border-primary fixed-bottom">
<footer>
<p class="text-light">
<small>
© Gregory (OSQUAREG Tech)
<script>document.write(new Date().getFullYear())</script>
</small>
</p>
</footer>
</div>
</body>
</html>
The index.html file:
Starts with {% extends base.html %}
, then the {% block content %}
and ends with {% endblock %}
. The {% block content %}
tag connects every code within it into the main content section of the base.html, where the {% block content %}
tag is located.
{% extends 'base.html' %}
{% block content %}
<center>
<h1><b>Blogger's Zone</b></h1>
<p>Hi, welcome to my new blog</p>
<h4><a href="{{url_for('create')}}">Create an Article</a></h4>
</center>
{% endblock content %}
The create-article.html file:
The create-article.html file will be rewritten as shown below. Note, we have not created the route to create an article, which is used in action="{{ url_for('create') }}"
{% extends 'base.html' %}
{% block content %}
<div>
<center>
<h3><b>Create An Article</b></h3>
</center>
<br>
<div class="container form-group">
<form class="row g-3" action="{{ url_for('create') }}" method="POST">
<div class="row">{{ form.hidden_tag() }}</div>
<div class="row">
{{ form.title.label(class="form-label", style="font-weight:bold; font-size:18px") }}
{{ form.title(class="row form-control shadow p-2 mb-5 bg-body rounded border border-primary") }}
</div>
<div class="row">
{{ form.content.label (class="form-label", style="font-weight:bold; font-size:18px") }}
<div class="row form-control shadow p-2 mb-5 bg-body rounded border border-primary">
{{ form.content }}
</div>
</div>
<div class="d-grid gap-2 col-3 mx-auto">
{{ form.submit(class="btn btn btn-outline-primary" , style="font-weight:bold; font-size:18px") }}
</div>
</form>
</div>
</div>
{% endblock content %}
7. Using the Bootstrap toolkit
Before we progress to performing CRUD operations, let's beautify our application a little, by adding Bootstrap CSS and JS.
Add the bootstrap CSS styling and JS scripts link to the <head> tag of the base.html file and also apply CSS styling class to different areas of the HTML page, as shown below. To know more about using Bootstrap, click here
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
<title>My Blog App</title>
</head>
<body>
<center>
<h1><b>My Blog App</b></h1>
<p>Hi, welcome to my new blog</p>
</center>
<div>
<center>
<h3><b>Create An Article</b></h3>
</center>
<br>
<div class="container form-group">
<form class="row g-3" action="{{ url_for('index') }}" method="POST">
<div class="row">{{ form.hidden_tag() }}</div>
<div class="row">
{{ form.title.label(class="form-label", style="font-weight:bold; font-size:18px") }}
{{ form.title(class="row form-control shadow p-2 mb-5 bg-body rounded border border-primary") }}
</div>
<div class="row">
{{ form.content.label (class="form-label", style="font-weight:bold; font-size:18px") }}
<div class="row form-control shadow p-2 mb-5 bg-body rounded border border-primary">
{{ form.content }}
</div>
</div>
<div class="d-grid gap-2 col-3 mx-auto">
{{ form.submit(class="btn btn btn-outline-primary" , style="font-weight:bold; font-size:18px") }}
</div>
</form>
</div>
</div>
</body>
</html>
8. Performing CRUD Operations on the Database
Now that we have the web form rendered on our HTML file to collect the data and a database to hold/store our data, let's perform some CRUD operations on the database.
CRUD operation means creating, reading, updating and deleting data on the database.
Create Operation
To create an article and save it to the database, we will need a "route" to both access the web form and also perform a "POST" method which creates and saves our collected data into the database.
Import additional required dependencies in the main.py file as seen below:
from flask import Flask, render_template, request, flash, redirect, url_for from models import db from models import Article from webform import ArticleForm
"render_template" helps to render the HTML template on the client side.
"flash" will flash a message on the web page on completing a certain task, if set.
"request" handles or processes all the request data from the client side to the server.
"redirect" help to redirect a current page to another after performing a certain task.
"url_for" is for stating the route to which a particular function or task should go.
Create a new route "/create": This will render the create-article.html file and display the create article form.
Note: we added the flash function that flashes a message on the browser on completion of a task or an error.
# CREATE ARTICLE ROUTE @app.route("/create", methods=["GET", "POST"]) def create(): form = ArticleForm() # if request is validated and submitted to this... if request.method == "POST" and form.validate_on_submit(): title = form.title.data # gets title data from form title field content = form.content.data # gets content data from form content field # creating a new instance of Article and adding it to db new_article = Article(title=title, content=content) db.session.add(new_article) db.session.commit() flash(f"Article titled: '{title}' published successfully") context = { "form": form, } return render_template("create-article.html", **context)
Now, add the "create" route as a link in the index.html file.
<h4><a href="{{url_for('create')}}">Create an Article</a></h4>
Also, add the code to loop through the flash messages and display them.
{% for message in get_flashed_messages() %} <div class="alert alert-warning alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> {% endfor %}
Your index.html file should look like this...
{% extends 'base.html' %} {% block content %} {% for message in get_flashed_messages() %} <div class="alert alert-warning alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> {% endfor %} <center> <h1><b>Blogger's Zone</b></h1> <p>Hi, welcome to my new blog</p> <!-- new link added --> <h4><a href="{{url_for('create')}}">Create an Article</a></h4> </center> {% endblock content %}
Check that your server is still running or type
flask run
in the terminal to start it. Click on the "Home" bar on the Nav.Then click Create an Article link to go to the create article page, we created in the previous tutorial section. Fill in your first article and submit it.
On submission, it redirects you to "index.html" with a flash message.
You can create a few more articles using the Create an Article link.
Read Operation
To READ simply means to get or retrieve data from the server, using the "GET" method mainly.
Reading All Articles
First, let's read all articles and display them on the "index.html". To do this,
Create the below route just below the "/create" route.
# READ ALL ARTICLES ROUTE @app.route("/articles", methods=["GET"]) def articles(): # querying for all articles from the database articles = Article.query.all() print(articles) context = { "articles": articles, } return render_template("articles.html", **context)
Add the below link code to the index.html file, just below the "Create an Article" link.
<h4><a href="{{url_for('articles')}}">Read All Articles</a></h4>
Then create an "articles.html" file with the below code.
{% extends 'base.html' %} {% block content %} {% for message in get_flashed_messages() %} <div class="alert alert-warning alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> {% endfor %} <center> <h1><b>Articles List</b></h1> </center> <div class="shadow p-3 mb-3 bg-body rounded border border-primary" style="height: auto"> {% for article in articles %} <div class="card border-primary mb-3"> <small class="card-header"> <strong class="badge rounded border border-primary" style="color: black;">Date Created:</strong> {{ article.date_posted.strftime('%Y-%m-%d %I:%M %p') }} </small> <div class="card-body"> <h4>{{ article.title }}</h4> <div> <p>{{ article.content }}</p> </div> <br> </div> </div> {% endfor %} </div> {% endblock content %}
As seen above, after querying the database to get all articles, we, then, loop through them using the "for loop" python code, starting with
{% for article in articles %}
and ending with{% endfor %}
in the HTML file. So for each article in the loop, we will return the title{{ article.title }}
, content{{ article.content }}
and date posted{{ article.date_posted }}
of the article.Note: ".strftime('%Y-%m-%d %I:%M %p')" is used to format the date.
On clicking the "Read All Articles" link...
Reading a Single Article
Secondly, to read a single article...
Create a route as seen below:
Note: that we are passing an id argument into the view function to allow us to single out one article to view.
# READ A SINGLE ARTICLE ROUTE @app.route("/view/<id>", methods=["GET"]) def view(id): # querying for an article with specific id from the database article = Article.query.get_or_404(id) context = { "article": article, } return render_template("view-article.html", **context)
Once the route to view a single article is set, we will add the route link as a button to each article from the "for loop" on the Article List page, just below the
<div>
tag of thearticle.content
. Note thatid=article.id
, the id is the view function argument which is set asarticle.id
coming from the "for loop" article.<div> <a href="{{ url_for('view', id=article.id) }}" class="btn btn-light btn-outline-primary">Read Article</a> </div>
Then, go ahead to create the view-article.html file which the route
def view(id)
will render.{% extends 'base.html' %} {% block content %} {% for message in get_flashed_messages() %} <div class="alert alert-warning alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> {% endfor %} <!-- Article info --> <div class="container"> <h2 style="font-weight:bold">{{ article.title }}</h2> <hr> <small> <strong>Date:</strong> {{ article.date_posted.strftime('%Y-%m-%d %I:%M:%S %p') }} </small> </div> <br> <!-- Article Content --> <div class="shadow-lg p-3 mb-3 bg-body rounded border border-primary"> <div class="fs-5"> <p>{{ article.content }}</p> </div> </div> {% endblock content %}
Your Articles List page should look like this...
On clicking the "Read Article" button, it renders the view-article.html using the view route and the id of the article, from the for loop.
Update Operation
The update operation is simply to edit/update existing records in the database. So we are going to write the code for the "update" or "edit" route.
Go to the main.py file and below the "view" route, add the "edit" route.
# UPDATE AN ARTICLE ROUTE @app.route("/edit/<id>", methods=["GET", "POST"]) def edit(id): form = ArticleForm() # querying for an article with specific id from the database article = Article.query.get_or_404(id) # to update and save the edited article to database if form.validate_on_submit(): article.title = form.title.data article.content = form.content.data db.session.commit() flash(f"Article title: {article.title} updated successfully.") return redirect(url_for("view", id=article.id)) # to initially fill up the update form with existing data from database form.title.data = article.title form.content.data = article.content context = { "form": form, "article": article, } return render_template("edit-article.html", **context)
Now, create the "edit-article.html" with the code below, just like the create-article.html. The main difference is the form's action
url_for
.For create-article.html, we have
<form action="{{ url_for('create') }}" method="POST">
, while for edit-article.html, we have<form action="{{ url_for('edit', id=article.id) }}" method="POST">
.{% extends 'base.html' %} {% block content %} {% for message in get_flashed_messages() %} <div class="alert alert-warning alert-dismissible fade show" role="alert"> {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> {% endfor %} <div> <center> <h3><b>Edit Article</b></h3> </center> <br> <div class="container form-group"> <form class="row g-3" action="{{ url_for('edit', id=article.id) }}" method="POST"> <div class="row">{{ form.hidden_tag() }}</div> <div class="row"> {{ form.title.label(class="form-label", style="font-weight:bold; font-size:18px") }} {{ form.title(class="row form-control shadow p-2 mb-5 bg-body rounded border border-primary") }} </div> <div class="row"> {{ form.content.label (class="form-label", style="font-weight:bold; font-size:18px") }} <div class="row form-control shadow p-2 mb-5 bg-body rounded border border-primary"> {{ form.content }} </div> </div> <div class="d-grid gap-2 col-3 mx-auto"> {{ form.submit(class="btn btn btn-outline-primary" , style="font-weight:bold; font-size:18px") }} </div> </form> </div> </div> {% endblock content %}
Also, add the "Edit Article" button to the "articles.html" (just below the "Read Article" button) and "view-article.html" (just below the
<div>
tag forarticle.content
).<a href="{{ url_for('edit', id=article.id) }}" class="btn btn-light btn-outline-primary">Edit Article</a>
Articles List Page:
Single Article View page:
Clicking on the "Edit Article" button takes you to the Edit Article page. Add some extra text and submit.
On submission...
Delete Operations
For the Delete operation, we will...
Create a delete route to delete a specific article from the database. Let's add the delete route as seen below.
# DELETE AN ARTICLE ROUTE @app.route("/delete/<int:id>", methods=["GET", "POST"]) def delete(id): # querying for an article with specific id from the database article = Article.query.get_or_404(id) # deleting article from database db.session.delete(article) db.session.commit() flash(f"Article: '{article.title}' deleted Successfully!") return redirect(url_for("list"))
Note*: we are not rendering any template, only redirecting to the articles list route.*
Now, let's add a "Delete Article" button with the "delete" route to both "articles.html" and "view-article.html" as we did with the "Edit Article" button. See the HTML code for the delete button below.
<a href="{{ url_for('delete', id=article.id) }}" class="btn btn-light btn-outline-danger">Delete Article</a>
On clicking the "Delete Article" button for the article "Another Post", it is deleted.
12. Using Blueprints and Project Restructure
Blueprint is a tool that can be used to organize a group of related views/routes and other code. You can read more about blueprints and views here.
Now, we are going to restructure our project as shown in the project structure diagram below, and the use of blueprints will help make this possible and easy to implement.
Notice that almost all the folders have the __init__.py file. This enables us to use the folders as a package folder.
Now, create the article folder and create __init__.py file, article.py file and a templates folder inside it. In the article.py, import the following dependencies below. Also, import db from main.py.
# in article.py file
from flask import flash, render_template, redirect, request, url_for
from app.models import db, Article
from app.webform import ArticleForm
To start using the Blueprint, we will...
Import Blueprint from flask and instantiate the Blueprint class, with the blueprint name "article", and a template_folder "templates"
# in article.py file from flask import Blueprint blueprint = Blueprint("article", __name__, template_folder="templates")
Move all the routes we have created so far for articles (create, read, update and delete) during the CRUD operations to the articles.py file. Then, instead of using
@app.route()
, we will use@blueprint.route()
Import article.py from app/article into the app/__init__.py. So we can import directly from app,
from app import article
, instead offrom app.article import article
# __init__.py file from .config import sett from .article import article
Import article from app into the main.py file and register the Blueprint instance "article.blueprint" with a
url_prefix
"/article". This allows the application to recognize the blueprint routes.app.register_blueprint(article.blueprint, url_prefix="/article")
The
url_prefix="/article"
makes each routeing address starts with /article, that is, "127.0.0.1:5000/article". For example, the create route address will become 127.0.0.1:5000/article/create.Also on the templates and within routes where you use
url_for
, you must ptefix the name defined in the blueprint instance, in this case, "article".For example,
url_for('view', id=article.id)
becomesurl_for('article.view', id=article.id)
.The url_for the button links in articles.html should be rewritten like this...
<div> <a href="{{ url_for('article.view', id=article.id) }}" class="btn btn-light btn-outline-primary">Read Article</a> <a href="{{ url_for('article.edit', id=article.id) }}" class="btn btn-light btn-outline-primary">Edit Article</a> <a href="{{ url_for('article.delete', id=article.id) }}" class="btn btn-light btn-outline-danger">Delete Article</a> </div>
...and in article.py,
return redirect(url_for(...))
should look like this...# UPDATE AN ARTICLE ROUTE @blueprint.route("/edit/<id>", methods=["GET", "POST"]) def edit(id): ... return redirect(url_for("article.view", id=article.id)) @blueprint.route("/delete/<int:id>", methods=["GET", "POST"]) def delete(id): ... return redirect(url_for("article.list"))
Now to start the server, use this
python app\main.py
instead ofpython main.py
.
10. Authentication and Authorization
In this section of this tutorial, we will need to create (sign-up) users.
User Authentication
This deals with validating that the right user has the right access to the right data.
Install flask-login via terminal using
pip3 install flask-login
. Also, dopip freeze > requirements.txt
to update the requirements.txt file.Create an auth folder inside the app folder. Then inside the auth folder, create __init__.py, auth.py and templates folder.
Create the User model in models.py.
# in models.py file from flask_login import UserMixin # additional import from flask_sqlalchemy import SQLAlchemy from datetime import datetime # USER MODEL/TABLE class User(db.Model, UserMixin): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(25), nullable=False, unique=True) email = db.Column(db.Text(150), nullable=False, unique=True) password_hash = db.Column(db.Text(255), nullable=False, unique=True) bio = db.Column(db.Text(), nullable=True) date_joined = db.Column(db.DateTime, default=datetime.utcnow) def __repr__(self): return f"User: <{self.username}>"
Create the UserForm and LoginForm in webforms.py.
# in webforms.py file # there are additional imports from wtforms import StringField, TextAreaField, SubmitField, EmailField, PasswordField from wtforms.validators import InputRequired, Length, EqualTo from wtforms.widgets import TextArea # USER FORM class UserForm(FlaskForm): username = StringField( label="Username", validators=[InputRequired(message="*Required"), Length(5, 25, message="Password must be between 5 and 15 characters")] ) email = EmailField( label="Email", validators=[InputRequired(message="*Required"), Length(5, 120, message="Password must be between 5 and 120 characters")] ) password = PasswordField( label="Password", validators=[InputRequired(message="*Required"), Length(min=8, message="Password must be more than 8 characters")] ) confirm_password = PasswordField( label="Confirm Password", validators=[InputRequired(message="*Required"), EqualTo("password", message="Passwords do not match!")], ) bio = TextAreaField(label="Bio", widget=TextArea()) submit = SubmitField(label="Submit")
Set-up Login Manager - Now, this will be done in the main.py file. You can read more about it here.
# In main.py file from flask_login import LoginManager # Login Manager Setup login_man = LoginManager(app) login_man.login_view = "login" @login_man.user_loader def user_loader(id): return User.query.get(int(id))
Create Sign-up Route - In the auth.py file, we import the needed dependencies and create the route as seen below.
```python from flask import render_template, request, redirect, url_for, flash, Blueprint from flask_login import login_user, login_required, logout_user, current_user from werkzeug.security import generate_password_hash, check_password_hash from app.models import db, User from app.webforms import UserForm, LoginForm from datetime import timedelta
blueprint = Blueprint("auth", name, template_folder="templates")
"""SIGN UP ROUTE""" @blueprint.route("/sign-up", methods=["GET", "POST"]) def sign_up(): form = UserForm()
if request.method == "POST" and form.validate_on_submit(): username = form.username.data email = form.email.data password = form.password.data confirm_password = form.confirm_password.data bio = form.bio.data
"""checking if username and email already exists""" user_exists = User.query.filter_by(username=username).first() email_exists = User.query.filter_by(email=email).first()
if user_exists: flash(f"Whoops!!! Username '{username}' already exist! Please try again...") return redirect(url_for("auth.sign_up")) elif email_exists: flash(f"Whoops!!! Email '{email}' already exist! Please try again...") return redirect(url_for("auth.sign_up"))
"""confirming passwords match""" elif password != confirm_password: flash(f"Whoops!!! Passwords do not match! Please try again...") else: password_hash = generate_password_hash(password) # hashing the password
"""creating an instance of user""" user = User(username=username, email=email, password_hash=password_hash, bio=bio)
db.session.add(user) # adding to the db db.session.commit()
flash(f"Account signed up successfully!") return redirect(url_for("auth.login"))
context = { "form": form, }
return render_template("sign-up.html", **context)
* **Create Login Route**
```python
# LOGIN ROUTE in the auth.py file
@blueprint.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm()
if request.method == "POST" and form.validate_on_submit:
username_email = form.username_email.data
password = form.password.data
# checking if username or email exist
user_exists = User.query.filter(or_(User.username == username_email, User.email == username_email)).first()
if user_exists:
# checking hashed password against password entered
if check_password_hash(user_exists.password_hash, password):
# logs user data in if password is confirmed correct
login_user(user_exists)
flash("Login Successful")
return redirect(url_for("general.index"))
else:
flash("Wrong Password! Please try again...")
return redirect(url_for("auth.login"))
else:
flash("User does not exist! Please try again...or Sign up.")
return redirect(url_for("auth.login"))
context = {
"form": form
}
return render_template("login.html", **context)
Create Logout Route
# LOGOUT ROUTE in the auth.py file @blueprint.route("/logout", methods=["GET", "POST"]) @login_required def logout(): logout_user() flash("You have been logged out!") return redirect(url_for("auth.login"))
User Authorization
This deals with giving users the right to perform only certain allowed operations. This is quite simple to implement.
Implementation will be at 2 points,
Point One, adding
@login_required
to the routes within the python code, as seen in the Logout Route above.Point Two, inserting the if statement
{{ if current_user.is_authenticated }}
to our HTML codes to restrict certain users.
11. Conclusion
I know it's been a long read, but I'm sure that you have learned something new and good. Now, you should be able to build a simple web application to start with, and then advance in it to more complex ones, applying various concepts introduced in this article.
Remember with Flask you can do more because of its flexibility. Thanks for taking the time to read this article. Please feel free to contact me if you need any guidance or help or collaboration with any project. I will be glad to help or join you. Thanks once again.