Creating Web Applications with Flask
This tutorial explains how to develop a web-based application using the Flask web framework. Our MeteoMaster application processes meteo data stored in a database and presents it in the form of the following charts:
Scatter chart — a cumulative report of the average yearly temperature and humidity in Prague, St. Petersburg, San Francisco, Paris , and Singapore.
Line charts — Average monthly temperature and humidity in each city.
'Staying Above the Weather' application overview
You will use various Web technologies to implement the following application functions:
Function | Technology |
---|---|
Meteo data operations | SQLite database to store data, SQLAlchemy package to perform operations with the database in Python code. |
Graphical representation | matplotlib package to plot graphs. |
Viewing and editing content | HTML to create page views and Jinja to create smart templates. |
Managing content | Flask to orchestrate the application content. |
MeteoMaster is a web application that comprises the following components:
- Main page
Application entry point that renders the scatter chart and provides links to the detailed summary of climate in a particular city.
- City
A series of pages with the detailed information about climate in each city.
- Login
Authentication page. One needs to enter the valid credentials to edit meteo data.
- Edit
A series of pages for editing city specific data.
Each HTML page has a corresponding Flask view implemented in Python code.
Creating a Flask application in PyCharm
Create a basic Flask project as described in Creating a Flask Project to start prototyping the application.
Select Flask in the New Project dialog.
In the Location field, provide the path to the project location and type meteoMaster as the project name. Leave the rest of the settings default and click Create.
PyCharm creates the new virtual environment for you and opts to use Jinja2 as a template language. As the result, you'll get the predefined project structure and the basic "Hello World!" code.
Click Run 'meteoMaster' in the Run widget at the top to launch the automatically created run configuration.
In the Run tool window, click the hyperlink and preview the page of the basic 'Hello, world' application.
Now install all the packages required for the MeteoMaster application. The easiest way to do that is to use project dependencies (refer to Use requirements.txt). Right-click the project root and select , then specify requirements.txt as the filename and add the following list of the dependencies to it.
blinker==1.7.0 click==8.1.7 contourpy==1.2.0 cycler==0.12.1 flask==3.0.0 fonttools==4.47.0 itsdangerous==2.1.2 jinja2==3.1.2 kiwisolver==1.4.5 markupsafe==2.1.3 matplotlib==3.8.2 numpy==1.26.2 packaging==23.2 pillow==10.2.0 pyparsing==3.1.1 python-dateutil==2.8.2 six==1.16.0 sqlalchemy==2.0.24 typing-extensions==4.9.0 werkzeug==3.0.1Click the Install requirements link to proceed with installation of the packages.
Setting up a database
Now, set up a data source for your application. With PyCharm, it is quite straightforward.
Download the predefined database with the meteo data for five cities from the following location:
https://github.com/allaredko/flask-tutorial-demo/blob/master/user_database
Save the user_database file in the project root directory.
Double-click the added file. In the Data Sources and Drivers dialog that opens, click Test connection to ensure that the data source has been configured properly. If you see an Incomplete Configuration warning, click Download Driver Files.
Click OK to complete the creation of the data source, the Database tool window opens.
You should see the following tables:
city
andmeteo
. Double-click each table to preview the data. Thecity
table has three columns:city_id
,city_name
, andcity_climate
(a brief text description of the city climate). Themeteo
table has four columns:city_id
,month
,average_humidity
, andaverage_temperature
. A foreign key is defined for thecity_id
column of thetable
to set a relation between two tables.Create a Python file, user_database.py, to work with the newly created database. Use SQLAlchemy declarative base syntax to describe the database.
metadata = MetaData() engine = create_engine('sqlite:///user_database', connect_args={'check_same_thread': False}, echo=False) # echo=False Base = declarative_base() db_session = sessionmaker(bind=engine)() # Table city class City(Base): __tablename__ = 'city' city_id = Column(Integer, primary_key=True) city_name = Column(String) city_climate = Column(String) city_meteo_data = relationship("Meteo", backref="city") # Table meteo class Meteo(Base): __tablename__ = 'meteo' id = Column(Integer, primary_key=True) city_id = Column(ForeignKey('city.city_id')) month = Column(String) average_humidity = Column(Integer) average_temperature = Column(Float)Instead of manually adding import statements for the code fragment, apply the suggested quick fixes: just click the bulb icon (or press Alt+Enter).
Now that you have defined the tables and their relationship, add the following functions to retrieve data from the database:
# Retrieving data from the database def get_cities(): return db_session.query(City) # Generating the set of average temperature values for a particular city def get_city_temperature(city): return [month.average_temperature for month in city.city_meteo_data] # Generating the set of average humidity values for a particular city def get_city_humidity(city): return [month.average_humidity for month in city.city_meteo_data] data = get_cities() MONTHS = [record.month for record in data[0].city_meteo_data] CITIES = [city.city_name for city in data]The full code of the user_database.py file is available below:
- user_database.py
- from sqlalchemy import MetaData, create_engine, Column, Integer, String, ForeignKey, Float from sqlalchemy.orm import declarative_base, sessionmaker, relationship metadata = MetaData() engine = create_engine('sqlite:///user_database', connect_args={'check_same_thread': False}, echo=False) # echo=False Base = declarative_base() db_session = sessionmaker(bind=engine)() # Table city class City(Base): __tablename__ = 'city' city_id = Column(Integer, primary_key=True) city_name = Column(String) city_climate = Column(String) city_meteo_data = relationship("Meteo", backref="city") # Table meteo class Meteo(Base): __tablename__ = 'meteo' id = Column(Integer, primary_key=True) city_id = Column(ForeignKey('city.city_id')) month = Column(String) average_humidity = Column(Integer) average_temperature = Column(Float) # Retrieving data from the database def get_cities(): return db_session.query(City) # Generating the set of average temperature values for a particular city def get_city_temperature(city): return [month.average_temperature for month in city.city_meteo_data] # Generating the set of average humidity values for a particular city def get_city_humidity(city): return [month.average_humidity for month in city.city_meteo_data] data = get_cities() MONTHS = [record.month for record in data[0].city_meteo_data] CITIES = [city.city_name for city in data]
Plotting the scatter chart
You are ready to retrieve data and build the first graph - the scatter chart with the yearly average temperature and humidity in each city. Use the matplotlib library to set up the graph and assign the values.
Create another Python file, charts.py, and fill it with the following code:
import matplotlib.pyplot as plt from user_database import data, get_city_temperature, get_city_humidity, CITIES yearly_temp = [] yearly_hum = [] for city in data: yearly_temp.append(sum(get_city_temperature(city))/12) yearly_hum.append(sum(get_city_humidity(city))/12) plt.clf() plt.scatter(yearly_hum, yearly_temp, alpha=0.5) plt.title('Yearly Average Temperature/Humidity') plt.xlim(70, 95) plt.ylabel('Yearly Average Temperature') plt.xlabel('Yearly Average Relative Humidity') for i, txt in enumerate(CITIES): plt.annotate(txt, (yearly_hum[i], yearly_temp[i])) plt.show()The quickest way to preview the graph is to right-click anywhere in the editor and select Run 'charts' from the context menu. PyCharm renders the scatter chart in the Plots tool window.
Now save the graph into an image so that you can add it to the main page of your application. Replace
plt.show()
with the fragment that utilizes thesavefig(img)
method and wrap the code into theget_main_image()
function. Here is what you should get:from io import BytesIO import matplotlib.pyplot as plt from user_database import data, MONTHS, get_city_temperature, get_city_humidity, CITIES def get_main_image(): """Rendering the scatter chart""" yearly_temp = [] yearly_hum = [] for city in data: yearly_temp.append(sum(get_city_temperature(city))/12) yearly_hum.append(sum(get_city_humidity(city))/12) plt.clf() plt.scatter(yearly_hum, yearly_temp, alpha=0.5) plt.title('Yearly Average Temperature/Humidity') plt.xlim(70, 95) plt.ylabel('Yearly Average Temperature') plt.xlabel('Yearly Average Relative Humidity') for i, txt in enumerate(CITIES): plt.annotate(txt, (yearly_hum[i], yearly_temp[i])) img = BytesIO() plt.savefig(img) img.seek(0) return img
Creating the main page
Set up the main page of the application and create a view for the scatter chart.
Replace the definition of the
hello_world
function with theapp.route()
decorator in the app.py file with the following code:def get_headers(response): response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' @app.route('/') def main(): """Entry point; the view for the main page""" cities = [(record.city_id, record.city_name) for record in data] return render_template('main.html', cities=cities) @app.route('/main.png') def main_plot(): """The view for rendering the scatter chart""" img = get_main_image() response = send_file(img, mimetype='image/png') get_headers(response) return responseApply the suggested quick fixes to add the missing import statements.
Note that PyCharm highlights main.html because you have not created this file yet.
With PyCharm intention action you can quickly create the missing template file. Press Alt+Enter and select Create template main.html from the context menu. Confirm the name and location of the template file and click OK. As the result, the main.html will be added to the templates directory. Open the newly added file and paste the following code into it:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link href="../static/style.css" rel="stylesheet" type="text/css"> <title>Typical Climate</title> </head> <body> <div id="element1"> <img src="{{ url_for('main_plot') }}" alt="Image"> </div> <div id="element2"> <p>What city has the best climate?</p> <p>When planning your trip or vacation you often check weather data for humidity and temperature.</p> <p>Below is the collection of the yearly average values for the following cities: </p> <ul> {% for city_id, city_name in cities %} <li><a href="">{{ city_name }}</a></li> {% endfor %} </ul> </div> </body> </html>Note that
{{ city_name }}
in this fragment is a Jinja2 template variable used to pass thecity_name
Python variable to the HTML template.Also create the stylesheet to set up font and layout settings for all HTML pages in your application. Right-click the static directory and select , then specify the name of the css file, style.css, and paste the following style definitions:
body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } #element1 { display: inline-block; } #element2 { display: inline-block; vertical-align: top; margin-top: 90px; alignment: left; width: 25%; }You can use the automatically created
meteoMaster
configuration to run the modified application any time you want to evaluate the result.Click the Run or Rerun and follow the http://127.0.0.1:5000/ link in the Run tool window.
You should see the following page:
Plotting line charts
To provide application users with the detailed information about climate of a particular city, render the line charts with the relevant information.
Modify the charts.py file by adding the
get_city_image
function:def get_city_image(city_id): """Rendering line charts with city specific data""" city = data.get(city_id) city_temp = get_city_temperature(city) city_hum = get_city_humidity(city) plt.clf() plt.plot(MONTHS, city_temp, color='blue', linewidth=2.5, linestyle='-') plt.ylabel('Mean Daily Temperature', color='blue') plt.yticks(color='blue') plt.twinx() plt.plot(MONTHS, city_hum, color='red', linewidth=2.5, linestyle='-') plt.ylabel('Average Relative Humidity', color='red') plt.yticks(color='red') plt.title(city.city_name) img = BytesIO() plt.savefig(img) img.seek(0) return imgThis function plots two linear charts: monthly Mean Daily Temperature and Average Relative Humidity for each city. Similarly to the
get_main_image
function, it saves the chart in an image.Create one more .html file to show the line charts.
Right-click the templates directory in the project root and select , then enter, city.html as the file's name, and paste the following code in the newly created file:
- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link href="../static/style.css" rel="stylesheet" type="text/css"> <title>{{ city_name }}</title> </head> <body> <div id="element1"> <img src="{{ url_for('city_plot', city_id=city_id) }}" alt="Image"> </div> <div id="element2"> <p>This graph shows mean daily temperature and average relative humidity in {{ city_name }}.</p> <p> {{ city_climate }}</p> <hr/> <p><a href="/">Back to the main page</a></p> </div> </body> </html>
The remaining step is to create two more views using the
Flask.route
function. Add the following code fragment to the app.py file:@app.route('/city/<int:city_id>') def city(city_id): """Views for the city details""" city_record = data.get(city_id) return render_template('city.html', city_name=city_record.city_name, city_id=city_id, city_climate=city_record.city_climate) @app.route('/city<int:city_id>.png') def city_plot(city_id): """Views for rendering city specific charts""" img = get_city_image(city_id) response = send_file(img, mimetype='image/png') get_headers(response) return responseDon't forget to use the quick fixes Alt+Enter to add the missing import statements.
Now modify the main.html file to populate the list of the cities with the links to the corresponding city/* page. Replace
<li><a href="">{{ city_name }}</a></li>
with<li><a href="{{ url_for('city', city_id=city_id) }}">{{ city_name }}</a></li>
.
Rerun the Run/Debug configuration to restart the application and click the link that leads to Paris. You should expect to get navigated to the city/2 page.
Creating a login form
By this time, you have created a fully functional application that retrieves meteo data from the database and presents them as charts. However, in real life you typically want to edit the data. Reasonably, editing should only be permitted to authorized users, so, let's create a login form.
Right-click the templates directory in the project root and select , then enter, login.html as the file's name, and paste the following code in the newly created file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="../static/style.css"> <title>Login form </title> </head> <body> <p>Login to edit the meteo database: </p> <div class="container"> <form action="" class="form-inline" method="post"> <input type="text" class="form-control" placeholder="Username" name="username" value="{{ request.form.username }}"> <input type="password" class="form-control" placeholder="Password" name="password" value="{{ request.form.password }}"> <input class="btn btn-default" type="submit" value="Login"> </form> <p>{{ error }}</p> </div> </body> </html>This code implements a typical login form with the Username and Password fields.
Add the following code to the app.py file to create the Flask view for the login form and to control the login session.
@app.route('/login/<int:city_id>', methods=["GET", "POST"]) def login(city_id): """The view for the login page""" city_record = data.get(city_id) try: error = '' if request.method == "POST": attempted_username = request.form['username'] attempted_password = request.form['password'] if attempted_username == 'admin' and attempted_password == os.environ['USER_PASSWORD']: session['logged_in'] = True session['username'] = request.form['username'] return redirect(url_for('edit_database', city_id=city_id)) else: print('invalid credentials') error = 'Invalid credentials. Please, try again.' return render_template('login.html', error=error, city_name=city_record.city_name, city_id=city_id) except Exception as e: return render_template('login.html', error=str(e), city_name=city_record.city_name, city_id=city_id) def login_required(f): @wraps(f) def wrap(*args, **kwargs): """login session""" if 'logged_in' in session: return f(*args, **kwargs) else: pass return redirect(url_for('login')) return wrap app.secret_key = os.environ['FLASK_WEB_APP_KEY']Use the Alt+Enter shortcut to add the missing import statements. Note that this code fragment introduces two environmental variables:
USER_PASSWORD
andFLASK_WEB_APP_KEY
.You can record the values of the newly created environmental variables in the meteoMaster Run/Debug configuration as this is a safer way to store security sensitive information than hardcoding it in the app.py file.
Click in the Run widget and select Edit Configurations. In the Run/Debug Configrations dialog, make sure that the meteoMaster configuration is selected and Click the icon in the Environmental variables field and add the two variables.
Modify the
<div id="element2">
element in the city.html file to accommodate login facilities:<p>This graph shows mean daily temperature and average relative humidity in {{ city_name }}.</p> <p> {{ city_climate }}</p> {% if session['logged_in'] %} <p>Want to add more data?</p> <p>Go and <a href="{{ url_for('edit_database', city_id=city_id) }}">edit</a> the meteo database.</p> {% else %} <p>Want to edit meteo data?</p> <p>Please <a href="{{ url_for('login', city_id=city_id) }}">login</a> first.</p> {% endif %} <hr/> <p><a href="/">Back to the main page</a></p>Restart the application and click any city link, then click the login link in the "Please login first" sentence. You should expect to see the login form.
For time being, this form doesn't enable editing because you have not implemented the corresponding page. Meanwhile, you can try to enter any incorrect password to check if it is processed with the message: "Invalid credentials. Please, try again."
Editing the data
The last remaining step is to enable editing of the meteo data.
Right-click the templates directory in the project root and select , then enter, edit.html as the file's name, and paste the following code in the newly created file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="../static/style.css"> <title>Edit meteo data for {{ city_name }}</title> </head> <body> <p>Edit the data for {{ city_name }} as appropriate:</p> <div class="container"> <form name="meteoInput" action="" class="form-inline" method="post"> <table> <tr> <td>Month</td> <td colspan="2" align="center">Average Temperature</td> <td colspan="2" align="center">Average Humidity</td> </tr> {% for month in months %} <tr> <td>{{ month }}</td> <td> <input placeholder="20" class="form-control" name="temperature{{ loop.index0 }}" value="{{ meteo[0][loop.index0]}}" type="range" min="-50.0" max="50.0" step="0.01" oninput="temp_output{{ loop.index0 }}.value=this.value" > </td> <td> <output name="temp_output{{ loop.index0 }}">{{ '%0.2f' % meteo[0][loop.index0]|float }}</output> <label> C</label> </td> <td> <input placeholder="20" class="form-control" name="humidity{{ loop.index0 }}" value="{{ meteo[1][loop.index0]}}" type="range" min="0" max="100" oninput="hum_output{{ loop.index0 }}.value=this.value"> </td> <td> <output name="hum_output{{ loop.index0 }}">{{ meteo[1][loop.index0]}}</output> <label> %</label> </td> </tr> {% endfor %} </table> <input class="btn btn-default" type="submit" value="Save"> </form> <p>{{ error }}</p> </div> </body> </html>This fragment also utilizes a Jinjia2 template to process the input data and pass it to the Python code that performs a commit to the database.
Add one more code fragment to the app.py file that creates a Flask view for the edit page, processes input data, and updates the database:
@app.route('/edit/<int:city_id>', methods=["GET", "POST"]) @login_required def edit_database(city_id): """Views for editing city specific data""" month_temperature = [] month_humidity = [] city_record = data.get(city_id) meteo = [get_city_temperature(city_record), get_city_humidity(city_record)] try: if request.method == "POST": # Get data from the form for i in range(12): # In a production application we ought to validate the input data month_temperature.append(float(request.form[f'temperature{i}'])) month_humidity.append(int(request.form[f'humidity{i}'])) # Database update for i, month in enumerate(city_record.city_meteo_data): month.average_temperature = month_temperature[i] month.average_humidity = month_humidity[i] db_session.commit() return redirect(url_for('main', city_id=city_id)) else: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo) except Exception as error: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo, error=error)The full code of the app.py file is available below:
- app.py
- import os from functools import wraps from flask import Flask, send_file, render_template, request, session, redirect, url_for from charts import get_main_image, get_city_image from user_database import data, db_session, get_city_temperature, get_city_humidity, MONTHS app = Flask(__name__) def get_headers(response): response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' @app.route('/') def main(): """Entry point; the view for the main page""" cities = [(record.city_id, record.city_name) for record in data] return render_template('main.html', cities=cities) @app.route('/main.png') def main_plot(): """The view for rendering the scatter chart""" img = get_main_image() response = send_file(img, mimetype='image/png') get_headers(response) return response @app.route('/city/<int:city_id>') def city(city_id): """Views for the city details""" city_record = data.get(city_id) return render_template('city.html', city_name=city_record.city_name, city_id=city_id, city_climate=city_record.city_climate) @app.route('/city<int:city_id>.png') def city_plot(city_id): """Views for rendering city specific charts""" img = get_city_image(city_id) response = send_file(img, mimetype='image/png') get_headers(response) return response @app.route('/login/<int:city_id>', methods=["GET", "POST"]) def login(city_id): """The view for the login page""" city_record = data.get(city_id) try: error = '' if request.method == "POST": attempted_username = request.form['username'] attempted_password = request.form['password'] if attempted_username == 'admin' and attempted_password == os.environ['USER_PASSWORD']: session['logged_in'] = True session['username'] = request.form['username'] return redirect(url_for('edit_database', city_id=city_id)) else: print('invalid credentials') error = 'Invalid credentials. Please, try again.' return render_template('login.html', error=error, city_name=city_record.city_name, city_id=city_id) except Exception as e: return render_template('login.html', error=str(e), city_name=city_record.city_name, city_id=city_id) def login_required(f): @wraps(f) def wrap(*args, **kwargs): """login session""" if 'logged_in' in session: return f(*args, **kwargs) else: pass return redirect(url_for('login')) return wrap app.secret_key = os.environ['FLASK_WEB_APP_KEY'] @app.route('/edit/<int:city_id>', methods=["GET", "POST"]) @login_required def edit_database(city_id): """Views for editing city specific data""" month_temperature = [] month_humidity = [] city_record = data.get(city_id) meteo = [get_city_temperature(city_record), get_city_humidity(city_record)] try: if request.method == "POST": # Get data from the form for i in range(12): # In a production application we ought to validate the input data month_temperature.append(float(request.form[f'temperature{i}'])) month_humidity.append(int(request.form[f'humidity{i}'])) # Database update for i, month in enumerate(city_record.city_meteo_data): month.average_temperature = month_temperature[i] month.average_humidity = month_humidity[i] db_session.commit() return redirect(url_for('main', city_id=city_id)) else: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo) except Exception as error: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo, error=error) if __name__ == '__main__': app.run()
Relaunch the run configuration or save the app.py file (Ctrl+S) to trigger the automatic relaunch. Now you can select, for example,
Paris
on the app's homepage, click edit, enter the admin's credentials, and you'll get to the page where you can edit meteo data.
With this step, you have completed the task of creating a Flask-based application that interacts with the database. Now you've got full power over the weather. Go and modify meteo data for any of the cities, so that the changes be prominent on the charts. Then preview the changes.