Skip to content

Presentation

So far, we have been taking no account whatsoever of presentation, and our page layouts rely only on the default browser behaviour. The results are ugly, difficult to use and also require a lot of effort to maintain. Before going any further we need to improve the visual aspect of the application and at the same time make some improvements in structure so that the rest of the development will be easier.

Bootstrap

We will be using Bootstrap to improve the appearance of the application and to provide some additional user interface features. The Bootstrap-Flask package integrates Bootstrap into Flask applications.

Please note that this extension is one of the few that is not available through Anaconda at the time of writing. There is an older extension called Flask-Bootstrap, but it is a little outdated and does not provide support for Bootstrap 4.0.

To install Bootstrap-Flask, open the terminal panel in PyCharm and type or paste in the following command:

1
pip install bootstrap-flask

To include the new package's functionality in the application, we need to make two additions to app/__init__.py. The first is to add the third=party import shown below.

1
from flask_bootstrap import Bootstrap

Then in the factory function, add the line shown below in a convenient place. This could be, for example, just before the lines that register your blueprints.

1
Bootstrap(app)

Bootstrap makes many aspects of development more convenient. Although the code we have written so far is very simple, we can already make an improvement to our subject group template file by using the WTForms integration features of Flask-Bootstrap. We originally wrote an explicit <form> element into our template, but in fact we can replace it with the quick_form() function as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{% from 'bootstrap/form.html' import render_form %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <div>
        <div>
            {{ render_form(form) }}
        </div>
    </div>
</body>
</html>

Points of interest

Line 1: Import the render_form() function from the bootstrap form template.

Line 13: This single lone replaces the entire <form> element. Notice that it automatically includes the form method and action, and it also generates the CSRF token.

If you restart the application and run the page at this point, you will only see a minor change - the Save button now appears on a different line. However, we have now set the scene to take advantage of some of the more powerful features of Bootstrap. We will build these in over the course of the next few sections.

Template inheritance

You may have noticed that some parts of the templates we have written so far are identical. This goes against the DRY principle (Do not Repeat Yourself). Jinja2 can help eliminate this redundancy by allowing one template to inherit properties from another. That way we can define a base template for our application which captures the common features. This goes far beyond just removing pieces of boilerplate HTML, though. For example, we will need to provide the application user with a menu to navigate around the different pages, and that menu should be visible on all pages. The base template is also a way of grouping all of our css and script imports into a single place.

Base template

Let's start building our template structure by defining a base template. The key features will be explained afterwards. Create the file app/templates/base.html and paste in the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!doctype html>
<html lang="en">

<head>
    {% block head %}
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="icon" href="{{url_for('static', filename='images/logo.png')}}" type="image/png" sizes="any">

        {% block styles %}
            {{ bootstrap.load_css() }}
            <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.0/font/bootstrap-icons.css">
            <link rel="stylesheet" type="text/css" href="{{url_for('static', filename='css/style.css')}}">
        {% endblock %}

        {% block extra_css %}
        {% endblock %}

        <title>
            {% block title %}
                {{ title }}
            {% endblock %}
        </title>
    {% endblock %}
</head>

<body>
    {% block navbar %}
        <nav class="navbar navbar-expand-lg navbar-default bg-light topnav">
            <div class="container-fluid">
                <a id="logo" class="navbar-brand" href="{{ url_for('public.index') }}">
                    <img src="{{url_for('static', filename='images/logo.png')}}">
                </a>
                <a href="#" class="navbar-toggler" data-toggle="collapse" data-target="#main-navbar">
                    <i class="bi-list"></i>
                </a>

                <div class="collapse navbar-collapse" id="main-navbar">
                    <ul class="nav navbar-nav navbar-left">
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="DataDropdown"
                               role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                               Data maintenance
                            </a>
                            <div class="dropdown-menu" aria-labelledby="DataDropdown">
                                <a class="dropdown-item" href="{{ url_for('admin.list_subject_groups') }}">
                                    Subject groups
                                </a>
                            </div>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    {% endblock %}

    <div class="container-fluid">
        {% block content %}{% endblock %}
    </div>

    {% block footer %}{% endblock %}

    {% block scripts %}
        {{ bootstrap.load_js() }}
        <script src="{{url_for('static', filename='scripts/script.js')}}"></script>
    {% endblock %}
    {% block extra_scripts %}{% endblock %}

</body>
</html>

Explanation

Line 5: Templates can be organised into blocks. A template that inherits from this one can override a block by redefining it.

Line 8: The template specifies an image file to use in the browser tab.

Line 10: Blocks can be nested. This allows templates to be selective in the parts of the parent that they override.

Line 11: The Bootstrap-Flask extension provides the convenience function load_css() to initialise the Bootstrap css (see also Line 58)

Line 12: Bootstrap provide an icon library which can be loaded from CDN

Line 13: Imports a local stylesheet where any custom styles are defined.

Line 16: Some blocks in the base template are empty purely to provide placeholders for detailed content in child templates.

Line 28: The base template defines the main navigation that is present on all pages.

Lines 31 - 32: These two lines define a logo element that appears at the left-hand side of the navigation bar. Clicking on the logo element will return the browser to the home page.

Lines 34 - 36: Using Bootstrap it is easy to make your page responsive. These three lines define a link for viewing the menu which is only visible when the browser window is narrow. This is useful when viewing the page on a mobile device.

Line 39 - 51: This combination of elements defined a dropdown menu.

Lines 57 - 59: The block defines a placeholder for the main content of a child page.

Line 61: Placeholder for a page footer.

Lines 63 - 66: Following the main page content is a block where scripts are loaded. This includes the main Bootstrap script using the Bootstrap-Flask convenience function, and a local script file.

Static files

Later in the development, we may need to define custom styles and scripts. The base template makes reference to these files, and here we create empty placeholders for future use.

The two files required are app/static/css/styles.css and app/static/scripts/script.js, so the first thing to do is to modify the directory structure of our application. Specifically, we need to create the static branch as shown below.


    └── students_app
        └── app/
            └── static/
                ├── css/
                ├── images/
                └── scripts/

The static directory contains files such as scripts, style definitions and images. With the directories in place, create the files app/static/css/style.css and app/static/scripts/script.js.

Download the logo image (credit to Pinclipart) and save it to the directory app/static/images.

Using the template

The new template contains a lot of features that should appear on every page. On any given page, we therefore only need to think about the content that is unique to that page. Everything else is handled by the template. To take advantage of template inheritance, we can modify the index.html template that we created earlier as shown below. You will see that most of the original content has been replaced because it is now part of the template.

1
2
3
4
5
6
{% extends "base.html" %}
{% block content %}
    <div class="d-flex justify-content-center">
        <h1>{{ content }}</h1>
    </div>
{% endblock %}

Explanation

Line 1: Link to the base template.

Lines 2 - 6: Override the placeholder block on lines 57 - 59 of the base template.

Lines 3 - 5: This is the unique content of the page that is placed into the content block.

Line 3: The new div element makes use of some Bootstrap styles to centre its content horizontally.

Restart the application and navigate to the index page. You should see a styled home page with a logo and active navigation as shown in Figure 16.

Figure 16. Styled home page

Linking templates (list page)

Although the menu works, the new styling has not yet been applied to the subject group pages. To implement this, we need to reference the base template in the page templates. Open the file app/templates/admin/subject_groups.html and modify the content to match the code below. NB. Don't copy and paste the code below - it is not complete!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% extends "base.html" %}
{% block content %}
    <div class="d-flex justify-content-center">
        <div class="flex-column">
            <h1>{{ title }}</h1>
            {% if rowdata %}
                :
                :
            {% endif %}
            <div>
                <a href="{{ url_for('admin.add_subject_group') }}">New</a>
            </div>
        </div>
    </div>
{% endblock %}

Explanation

Line 1: Directive that links this template to the base template.

Line 2: Override the content block.

Lines 3 - 4: Use some Bootstrap styling to centre the content.

To improve the appearance of the table, find its opening tag and replace the line with the following.

1
<table class="table table-striped table-bordered fixed-header">

The New link can also be improved by applying styling to give it the appearance of a button. Find the line where the New link is defined and replace it with the following.

1
<a class="btn btn-primary" href="{{ url_for('admin.add_subject_group') }}">New</a>

Linking templates (form page)

We need to do a similar job on the subject_group.html file. Edit the contents to match the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{% from 'bootstrap/form.html' import render_form %}
{% extends "base.html" %}
{% block content %}
    <div class="d-flex justify-content-center">
        <div class="flex-column">
            <h1>{{ title }}</h1>
            <div>
                <div>
                    {{ render_form(form) }}
                </div>
            </div>
        </div>
    </div>
{% endblock %}

Take a look at the new version of the template - notice anything interesting? By using the render_form() function, we have removed all explicit references to the subject group model. This means that the template will work in any situation where the only page content is a rendered form. We can take advantage of this generality by renaming the template to form_page.html and moving it up to the templates directory instead of the admin subdirectory. We will also need to make a corresponding change to the file app/admin/views/subject_group.py to update the two lines where the template is referenced. You will find these reference in the add_subject_group() and edit_subject_group() functions.

Using Bootstrap icons

The final piece of styling we will do before moving on will be to replace the text on the Edit and Del links with icons. For this, we will use the Bootstrap icon library that we loaded as part of the base template.

Bootstrap icons can be embedded in several different ways, and here we will use the font representation. This simply consists of adding an icon tag with the required class.

Open the file app/templates/admin/subject_group.html and find the Edit and Del links. Replace the existing code with the following.

1
2
3
4
5
6
<a title="Edit" href="{{ url_for('admin.edit_subject_group', id=row.id) }}">
    <i class="bi-pencil"></i>
</a>
<a title="Delete" href="{{ url_for('admin.delete_subject_group', id=row.id) }}">
    <i class="bi-trash"></i>
</a>

Points of interest

Lines 1 & 4: Notice the addition of the title attribute to provide popup text.

Lines 2 & 5: Bootstrap icon references.

Flash messages

Flask includes a simple method of providing feedback to users on the results of the most recent operation. The message is delivered along with the next http request and is then discarded. Message flashing can be used to confirm database operations, for example, or for reporting errors. Here, we will add a confirmation message when a new subject group record is created.

Creating a flash message

We need to make some changes to the file app/admin/views/subject_group.py. The first one is to import the flash function from the flask package. Modify the import statement at the beginning of the file so that it matches the following.

1
from flask import render_template, url_for, redirect, flash

Then we need to add the message itself. Scroll down to the add_subject_group() and modify it so that it matches the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def add_subject_group():
    form = SubjectGroupForm()
    if form.validate_on_submit():
        subject_group = SubjectGroup(name=form.name.data)
        try:
            db.session.add(subject_group)
            db.session.commit()
            flash('New record created', 'success')
        except:
            db.session.rollback()
            flash('An error occurred - no record created', 'error')

        return redirect(url_for('admin.list_subject_groups'))

    return render_template('admin/subject_group.html',
                           form=form,
                           title="Add subject group")

Points of interest

Line 8: Success message

Line 11: Error message

If no category is supplied in the second parameter, the message type defaults to info. There is also a warning type available.

Displaying flash messages

Any messages have to be displayed in an HTML div element. First, we will do things the simple way and use a Bootstrap-Flask macro to all the work for us. Because message may be delivered to any page of the application, it makes sense to process the messages in the base template. Open app/templates/base.html and add the following line at the start of the file.

1
{% from 'bootstrap/utils.html' import render_messages %}

Then add the following line after the navbar block and before the div containing the content block.

1
{{ render_messages() }}

Restart the application and try out the new feature. The default behaviour might be sufficient for your purposes, but if not, the following example illustrates how to display messages as toasts.

First, edit the base template again and add the following code before the head element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    {% for category, message in messages %}
        <div id="toast-{{ loop.index }}" class="toast toast-{{ category }}">
            {{ message }}
        </div>
            <script type="text/javascript">
                var x = document.getElementById("toast-{{ loop.index }}");
                x.className += " show";
                setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
            </script>
    {% endfor %}
  {% endif %}
{% endwith %}

The code adds a new div element for each message and displays it for three seconds. In addition, it adds a css class to the div which animates it. The required css code shown below is based on an example from W3Schools. Copy it and paste it into the file app/static/css/style.css.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* Toast functionality. */

.toast {
    visibility: hidden;  /* Hidden by default. Visible on click */
    min-width: 250px;    /* Set a default minimum width */
    margin-left: -125px; /* Divide value of min-width by 2 */
    text-align: center;  /* Centered text */
    border-radius: 8px;  /* Rounded borders */
    padding: 16px;       /* Padding */
    position: fixed;     /* Sit on top of the screen */
    z-index: 1;          /* Add a z-index if needed */
    right: 20px;         /* Horizontal position */
    top: 100px;          /* vertical position */
}

/* Show the toast when clicking on a button (class added with JavaScript) */
.toast.show {
    visibility: visible; /* Show the toast */
    /* Add animation: Take 0.5 seconds to fade in and out the toast.
   However, delay the fade out process for 2.5 seconds */
   -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
   animation: fadein 0.5s, fadeout 0.5s 2.5s;
}

/* Animations to fade the toast in and out */
@-webkit-keyframes fadein {
    from {top: 50px; opacity: 0;}
    to {top: 100px; opacity: 1;}
}

@keyframes fadein {
    from {top: 50px; opacity: 0;}
    to {top: 100px; opacity: 1;}
}

@-webkit-keyframes fadeout {
    from {top: 100px; opacity: 1;}
    to {top: 150px; opacity: 0;}
}

@keyframes fadeout {
    from {top: 100px; opacity: 1;}
    to {top: 150px; opacity: 0;}
}

.toast-info {
    background-color: #d1ecf1;
    color: #0c5460;
}

.toast-success {
    background-color: #d4edda;
    color: #155724;
    font-weight: bold;
}

.toast-warning {
    background-color: #fff3cd;
    color: #856404;
}

.toast-error {
    background-color: #f8d7da;
    color: #721c24;
    font-weight: bold;
}

When you restart the application to see the new message presentation,you may have to clear your browser's cache. On Chrome, you can force the browser to reload static files by holding down the SHIFT key as you refresh the page.

Exception handling

With the new flash messaging in place, you can see what happens when a database error occurs. In the subject group model, the name field was defined as being unique. That means that the database will reject any attempt to insert a record with a duplicate name. If you try adding two subject groups with the same name in the current version of the application, you will just see the generic error message. This is not very helpful for understanding what has gone wrong. It would be better to return the actual error message from the database.

Edit the file app/admin/views/subject_group.py and add the followinf import statement at the start.

1
from sqlalchemy.exc import SQLAlchemyError

Then add the following more specific exception clause before the existing one in the function add_subject_group().

1
2
3
4
except SQLAlchemyError as e:
    db.session.rollback()
    error = str(e.__dict__['orig'])
    flash('{}'.format(error), 'error')

A try: ... except: block can include several except: clauses and the first one that matches the error will be executed. In this case, any database errors will be trapped by the new clause and any other errors will be trapped by the original one.

Custom error pages

Web applications make use of HTTP errors to let users know that something has gone wrong. Default error pages are usually quite plain, so we will create our own custom ones for the following common HTTP errors:

  • 403 Forbidden: this occurs when a user is logged in (authenticated), but does not have sufficient permissions to access the resource.
  • 404 Not Found: this occurs when a user attempts to access a non-existent resource such as an invalid URL.
  • 500 Internal Server Error: this is a general error thrown when a more specific error cannot be determined. It means that for some reason, the server cannot process the request.

We'll start by writing the views for the custom error pages. In your app/__init__.py file, add the following code after the registration of the blueprints:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@app.errorhandler(403)
def forbidden(error):
    return render_template(
        'error.html',
        title='Forbidden',
        errno=403,
        message ='You do not have sufficient permissions to access this page'
    ), 403

@app.errorhandler(404)
def page_not_found(error):
    return render_template(
        'error.html',
        title='Page Not Found',
        errno=404,
        message="The page you're looking for doesn't exist"
    ), 404

@app.errorhandler(500)
def internal_server_error(error):
    return render_template(
        'error.html',
        title='Server Error',
        errno=500,
        message="The server encountered an internal error. That's all we know."
    ), 500

You will need to update the import statement at the start of the file to import the render_template function from flask.

Now we need to add the error template itself. Create the file app/templates/error.html and paste in the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
    <div class="d-flex justify-content-center">
        <div class="flex-column">
            <h1>Error {{ errno }}: {{ title }}</h1>
            <h3>{{ message }}</h3>
            <a class="btn btn-primary" href="{{ url_for('public.index') }}">
                <i class="bi-house"></i>
                Home
            </a>
        </div>
    </div>
{% endblock %}

Summary

This section has presented some simple ways to improve the appearance of the application and the usability of the interface. However, we have barely scratched the surface of what can be achieved with the Bootstrap framework and with custom css and javascript. We have not even begun to consider other third-party widgets that could be integrated into the application.

Further reading

 Bootstrap 4 Get Started

 Bootstrap tutorial