UG Web development project: Creatting Web Applications with Flask

  12 minute read   Follow @GaryLi

Introduction

This tutorial is designed for the University open day short class taste session. It explains how to develop a web-based application using the Flask web framework. It demonstrates a MeteoMaster application which processes meteo data stored in a database and presents it in the form of web application which has 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.

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.
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.

The MeteoMaster web application comprises the following components:

structure

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.

  1. Select Flask in the New Project dialog.

Creating a new Flask project

  1. In the Location field, provide the path to the project location and type the MeteoMaste as the project name. Leave the rest of the settings default and save the changes.

  2. Click Shift+F10 to run the default application. In the Run tool window, click the hyperlink and preview the target page.

Preview the Hello World app

  1. Now install all the packages required for the MeteoMaster application. The easiest way to do that is to use project dependencies (see Use requirements.txt). Right-click the project root and select New | File, then specify requirements.txt as the filename and add the following list of the dependencies to it.
Click==7.0
cycler==0.10.0
Flask==1.0.2
Jinja2==2.10
kiwisolver==1.0.1
MarkupSafe==1.1.0
matplotlib==3.0.2
numpy==1.15.4
pyparsing==2.3.0
python-dateutil==2.7.5
six==1.11.0
SQLAlchemy==1.2.14
Werkzeug==0.14.1

Click the Install requirements link to proceed with installation of the packages.

Installing packages required for the application

Setting up a Database

Now, set up a data source for your application. With PyCharm, it is quite straightforward.

  1. 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.

  2. Double-click the added file and open the Database tool window.

    Meteo database

    If you haven’t installed the SQLite database driver yet, click the configure database and click the warning message to install the missing driver.

    You should see the following tables in the user_database database: city and meteo. Double-click each table to preview the data. The city table has three columns: city_id, city_name, and city_climate (a brief text description of the city climate). The meteo table has four columns: city_id, month, average_humidity, and average_temperature. A *foreign key* is defined for the city_id column of the table` to set a relation between two tables.

  3. Optionally, right-click user_database in the Database tool window and select Properties. In the Data Sources and Drivers dialog, click Test connection to ensure that the data source has been configured properly.

    Adding a data source

  4. 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).

    Applying quick fixes for the missing import statements

  5. 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]
See the complete code of the user_database.py file in the project repository.

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.

  1. Create yet another Python file, charts.py, and paste the following code fragment:

     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()
    
    
  2. The quickest way to preview the graph is to use the Ctrl+Shift+F10 shortcut. PyCharm renders the scatter chart in the SciView tool window. You can use the Plots tab to preview the generated graph.

    Previewing a scatter chart

  3. 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 the savefig(img) function:

     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.

  1. Replace the app.route() code fragment in the app.py file with the following code:
    @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()
        return send_file(img, mimetype='image/png', cache_timeout=0)
  1. Apply the suggested quick fixes to add the missing import statements.

  2. Note that PyCharm highlights main.html because you have not created this file yet.

    Code inspection

    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="" 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=""></a></li>
                    {#% endfor %}
            </ul>
        </div>
    </body>
    </html>
Note that ·· in this fragment is a [jinja2](http://jinja.pocoo.org/) template variable used to pass the `city_name` Python variable to the HTML template.
  1. 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 New | Stylesheet, 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%;
     }
    

    PyCharm will autocomplete identifiers such as #element1 from those used in the project HTML files.

  2. When you run the application for the very first time, PyCharm automatically generated the Run/Debug configuration. You can use it to run the modified application any time you want to evaluate the result. Run/Debug configuration

Click the the Run button and follow the http://127.0.0.1:5000/ link in the Run tool window. You should expect to see the following page:

The main page of the MeteoMaster application You can continuously rerun your application on any code change. Click the list of the available configurations and select Edit Configurations. In the Flask Run/Debug configuration dialog, select the FLASK_DEBUG checkbox. With this mode enabled, PyCharm will restart the running application each time you save the code changes Ctrl+S.

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.

  1. 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 img
    

This 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.

  1. Create one more .html file to show the line charts.

    Right-click the templates directory in the project root and select New | HTML File, 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></title>
     </head>
     <body>
         <div id="element1">
             <img src="" alt="Image">
         </div>
         <div id="element2">
             <p>This graph shows mean daily temperature and average relative humidity in .</p>
             <p> </p>
             <hr/>
             <p><a href="/">Back to the main page</a></p>
         </div>
     </body>
     </html>
    
  2. 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)
         return send_file(img, mimetype='image/png', cache_timeout=0,)
    

    Don’t forget to use the quick fixes Alt+Enter to add the missing import statements.

  3. 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=""></a></li> with <li><a href=""></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.

Detailed weather information for Paris

Creating a Login Form

By this time, you have created a fully functional application that retrieves meteo data from the database and present 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.

  1. Right-click the templates directory in the project root and select New | HTML File, 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="">
                     <input type="password" class="form-control" placeholder="Password" name="password"
                            value="">
                     <input class="btn btn-default" type="submit" value="Login">
                 </form>
                 <p> </p>
             </div>
         </body>
     </html>
    

    This code implements a typical login form with the Username and Password fields.

  2. 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 and FLASK_WEB_APP_KEY.

  3. 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.

    Go to the Run/Debug configurations list on the top-right of the editor and select Edit configurations. Click the edit environmental variables icon in the Environmental variables field and add two variables.

    Adding environmental variables

  4. 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 .</p>
     <p> </p>
        
     <p>Want to edit meteo data?</p>
     <p>Please <a href="">login</a> first.</p>
        
     <hr/>
     <p><a href="/">Back to the main page</a></p>
    
    
  5. 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.

    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.

  1. Right-click the templates directory in the project root and select New | HTML File, 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 </title>
         </head>
         <body>
             <p>Edit the data for  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>
                            
    
                     </table>
                     <input class="btn btn-default" type="submit" value="Save">
                 </form>
                 <p></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.

  2. 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)

Don’t forget to add the missing import statements. See the complete code of the app.py file in the project repository.

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.

References

Last updated: 26/03/2020