Creating a Python web application with Flask
With this tutorial, you will develop a simple web application from scratch: a mini blog where users can create, view, edit, and delete posts. To do this, you will create a Yandex Compute Cloud VM in Yandex Cloud and run a web server on it.
The web app is written in Python
We opted for Flask
The Flask framework allows you to create web applications from a single source code file; it does not require following any specific directory structure or writing complex template code before you start using it. Flask also has out-of-the-box support for the Werkzeug
The source code for the application is available in the flask_blog.zip
Application workflow:
- HTTP requests are sent from the user's browser to a Linux Ubuntu VM which has a Flask web server installed and running.
- The web server forwards a request to the web application, whose router invokes the appropriate handler function based on the request URL.
- The handler function executes a query to the SQLite database to retrieve or write the required data.
- The function sends the data retrieved from the database to the appropriate Jinja HTML template, which returns the final HTML code of the page.
- This HTML code is forwarded to the web server, which in turn serves this code to the user's browser.
To create a web application with Flask:
- Prepare your cloud.
- Create and set up a virtual machine.
- Create and run the application.
- Create and configure the HTML templates.
- Configure the database.
- Display blog posts.
- Add actions with posts.
- Summarize the results.
If you no longer need the web application, delete the resources it uses.
Getting started
In Yandex Cloud, you only pay for the resources you consume and the time of their actual use. A billing account is required to identify the user who pays for such resources.
The cost of support for a web application includes:
- Fee for using a public IP address (see Yandex Virtual Private Cloud pricing).
- Fee for VM computing resources and disks (see Yandex Compute Cloud pricing).
Sign up for Yandex Cloud and create a billing account:
- Go to the management console
and log in to Yandex Cloud or create an account if you do not have one yet. - On the Yandex Cloud Billing
page, make sure you have a billing account linked and it has theACTIVE
orTRIAL_ACTIVE
status. If you do not have a billing account, create one.
If you have an active billing account, you can go to the cloud page
Learn more about clouds and folders.
Create a cloud network and subnet
In Yandex Cloud, resources are connected to each other and the internet using cloud networks, in which resources receive public IP addresses and private IP address ranges, or subnets.
To create a virtual network and subnet for your web server:
- In the management console
, select your folder. - In the list of services, select Virtual Private Cloud.
- At the top right, click Create network.
- In the Name field, specify
webserver-network
. - In the Advanced field, disable the Create subnets option.
- Click Create network.
- In the left-hand panel, select
Subnets. - At the top right, click Create.
- In the Name field, specify
webserver-subnet-ru-central1-b
. - In the Zone field, select the
ru-central1-b
availability zone. - In the Network field, select the
webserver-network
cloud network. - In the CIDR field, specify
192.168.1.0/24
. - Click Create subnet.
-
Create a network named
webserver-network
:yc vpc network create webserver-network
Result:
id: enp1gg8kr3pv******** folder_id: b1gt6g8ht345******** created_at: "2023-12-20T20:08:11Z" name: webserver-network default_security_group_id: enppne4l2eg5********
For more information about the
yc vpc network create
command, see the CLI reference. -
Create a subnet in the
ru-central1-b
availability zone:yc vpc subnet create webserver-subnet-ru-central1-b \ --zone ru-central1-b \ --network-name webserver-network \ --range 192.168.1.0/24
Result:
id: e2li9tcgi7ii******** folder_id: b1gt6g8ht345******** created_at: "2023-12-20T20:11:16Z" name: webserver-subnet-ru-central1-b network_id: enp1gg8kr3pv******** zone_id: ru-central1-b v4_cidr_blocks: - 192.168.1.0/24
For more information about the
yc vpc subnet create
command, see the CLI reference.
-
To create a network, use the create REST API method for the Network resource or the NetworkService/Create gRPC API call.
-
To create a subnet, use the create REST API method for the Subnet resource or the SubnetService/Create gRPC API call.
Create a security group
Security groups are the main tool for managing network access in Yandex Cloud and help restrict unauthorized traffic to cloud resources at the cloud network level.
Create a security group that allows inbound TCP traffic on ports 5000
and 22
as well as any outbound traffic. Port 22
is used to connect to the VM via SSH, while 5000
is the default port to run a Flask web server.
-
In the management console
, select your folder. -
In the list of services, select Virtual Private Cloud.
-
In the left-hand panel, select
Security groups. -
Click Create security group.
-
In the Name field, enter the name:
webserver-sg
. -
In the Network field, select the
webserver-network
network you created earlier. -
Under Rules, create the following traffic management rules:
Traffic
directionDescription Port range Protocol Source /
Destination nameCIDR blocks Incoming Flask
5000
TCP
CIDR
0.0.0.0/0
Incoming ssh
22
TCP
CIDR
0.0.0.0/0
Outgoing any
All
Any
CIDR
0.0.0.0/0
-
Click Save.
Run this command:
yc vpc security-group create \
--name webserver-sg \
--rule "description=Flask,direction=ingress,port=5000,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
--rule "description=ssh,direction=ingress,port=22,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
--rule "description=any,direction=egress,port=any,protocol=any,v4-cidrs=[0.0.0.0/0]" \
--network-name webserver-network
Result:
id: enpv1c0q7j01********
folder_id: b1gt6g8ht345********
created_at: "2024-03-23T18:54:05Z"
name: webserver-sg
network_id: enp9mji1m7b3********
status: ACTIVE
rules:
- id: enpmbsk2hjfd********
description: Flask
direction: INGRESS
ports:
from_port: "5000"
to_port: "5000"
protocol_name: TCP
protocol_number: "6"
cidr_blocks:
v4_cidr_blocks:
- 0.0.0.0/0
- id: enpna5id9265********
description: ssh
direction: INGRESS
ports:
from_port: "22"
to_port: "22"
protocol_name: TCP
protocol_number: "6"
cidr_blocks:
v4_cidr_blocks:
- 0.0.0.0/0
- id: enpen3vf7rui********
description: any
direction: EGRESS
protocol_name: ANY
protocol_number: "-1"
cidr_blocks:
v4_cidr_blocks:
- 0.0.0.0/0
Save the security group ID (id
) as you will need need it to create a VM.
For more information about the yc vpc security-group create
command, see the CLI reference.
To create a security group, use the create REST API method for the SecurityGroup resource or the SecurityGroupService/Create gRPC API call.
Create and set up a virtual machine
A virtual machine is similar to a server in the cloud infrastructure. In Yandex Cloud, you can create VMs with different hardware characteristics in terms of performance, RAM, and disk space, as well as running different operating systems.
This web application will be deployed on an Ubuntu 22.04 LTS Linux VM.
-
Create a virtual machine:
Before you start, prepare a key pair (public and private keys) to access your VM over SSH.
Management consoleCLIAPI-
In the management console
, select the folder to create your VM in. -
In the list of services, select Compute Cloud.
-
In the left-hand panel, select
Virtual machines. -
Click Create virtual machine.
-
Under Boot disk image, select the Ubuntu 22.04 LTS image.
-
Under Location, select the
ru-central1-b
availability zone. -
Under Network settings:
- In the Subnet field, select the
webserver-subnet-ru-central1-b
subnet you created earlier. - In the Public IP field, select
Auto
. - In the Security groups field, select the
webserver-sg
security group you created earlier.
- In the Subnet field, select the
-
Under Access, select SSH key and specify the information required to access the VM:
- Enter the username in the Login field:
yc-user
. -
In the SSH key field, select the SSH key saved in your organization user profile.
If there are no saved SSH keys in your profile, or you want to add a new key:
- Click Add key.
- Enter a name for the SSH key.
- Upload or paste the contents of the public key file. You need to create a key pair for the SSH connection to a VM yourself.
- Click Add.
The SSH key will be added to your organization user profile.
If users cannot add SSH keys to their profiles in the organization, the added public SSH key will only be saved to the user profile of the VM being created.
- Enter the username in the Login field:
-
Under General information, specify the VM name:
sftp-server
. -
Click Create VM.
Run the command specifying the security group ID you saved at the previous step:
yc compute instance create \ --name mywebserver \ --zone ru-central1-b \ --network-interface subnet-name=webserver-subnet-ru-central1-b,nat-ip-version=ipv4,security-group-ids=<security_group_ID> \ --create-boot-disk image-folder-id=standard-images,image-id=fd8ne6e3etbrr2ve9nlc \ --ssh-key <public_SSH_key_file>
Where
--ssh-key
is the path to the file with the public SSH key, e.g.,~/.ssh/id_ed25519.pub
.Result:
done (32s) id: epdv79cu67np******** folder_id: b1gt6g8ht345******** created_at: "2024-03-23T19:17:09Z" name: mywebserver zone_id: ru-central1-b platform_id: standard-v2 resources: memory: "2147483648" cores: "2" core_fraction: "100" status: RUNNING metadata_options: gce_http_endpoint: ENABLED aws_v1_http_endpoint: ENABLED gce_http_token: ENABLED aws_v1_http_token: DISABLED boot_disk: mode: READ_WRITE device_name: epdg0926k12t******** auto_delete: true disk_id: epdg0926k12t******** network_interfaces: - index: "0" mac_address: d0:0d:1f:3a:59:e3 subnet_id: e2l3qffk0h6t******** primary_v4_address: address: 192.168.1.14 one_to_one_nat: address: 62.84.***.*** ip_version: IPV4 security_group_ids: - enpv1c0q7j01******** serial_port_settings: ssh_authorization: INSTANCE_METADATA gpu_settings: {} fqdn: epdv79cu67np********.auto.internal scheduling_policy: {} network_settings: type: STANDARD placement_policy: {}
For more information about the
yc compute instance create
command, see the CLI reference.To create a VM, use the create REST API method for the Instance resource or the InstanceService/Create gRPC API call.
This will create the
mywebserver
VM in your folder. To connect to the VM over SSH, use theyc-user
username and the VM’s public IP address. If you plan to use the created web server over a long period of time, make this VM's public IP address static. -
-
Create and activate the virtual environment.
A virtual environment provides an isolated space for Python projects on the server. All required dependencies, e.g., executables, libraries, and other files, are copied to a selected folder, and the application uses them instead of those installed on the system. This ensures the stability of the development environment and keeps the base system clean.
To create a virtual environment, use the Python 3
venv
library module:-
Connect to the
mywebserver
VM. -
In the current user's directory, create a project subdirectory named
flask_blog
where the application will reside and navigate into it:mkdir flask_blog && cd flask_blog
-
Install the
venv
virtualization module:sudo apt install python3.10-venv
-
Create the
env
virtual environment:python3 -m venv env
-
Activate the virtual environment:
source env/bin/activate
After you activate the virtual environment, a prefix with the name of the environment will appear in the command line:
(env) yc-user@ubuntu:~/flask_blog$
Note
To effectively track and manage the project development process, you can use a version control system. In this case, add the
env
directory to the.gitignore
file to only track files related to the project.To deactivate the virtual environment, run this command:
deactivate
-
-
Install Flask:
pip install flask
Result:
Successfully installed Jinja2-3.1.3 MarkupSafe-2.1.5 Werkzeug-3.0.1 blinker-1.7.0 click-8.1.7 flask-3.0.2 itsdangerous-2.1.2
Create and run the application
Create a simple web application inside a Python file and run it to start the server.
-
In the
flask_blog
project directory, create and open theapp.py
file:nano app.py
This file allows you to understand how the application will handle HTTP requests.
-
Add the following code to the file:
from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello, World!'
In this code:
- A
Flask
object is imported from theflask
package. - An instance of the Flask application named
app
is created. The special__name__
variable contains the name of the current Python module and indicates its location to the instance. This is necessary because Flask creates several routes within the application. - The
@app.route('/')
decorator transforms the Python function into a Flask view function. The view function converts the return value into an HTTP response that can be processed by an HTTP client, such as a web browser. The'/'
value in@app.route()
indicates that this function will respond to web requests for the/
URL, which is the primary URL of the web application. - The
hello()
function is created that returns theHello, World!
string as a response.
Save and close the
app.py
file. - A
-
Set the
Flask
environment variables:export FLASK_APP=app && export FLASK_DEBUG=true
Where:
FLASK_APP=app
indicates the location of the app, i.e., theapp.py
file.FLASK_DEBUG=true
indicates the application should run in the development mode.
-
Run the application:
flask run \ --host=0.0.0.0
Where the
--host=0.0.0.0
parameter starts the application server on all IP addresses of the VM. If you do not specify this parameter, you will not be able to access the application using the VM's public IP address.Result:
* Serving Flask app 'app' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://192.168.1.14:5000 Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 884-371-***
You will need the following information:
- Name of the running application:
app
. Debug mode: on
: Indicates that the Flask debugger is active. This feature is useful in development because it provides detailed error messages when problems occur, making it easier to resolve them.- The application is running on all addresses.
You can change the web server port to any other value by setting this optional parameter:
--port=<port_number>
. For example, theflask run --host=0.0.0.0 --port=5001
command will start the Flask web server on port5001
. - Name of the running application:
-
In you browser address bar, enter
http://<public_IP_address_of_VM>:5000/
.The browser will display the
Hello, World!
string. This means that your application is running properly and can be accessed from the internet.
Warning
Flask uses a simple web server to serve the application in the development environment. It is reserved for testing and debugging and should not be used in the production environment. For more information, see the Flask documentation
Keep the web server running in the current window and open a new terminal window. You will use this new window to finish setting up your application. This way, you can test your changes without having to stop and restart the web server.
When you open a new terminal window:
-
Connect to the VM.
-
Go to the
flask_blog
project directory, activate the virtual environment, and set environment variables:cd flask_blog && source env/bin/activate && export FLASK_APP=app && export FLASK_DEBUG=true
Create and configure the HTML templates
At this point, the application can only display a plain text message in the browser. To display the data and controls that the user needs, you need to add HTML templates to your application.
To use templates in the app, you can use the render_template()
helper function
Create the main page HTML template
-
Open the
app.py
file:nano app.py
-
Replace the file contents with this code:
from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): return render_template('index.html')
In this code:
- The
render_template()
helper function is imported to render HTML templates. - The
hello()
function is replaced with theindex()
function that returns the result of invoking therender_template()
helper function with theindex.html
argument. The function argument points to a template file in the template directory to use for rendering.
Note
You will create the template directory and the
index.html
file later. Running the application at this stage will return an error.Save and close the
app.py
file. - The
-
In the
flask_blog
project directory, create a subdirectory namedtemplates
:mkdir templates
-
Create and open the
index.html
template file in thetemplates
directory:nano templates/index.html
-
Add the following code to the
index.html
file:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>FlaskBlog</title> </head> <body> <h1>Welcome to FlaskBlog</h1> </body> </html>
This HTML code describes a simple page with the
FlaskBlog
header andWelcome to FlaskBlog
as the first level header.Save and close the
index.html
file. -
Refresh the main page of your application,
http://<public_IP_address_of_VM>:5000/
.Result:
Welcome to FlaskBlog.
-
In addition to the
templates
directory, Flask web applications typically have thestatic
directory that stores static files, such as CSS styles, JavaScript scripts, or images used by the application.In the
flask_blog
project directory, create a subdirectory namedstatic
:mkdir static
-
In the
static
directory you just created, create thecss
subdirectory:mkdir static/css
Note
Subdirectories are used to organize static files in dedicated folders. For example, the
js
directory usually stores JavaScript files, theimages
(orimg
) directory stores images, etc. -
In the
static/css
project directory, create and open thestyle.css
file:nano static/css/style.css
-
Add the following code to the
style.css
file:h1 { border: 2px #eee solid; color: #fc3d17; text-align: center; padding: 10px; }
This CSS code changes the formatting of the text wrapped by the
<h1>
HTML tags:border
adds a border.color
changes the text color.text-align
sets text alignment.padding
adds a padding between the element's border and text.
Save and close the
style.css
file. -
Open the
index.html
template:nano templates/index.html
-
Add a link to
style.css
inside the<head>
section:... <head> <meta charset="UTF-8"> <link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}"> <title>FlaskBlog</title> </head> ...
The link uses the
url_for()
helper function to generate the URL for the file location. The first argument indicates that the link references a static file; the second argument specifies the path to the file in the static file subdirectory.Save and close the
index.html
file. -
Refresh the main page of your application,
http://<public_IP_address_of_VM>:5000/
.You will see that the
Welcome to FlaskBlog
text changed its color to red and is now centered inside the frame.
The CSS language
Configure other HTML templates
In other HTML templates, you will need to reuse most of the HTML code from the index.html
template. To avoid unnecessary duplicate code, use a base template file as a parent for all other HTML templates in the project. See more in the Jinja documentation
-
Create and open the
base.html
base template in thetemplates
directory:nano templates/base.html
-
Add the following code to the file:
<!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> <title>{% block title %} {% endblock %}</title> </head> <body> <nav class="navbar navbar-expand-lg bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <a class="nav-link active" href="#">About</a> </li> </ul> </div> </div> </nav> <div class="container"> {% block content %} {% endblock %} </div> <!-- Optional JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> </body> </html>
This file contains HTML code as well as additional code required by Bootstrap. The
<meta>
tags contain browser information, the<link>
tag binds Bootstrap CSS files, and the<script>
tag refererces a JavaScript script that enables additional Bootstrap features. For more information, see the Bootstrap documentation .The following code elements are specific to the Jinja template engine:
{% block title %} {% endblock %}
: Placeholder block for a header. You will use it in other templates to give a custom header for each page in your application without rewriting the<head>
section.{{ url_for('index')}}
: Calls a function that returns the URL for theindex()
view function. This call is different fromurl_for()
used previously to bind the CSS file. In this case, you need to provide only one argument, the name of the view function linked to its associated route and not a file.{% block content %} {% endblock %}
: Block that will contain content specific to each child template.
Save and close the
base.html
file. -
Configure your HTML templates to inherit code from the base template. Open the
index.html
template:nano templates/index.html
-
Replace the file contents with this code:
{% extends 'base.html' %} {% block title %} Welcome to FlaskBlog {% endblock %} {% block content %} <h1> Welcome to FlaskBlog </h1> {% endblock %}
In this code:
{% extends 'base.html' %}
: Indicates inheritance from thebase.html
template.{% block title %} ... {% endblock %}
: Specifies a header that will replace the variable in the corresponding block of thebase.html
parent template.{% block content %} ... {% endblock %}
: Specifies the content that will replace the variable in the corresponding block of thebase.html
parent template.
Since
Welcome to FlaskBlog
is used as both the page name and the header that appears below the navigation bar, you can make this code even more concise and avoid repeating the same text twice:{% extends 'base.html' %} {% block content %} <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1> {% endblock %}
Save and close the
index.html
file. -
Refresh the main page of your application,
http://<public_IP_address_of_VM>:5000/
.Your page will have a navigation bar and a styled header.
Configure the database
Create a database to store blog posts and populate it with sample entries.
You will use SQLite
Prepare the schema.sql
file containing SQL commands to create a database consisting of a table named posts
with multiple columns.
-
In the
flask_blog
project directory, create and open theschema.sql
file:nano schema.sql
-
Add the following code to the file:
DROP TABLE IF EXISTS posts; CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, content TEXT NOT NULL );
In this code:
- The
DROP TABLE IF EXISTS posts;
command deletes theposts
table if it already exists. We recommend performing this step each time you create a new table to prevent possible errors. - The
CREATE TABLE posts
command creates theposts
table with the following columns:id
: Integer that is thePRIMARY KEY
of the table.AUTOINCREMENT
means that its value will be a unique number generated automatically when inserting a new record (blog post) into the table.created
: New record timestamp.NOT NULL
indicates that the value in the column cannot be empty.DEFAULT CURRENT_TIMESTAMP
sets the default value as the time when a new record is added to the table. This means that you do not need to manually specify the time when a post is created, it will be added automatically.title
: Post title.content
: Main text of the post.
Save and close the
schema.sql
file.Note
When using these SQL commands, any previous contents of the
posts
table in thedatabase.db
file will be deleted. Do not save anything important to the database via the web application until you have completed this tutorial. - The
-
In the
flask_blog
project directory, create and open theinit_db.py
file:nano init_db.py
This file will create the SQLite database file for your web application.
-
Add the following code to the file:
import sqlite3 connection = sqlite3.connect('database.db') with open('schema.sql') as f: connection.executescript(f.read()) cur = connection.cursor() cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)", ('First Post', 'Content for the first post') ) cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)", ('Second Post', 'Content for the second post') ) connection.commit() connection.close()
In this code:
- The
sqlite3
module is imported. - The connection to the
database.db
file is opened; this file is created once you run the Python file. - The
open()
function opens the previously createdschema.sql
SQL schema. - The
executescript()
method formalizes the contents of the schema by running multiple SQL statements at once and creating theposts
table. - The
cursor()
method opens the cursor for sending queries to the database. - The
execute()
method executes twoINSERT
SQL statements, adding two sample posts to theposts
table. - The changes are committed and the connection is closed.
Save and close the
init_db.py
file. - The
-
Run the file using
python
:python init_db.py
-
Make sure the
database.db
file is in theflask_blog
project directory:ls -l
Result:
total 40 -rw-rw-r-- 1 yc-user yc-user 105 Mar 23 19:45 app.py -rw-r--r-- 1 yc-user yc-user 12288 Mar 24 12:27 database.db drwxrwxr-x 5 yc-user yc-user 4096 Mar 23 19:30 env -rw-rw-r-- 1 yc-user yc-user 472 Mar 24 12:27 init_db.py drwxrwxr-x 2 yc-user yc-user 4096 Mar 23 19:45 __pycache__ -rw-rw-r-- 1 yc-user yc-user 204 Mar 24 12:26 schema.sql drwxrwxr-x 2 yc-user yc-user 4096 Mar 24 12:29 static drwxrwxr-x 2 yc-user yc-user 4096 Mar 24 12:30 templates
Display blog posts
On the main page of your application, customize the display of the list of all blog posts and create a page to view individual posts by their ID.
Display the list of all blog posts
Update the index()
view function and the index.html
template to display a list of all posts stored in the database.
-
Open the
app.py
file:nano app.py
-
Replace the file contents with this code:
import sqlite3 from flask import Flask, render_template def get_db_connection(): conn = sqlite3.connect('database.db') conn.row_factory = sqlite3.Row return conn app = Flask(__name__) @app.route('/') def index(): conn = get_db_connection() posts = conn.execute('SELECT * FROM posts').fetchall() conn.close() return render_template('index.html', posts=posts)
This code does the following as well:
- Imports the
sqlite3
module. - Creates the
get_db_connection()
function which:- Creates the
conn
object for connecting to thedatabase.db
database. - Assigns database data to the
row_factory
attribute of theconn
object. - Returns the
conn
object which will be used to access the database.
- Creates the
- The updated
index()
function:- Assigns the database connection object returned by
get_db_connection()
toconn
. - Uses the
fetchall()
method to assign theposts
object the result of the SQL query for selecting all the records from theposts
table. - Uses the
close()
method to close the connection to the database. - Returns the HTML code of the application main page, formed from the
index.html
template, to which theposts
object containing all records of the table with posts is provided as an argument.
- Assigns the database connection object returned by
Save and close the
app.py
file. - Imports the
-
Open the
index.html
template of the main page:nano templates/index.html
-
Replace the file contents with this code:
{% extends 'base.html' %} {% block content %} <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1> {% for post in posts %} <a href="#"> <h2>{{ post['title'] }}</h2> </a> <span class="badge text-bg-primary">{{ post['created'] }}</span> <hr> {% endfor %} {% endblock %}
This code adds:
{% for post in posts %} ... {% endfor %}
: Jinja loop which displays, in order, each element contained in theposts
object provided by theindex()
function inapp.py
. Inside this loop, for each post, the post title with a link is displayed in the<h2>
HTML header. Later on, these links will be used to open a view page for each post.{{ post['title'] }}
: Post title from thepost
variable, i.e. the element of theposts
object processed in the current iteration of thefor
loop.- Similarly, the
post['created']
post creation date is displayed with thebadge
CSS class applied to it.
Save and close the
index.html
file. -
Refresh the main page of your application,
http://<public_IP_address_of_VM>:5000/
.You will see a list of the two post titles you added to the database when creating it.
Display individual blog posts
Create a new Flask route with a view function that will process a new HTML template to display an individual blog post by its ID. The application will use the new route to display the post at http://<public_IP_address_of_VM>:5000/ID
with the appropriate ID
, if such an ID exists in the database.
-
Open the
app.py
file:nano app.py
-
Replace the file contents with this code:
import sqlite3 from flask import Flask, render_template from werkzeug.exceptions import abort def get_db_connection(): conn = sqlite3.connect('database.db') conn.row_factory = sqlite3.Row return conn def get_post(post_id): conn = get_db_connection() post = conn.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone() conn.close() if post is None: abort(404) return post app = Flask(__name__) @app.route('/') def index(): conn = get_db_connection() posts = conn.execute('SELECT * FROM posts').fetchall() conn.close() return render_template('index.html', posts=posts) @app.route('/<int:post_id>') def post(post_id): post = get_post(post_id) return render_template('post.html', post=post)
This code does the following as well:
- Imports the abort()
function from the Werkzeug library. It allows you to generate a Flask response with the404 Not Found
message if the blog post with the ID specified in the URL does not exist. - Creates the
get_post()
function that takes thepost_id
argument with the blog post ID to retrieve. This function:- Assigns the database connection object returned by
get_db_connection()
toconn
. - Uses the
fetchone()
method to assign thepost
object the result of the SQL query which retrieves the string with the blog post and its ID mapping to thepost_id
value from the database. - Uses the
close()
method to close the connection to the database. - Uses the
abort()
function to respond with the 404 error code if the post with the ID provided topost_id
is not found, and then terminates. - If the post with the ID provided to
post_id
is found, the function returns thepost
object containing the database table row for this post.
- Assigns the database connection object returned by
- Creates the
post(post_id)
view function. This function:- Uses the decorator to add the
<int:post_id>
variable rule specifying that the URL after/
is a positive integer (marked by theint
converter) used by the view function. - With the
get_post(post_id)
function, assigns thepost
object the contents of the database table row mapped to the blog post with the specified ID. - Returns the HTML code of the post view page formed from the
post.html
template, to which thepost
object containing the post contents is provided as an argument.
- Uses the decorator to add the
Save and close the
app.py
file. - Imports the abort()
-
Create and open the
post.html
template file in thetemplates
directory:nano templates/post.html
-
Add the following code to the file:
{% extends 'base.html' %} {% block content %} <h2>{% block title %} {{ post['title'] }} {% endblock %}</h2> <p>{{ post['content'] }}</p> <span class="badge text-bg-primary">{{ post['created'] }}</span> {% endblock %}
This code is similar to that of the
index.html
template. The difference is that it does not use thefor
loop here since the template should display a single post with its contents specified inpost['content']
.Save and close the
post.html
file. -
To view the first two posts you created together with the database, open these URLs:
http://<public_IP_address_of_VM>:5000/1
andhttp://<public_IP_address_of_VM>:5000/2
.If you try to open
http://<public_IP_address_of_VM>:5000/3
, you will see an error page stating that the requested blog post is not found. -
Add links from the post titles that open the post view pages to the main page of your application.
Open the
index.html
template:nano templates/index.html
-
Replace the
#
value of thehref
attribute with{{ url_for('post', post_id=post['id']) }}
so that thefor
loop is as follows:... {% for post in posts %} <a href="{{ url_for('post', post_id=post['id']) }}"> <h2>{{ post['title'] }}</h2> </a> <span class="badge text-bg-primary">{{ post['created'] }}</span> <hr> {% endfor %} ...
The
url_for()
function takes the following arguments:'post'
:post()
view function.post_id
: Variable with thepost['id']
value used by the view function.
The
post['id']
function returns a valid URL for each post title based on the post ID.Save and close the
index.html
file. -
Go to the main page of your application,
http://<public_IP_address_of_VM>:5000/
, and make sure that the post titles work as links that open the posts.
Add actions with posts
Enable users to create, edit, and delete posts in your application.
Creating a new post
Create a page where the user can add a new post with a title.
-
Open the
app.py
file:nano app.py
-
Replace the file contents with this code:
import sqlite3 from flask import Flask, render_template, request, url_for, flash, redirect from werkzeug.exceptions import abort def get_db_connection(): conn = sqlite3.connect('database.db') conn.row_factory = sqlite3.Row return conn def get_post(post_id): conn = get_db_connection() post = conn.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone() conn.close() if post is None: abort(404) return post app = Flask(__name__) app.config['SECRET_KEY'] = 'your secret key' @app.route('/') def index(): conn = get_db_connection() posts = conn.execute('SELECT * FROM posts').fetchall() conn.close() return render_template('index.html', posts=posts) @app.route('/<int:post_id>') def post(post_id): post = get_post(post_id) return render_template('post.html', post=post) @app.route('/create', methods=('GET', 'POST')) def create(): return render_template('create.html')
This code does the following as well:
- Imports:
request
global object to access the data provided through the HTML form.url_for()
function to generate URLs.flash()
function to display a message when processing a request.redirect()
function to redirect the client to a different page.
- Through the
app.config
object, adds theSECRET_KEY
configuration required for theflash()
function to store pop-up messages in the client's browser session. The secret key is a long string of random characters. It is used to create secure sessions and allows Flask to remember information between requests, for example, go from the post creation page to the main application page. A user can access the information stored in a session, but cannot edit it without the secret key. This is why you should never give anyone access to your secret key. For more information, see the Flask documentation . - Uses the
create()
view function to return the HTML code of the post creation page generated from thecreate.html
template. The decorator creates the/create
route that accepts GET and POST requests. By default, only GET requests are accepted. To ensure that the route also accepts POST requests, which are used by the browser when sending form data, you need to provide a tuple of valid request types in themethods
argument.
Save and close the
app.py
file. - Imports:
-
Create and open the
create.html
template file in thetemplates
directory:nano templates/create.html
-
Add the following code to the file:
{% extends 'base.html' %} {% block content %} <h1>{% block title %} Create a New Post {% endblock %}</h1> <form method="post"> <div class="mb-3"> <label for="title" class="col-sm-2 col-form-label">Title</label> <input type="text" name="title" placeholder="Post title" class="form-control" value="{{ request.form['title'] }}"> </div> <div class="mb-3"> <label for="content" class="col-sm-2 col-form-label">Content</label> <textarea name="content" placeholder="Post content" class="form-control" rows="3">{{ request.form['content'] }}</textarea> </div> <div class="mb-3"> <button type="submit" class="btn btn-primary">Submit</button> </div> </form> {% endblock %}
Basically, this is plain HTML code that will display fields for the title and body of the future post, as well as a button for submitting the form.
The post title value is stored in the
{{ request.form['title'] }}
object, and the content value is stored in the{{ request.form['content'] }}
object. This way the data you enter will not get lost if something goes wrong. For example, if you add the body text of a post but forget a title, you will see a message saying that the title is a required field. However, you will not lose the body text, as it will be saved in therequest
global object that you have access to in your templates.Save and close the
create.html
file. -
Open the
http://<public_IP_address_of_VM>:5000/create
URL and check that you can see the Create a New Post page with input fields for the post title and contents, and the submit form button.The form on this page sends a POST request to the
create()
view function. However, the function currently lacks the code to handle the POST request, so the input data will not be saved to the database after the form is submitted. You can fix this by updating thecreate()
view function. -
Open the
app.py
file:nano app.py
-
Replace the contents of the
create()
view function with the code below:... @app.route('/create', methods=('GET', 'POST')) def create(): if request.method == 'POST': title = request.form['title'] content = request.form['content'] if not title: flash('Title is required!') else: conn = get_db_connection() conn.execute('INSERT INTO posts (title, content) VALUES (?, ?)', (title, content)) conn.commit() conn.close() return redirect(url_for('index')) return render_template('create.html')
This function code:
- Employs the
if request.method == 'POST'
condition to make sure the code is executed only for POST requests. - Retrieves the new post title and contents submitted in the form fields from the
request.form
global object which contains the form data. - If no post title is specified in the form, the
if not title
condition will be executed and the user will see a message informing them to fill in the title field. - If the title is specified:
- The
get_db_connection()
function opens a connection to the database and a new post record is added to theposts
table, containing the title and post text received through the form. - The changes are committed and the connection is closed.
- The
redirect()
function redirects the user to your application's main page.
- The
Save and close the
app.py
file. - Employs the
-
Open the
http://<public_IP_address_of_VM>:5000/create
URL, enter the title and contents of the post, and submit the form. After submitting the form, you will be redirected to your application's main page where you will see a list of all posts, including the new one. -
In the
base.html
template, add a pop-up message display and a link to the new post form to the navigation bar. Open thebase.html
template:nano templates/base.html
-
Before the
About
link inside the<nav>
tag, add<li>
:... <nav class="navbar navbar-expand-lg bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <a class="nav-link active" href="{{url_for('create')}}">New Post</a> </li> <li class="nav-item"> <a class="nav-link active" href="#">About</a> </li> </ul> </div> </div> </nav> ...
-
Under
<div class="container">
, just before thecontent
section, add thefor
loop to display pop-up messages under the navigation bar:... <div class="container"> {% for message in get_flashed_messages() %} <div class="alert alert-danger">{{ message }}</div> {% endfor %} {% block content %} {% endblock %} </div> ...
Pop-up messages are available in the special
get_flashed_messages()
Flask function.Save and close the
base.html
file.The New Post control linked to the
/create
route will appear on the navigation bar. -
Go to the application's main page,
http://<public_IP_address_of_VM>:5000
, and make sure you can create a new post using the New Post link on the navigation bar.
Editing an existing post
Create a page where you can edit an existing post.
-
Open the
app.py
file:nano app.py
-
Add the
edit()
view function to the end of the file:... @app.route('/<int:id>/edit', methods=('GET', 'POST')) def edit(id): post = get_post(id) if request.method == 'POST': title = request.form['title'] content = request.form['content'] if not title: flash('Title is required!') else: conn = get_db_connection() conn.execute('UPDATE posts SET title = ?, content = ?' ' WHERE id = ?', (title, content, id)) conn.commit() conn.close() return redirect(url_for('index')) return render_template('edit.html', post=post)
Editing an existing post is similar to creating one, so this view function is almost identical to the
create()
view function. The post to edit is specified in the URL, and the post ID is provided to theedit()
function in theid
argument. The same value is also provided to theget_post()
function to retrieve the current contents of the post from the database. The application receives the updated data in a POST request, which is processed in theif request.method == 'POST'
condition.As with creating a new post, the function extracts the data from the
request.form
object into separate variables, and then checks that the user has filled in the post title. If the title is missing, the function displays a pop-up message saying that this is a required field. In other cases, the function opens a connection to the database and updates the fields in the row with the required post in theposts
table. The ID of the post in the database matches the ID specified in the URL.In the case of a GET request, the function returns HTML code generated from the
edit.html
template with the post title and text values from thepost
object.Save and close the
app.py
file. -
Create and open the
edit.html
template file in thetemplates
directory:nano templates/edit.html
-
Add the following code to the file:
{% extends 'base.html' %} {% block content %} <h1>{% block title %} Edit "{{ post['title'] }}" {% endblock %}</h1> <form method="post"> <div class="mb-3"> <label for="title" class="col-sm-2 col-form-label">Title</label> <input type="text" name="title" placeholder="Post title" class="form-control" value="{{ request.form['title'] or post['title'] }}"> </div> <div class="mb-3"> <label for="content" class="col-sm-2 col-form-label">Content</label> <textarea name="content" placeholder="Post content" class="form-control" rows="3">{{ request.form['content'] or post['content'] }}</textarea> </div> <div class="mb-3"> <button type="submit" class="btn btn-primary">Submit</button> </div> </form> <hr> {% endblock %}
The code is similar to the
create.html
template code, except for the{{ request.form['title'] or post['title'] }}
and{{ request.form['content'] or post['content'] }}
expressions. These expressions display the data stored in the request, or if the request does not exist, the data from thepost
variable provided to the template with the current database values.Save and close the
edit.html
file. -
Open
http://<public_IP_address_of_VM>:5000/1/edit
to edit the first post. You will see the Edit First Post page with the post details. Edit the post, submit the form, and check that the post is updated. -
On the main application page, add a link to the edit page for each post in the list. Open the
index.html
template:nano templates/index.html
-
Add a link to edit the post after its creation date:
... {% for post in posts %} <a href="{{ url_for('post', post_id=post['id']) }}"> <h2>{{ post['title'] }}</h2> </a> <span class="badge text-bg-primary">{{ post['created'] }}</span> <a href="{{ url_for('edit', id=post['id']) }}"> <span class="badge text-bg-warning">Edit</span> </a> <hr> {% endfor %} ...
This adds the Edit link to the post titles on the main page. This link points to the
edit()
view function and provides the post ID value frompost['id']
to it.Save and close the
index.html
file. -
Go to the application's main page,
http://<public_IP_address_of_VM>:5000
, and check that it has Edit links to edit posts.
Deleting a post
-
Open the
app.py
file:nano app.py
-
Add the
delete()
view function to the end of the file:... @app.route('/<int:id>/delete', methods=('POST',)) def delete(id): post = get_post(id) conn = get_db_connection() conn.execute('DELETE FROM posts WHERE id = ?', (id,)) conn.commit() conn.close() flash('"{}" was successfully deleted!'.format(post['title'])) return redirect(url_for('index'))
This function only accepts POST requests. This means that opening
http://<public_IP_address_of_VM>:5000/ID/delete
in a browser will return an error, as web browsers use GET requests by default. You can only send a POST request via this route by using a form that provides the ID of the post you want to delete.After receiving a POST request, the function opens a connection to the database and executes the
DELETE FROM
SQL command, and then the changes are committed, and the connection is closed. The application redirects the user to the main page and displays a message that the post was successfully deleted.The Delete button will be added to the post edit page, so you do not need a separate template for this feature.
Save and close the
app.py
file. -
Open the
edit.html
template:nano templates/edit.html
-
Add the
<form>
tag right after the<hr>
tag and before the{% endblock %}
line:... <hr> <form action="{{ url_for('delete', id=post['id']) }}" method="POST"> <input type="submit" value="Delete Post" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete this post?')"> </form> {% endblock %}
This code uses the
confirm()
method to ask the user to confirm their action before sending a request.Save and close the
edit.html
file. -
Go to the application's main page,
http://<public_IP_address_of_VM>:5000
, and check that the post edit page now has the Delete Post button.
Your application is now ready. If you did everything correctly, the final source code of your project should match the code on this page
Summarize the results
In this tutorial, you created a simple web application using the Flask
You can take this project further with a wide range of community-created Flask extensions
- Flask-Login
: Allows you to manage user sessions, i.e., handles login/logout tasks and user session management. - Flask-SQLAlchemy
: Streamlines the use of Flask by supporting SQLAlchemy . It is a Python SQL toolkit and Object Relational Mapper for working with databases. - Flask-Mail
: Helps you send emails from Flask applications.
Delete the resources you created
To stop paying for the deployed web server, delete the VM.
If you reserved a static public IP address for your web server, delete it.