The Flask Mega-Tutorial Part XI: Facelift (2018)

Posted by
on under

(Great news! There is a new version of this tutorial!)

This is the eleventh installment of the Flask Mega-Tutorial series, in which I'm going to tell you how to replace the basic HTML templates with a new set that is based on the Bootstrap user interface framework.

For your reference, below is a list of the articles in this series.

You have been playing with my Microblog application for a while now, so I'm sure you noticed that I haven't spent too much time making it look good, or better said, I haven't spent any time on that. The templates that I put together are pretty basic, with absolutely no custom styling. It was useful for me to concentrate on the actual logic of the application without having the distraction of also writing good looking HTML and CSS.

But I've focused on the backend part of this application for a while now. So in this chapter I'm taking a break from that and will spend some time showing you what can be done to make the application look a bit more polished and professional.

This chapter is going to be a bit different than previous ones, because I'm not going to be as detailed as I normally am with the Python side, which after all, is the main topic of this tutorial. Creating good looking web pages is a vast topic that is largely unrelated to Python web development, but I will discuss some basic guidelines and ideas on how to approach the task, and you will also have the application with the redesigned looks to study and learn from.

The GitHub links for this chapter are: Browse, Zip, Diff.

CSS Frameworks

While we can argue that coding is hard, our pains are nothing compared to those of web designers, who have to write templates that have a nice and consistent look on a list of web browsers. It has gotten better in recent years, but there are still obscure bugs or quirks in some browsers that make the task of designing web pages that look nice everywhere very hard. This is even harder if you also need to target resource and screen limited browsers of tablets and smartphones.

If you, like me, are a developer who just wants to create decent looking web pages, but do not have the time or interest to learn the low level mechanisms to achieve this effectively by writing raw HTML and CSS, then the only practical solution is to use a CSS framework to simplify the task. You will be losing some creative freedom by taking this path, but on the other side, your web pages will look good in all browsers without a lot of effort. A CSS framework provides a collection of high-level CSS classes with pre-made styles for common types of user interface elements. Most of these frameworks also provide JavaScript add-ons for things that cannot be done strictly with HTML and CSS.

Introducing Bootstrap

One of the most popular CSS frameworks is Bootstrap, created by Twitter. If you want to see the kind of pages that can be designed with this framework, the documentation has some examples.

These are some of the benefits of using Bootstrap to style your web pages:

  • Similar look in all major web browsers
  • Handling of desktop, tablet and phone screen sizes
  • Customizable layouts
  • Nicely styled navigation bars, forms, buttons, alerts, popups, etc.

The most direct way to use Bootstrap is to simply import the bootstrap.min.css file in your base template. You can either download a copy of this file and add it to your project, or import it directly from a CDN. Then you can start using the general purpose CSS classes it provides, according to the documentation, which is pretty good. You may also want to import the bootstrap.min.js file containing the framework's JavaScript code, so that you can also use the most advanced features.

There is a Flask extension called Flask-Bootstrap that provides a ready to use base template that has the Bootstrap framework installed. I should note that the Flask-Bootstrap extension uses Bootstrap version 3, which is not the latest version of this project. Unfortunately the author of this extension never upgraded it to support newer versions. There are a few forks on GitHub, but as of July 2021 none of them have support for Bootstrap 5, which is the current version. I have decided to continue using the Flask-Bootstrap extension because it is quite good and continues to work fine with Bootstrap 3.

Let's install this extension:

(venv) $ pip install flask-bootstrap

Using Flask-Bootstrap

Flask-Bootstrap needs to be initialized like most other Flask extensions:

app/__init__.py: Flask-Bootstrap instance.

# ...
from flask_bootstrap import Bootstrap

app = Flask(__name__)
# ...
bootstrap = Bootstrap(app)

With the extension initialized, a bootstrap/base.html template becomes available, and can be referenced from application templates with the extends clause.

But as you recall, I'm already using the extends clause with my own base template, which allows me to have the common parts of the page in a single place. My base.html template defined the navigation bar, which included a few links, and also exported a content block . All other templates in my application inherit from the base template and provide the content block with the main content of the page.

So how can I fit the Bootstrap base template? The idea is to use a three-level hierarchy instead of just two. The bootstrap/base.html template provides the basic structure of the page, which includes the Bootstrap framework files. This template exports a few blocks for derived templates such as title, navbar and content (see the complete list of blocks here). I'm going to change my base.html template to derive from bootstrap/base.html and provide implementations for the title, navbar and content blocks. In turn, base.html will export its own app_content block for its derived templates to define the page content.

Below you can see how the base.html looks after I modified it to inherit from the Bootstrap base template. Note that this listing does not include the entire HTML for the navigation bar, but you can see the full implementation on GitHub or by downloading the code for this chapter.

app/templates/base.html: Redesigned base template.

{% extends 'bootstrap/base.html' %}

{% block title %}
    {% if title %}{{ title }} - Microblog{% else %}Welcome to Microblog{% endif %}
{% endblock %}

{% block navbar %}
    <nav class="navbar navbar-default">
        ... navigation bar here (see complete code on GitHub) ...
    </nav>
{% endblock %}

{% block content %}
    <div class="container">
        {% with messages = get_flashed_messages() %}
        {% if messages %}
            {% for message in messages %}
            <div class="alert alert-info" role="alert">{{ message }}</div>
            {% endfor %}
        {% endif %}
        {% endwith %}

        {# application content needs to be provided in the app_content block #}
        {% block app_content %}{% endblock %}
    </div>
{% endblock %}

Here you can see how I make this template derive from bootstrap/base.html, followed by the three blocks that implement the page title, navigation bar and page content respectively.

The title block needs to define the text that will be used for the page title, with the <title> tags. For this block I simply moved the logic that was inside the <title> tag in the original base template.

The navbar block is an optional block that can be used to define a navigation bar. For this block, I adapted the example in the Bootstrap navigation bar documentation so that it includes a site branding on the left end, followed by the Home and Explore links. I then added the Profile and Login or Logout links aligned with the right border of the page. As I mentioned above, I omitted the HTML in the example above, but you can obtain the full base.html template from the download package for this chapter.

Finally, in the content block I'm defining a top-level container, and inside it I have the logic that renders flashed messages, which are now going to appear styled as Bootstrap alerts. That is followed with a new app_content block that is defined just so that derived templates can define their own content.

The original version of all the page templates defined their content in a block named content. As you saw above, the block named content is used by Flask-Bootstrap, so I renamed my content block as app_content. So all my templates have to be renamed to use app_content as their content block. As an example, here how the modified version of the 404.html template looks like:

app/templates/404.html: Redesigned 404 error template.

{% extends "base.html" %}

{% block app_content %}
    <h1>File Not Found</h1>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

Rendering Bootstrap Forms

An area where Flask-Bootstrap does a fantastic job is in rendering of forms. Instead of having to style the form fields one by one, Flask-Bootstrap comes with a macro that accepts a Flask-WTF form object as an argument and renders the complete form using Bootstrap styles.

Below you can see the redesigned register.html template as an example:

app/templates/register.html: User registration template.

{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block app_content %}
    <h1>Register</h1>
    <div class="row">
        <div class="col-md-4">
            {{ wtf.quick_form(form) }}
        </div>
    </div>
{% endblock %}

Isn't this great? The import statement near the top works similarly to a Python import on the template side. That adds a wtf.quick_form() macro that in a single line of code renders the complete form, including support for display validation errors, and all styled as appropriate for the Bootstrap framework.

Once again, I'm not going to show you all the changes that I've done for the other forms in the application, but these changes are all made in the code that you can download or inspect on GitHub.

Rendering of Blog Posts

The presentation logic that renders a single blog posts was abstracted into a sub-template called _post.html. All I need to do with this template is make some minor adjustments so that it looks good under Bootstrap.

app/templates/_post.html: Redesigned post sub-template.

    <table class="table table-hover">
        <tr>
            <td width="70px">
                <a href="{{ url_for('user', username=post.author.username) }}">
                    <img src="{{ post.author.avatar(70) }}" />
                </a>
            </td>
            <td>
                <a href="{{ url_for('user', username=post.author.username) }}">
                    {{ post.author.username }}
                </a>
                says:
                <br>
                {{ post.body }}
            </td>
        </tr>
    </table>

Rendering Pagination Links

Pagination links is another area where Bootstrap provides direct support. For this I just went one more time to the Bootstrap documentation and adapted one of their examples. Here is how these look in the index.html page:

app/templates/index.html: Redesigned pagination links.

    ...
    <nav aria-label="...">
        <ul class="pager">
            <li class="previous{% if not prev_url %} disabled{% endif %}">
                <a href="{{ prev_url or '#' }}">
                    <span aria-hidden="true">&larr;</span> Newer posts
                </a>
            </li>
            <li class="next{% if not next_url %} disabled{% endif %}">
                <a href="{{ next_url or '#' }}">
                    Older posts <span aria-hidden="true">&rarr;</span>
                </a>
            </li>
        </ul>
    </nav>

Note that in this implementation, instead of hiding the next or previous link when that direction does not have any more content, I'm applying a disabled state, which will make the link appear grayed out.

I'm not going to show it here, but a similar change needs to be applied to user.html. The download package for this chapter includes these changes.

Before And After

To update your application with these changes, please download the zip file for this chapter and update your templates accordingly.

Below you can see a few before and after pictures to see the transformation. Keep in mind that this change was achieved without changing a single line of application logic!

Login
Home Page

Buy me a coffee?

Thank you for visiting my blog! If you enjoyed this article, please consider supporting my work and keeping me caffeinated with a small one-time donation through Buy me a coffee. Thanks!

Buy Me A Coffee

Share this post

Hacker News
Reddit
LinkedIn
125 comments
  • #101 Dara said

    Dear sir
    After I update SQLAlchamy then when I click login user it shows errors as below :

    • Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
      127.0.0.1 - - [31/Dec/2020 15:21:03] "?[32mGET / HTTP/1.1?[0m" 302 -
      127.0.0.1 - - [31/Dec/2020 15:21:03] "?[37mGET /login?next=%2F HTTP/1.1?[0m" 200 -
      [2020-12-31 15:21:11,021] ERROR in app: Exception on /login [POST]
      Traceback (most recent call last):
      File "d:\document\my_learning\microblog\venv\lib\site-packages\sqlalchemy\util_collections.py", line 966, in call
      return self.registry[key]
      KeyError: <greenlet.greenlet object at 0x038F30D8>
  • #102 Miguel Grinberg said

    @Dara: you don't seem to be using the same code presented in this tutorial, so it is impossible for me to tell you what's wrong. This tutorial does not use greenlets at all.

  • #103 guido said

    hola miguel,
    muchas gracias por el tutorial. lejos el mejor de la web.

    te quería comentar que estoy intentado insertar un dashboard en la app dash de plotly y no me está quedando tal como lo quiero exactamente (lo quiero "integrado" al resto de la app -con la navbar por ejemplo- pero al ejecutarlo -le paso la app de flask como servidor- me pisa el resto de los bloques de bootstrap -sí, estoy usando bootstrap con los templates-).

    creo que no tengo en claro cómo pasarlo a una ruta/view para mostrarlo en pantalla. aún guardando el layout de Dash como container (en lugar de html), pasandolo luego como url a un iframe, se abre como html y me "pisa" todo el template.

    ¿vos alguna vez lo intentaste? ¿existe alguna alternativa a dash?

    gracias por todo

    abrazo

    Guido

  • #104 Miguel Grinberg said

    @guido: lamentablemente no tengo experiencia con dash. Supongo que una alternativa es armar el dashboard a mano, pero es mas trabajo.

  • #105 Ogbuchi arinze said

    Hello Miguel,

    Wonderful tutorial. I am having problems working with flask-mail . it gives me this ModuleNotFoundError even after installing flask-mail. How do i work around this, thanks in advance.
    from flask_mail import Mail
    ModuleNotFoundError: No module named 'flask_mail'

  • #106 Miguel Grinberg said

    @Ogbuchi: you may want to try making a new virtual environment and reinstalling all your dependencies to see if that helps.

  • #107 Matt said

    been following along with this so far and it's legit great. been really enjoying this mega-tutorial. Thank you SO MUCH for putting it all together!

  • #108 Hamid Allaoui said

    Thank you for this nice tutorial.

  • #109 Sean Phillips said

    Hi Miguel, you may have addressed this already:

    The Edit Profile functionality stopped working after this chapter. The reason being that my 'username already exists'. To fix it - I changed the EditProfileForm class's "validate_username" method:
    from:
    if user is not None:
    to:
    if user is not None and user.username != self.original_username:

  • #110 Miguel Grinberg said

    @Sean: The EditProfileForm.validate_username() method does not have a if user is not None conditional. I think you must have logic mixed up with another form class. I recommend that you look at the code on GitHub to have the source of truth. I don't think I have this incorrect code anywhere in the tutorial, but please let me know if you can point me to a place in which the incorrect code is shown.

  • #111 Phil said

    Hey Miguel,
    Happy new year!

    Have you looked into Bootstrap-Flask which originated as a fork from flask-bootstrap but now seems to be it's own project that is maintained?

    Best,
    Phil

  • #112 Miguel Grinberg said

    @Phil: Boostrap-Flask is also lagging as it does not have support for current releases of Bootstrap.

  • #113 jonlew said

    Hi Miguel,
    I am wondering whether you would, as an expert with tons of knowledge, build such an app for real usage precisely according to these steps, one step at a time, step by step? Or would you omit some steps and write the code directly "more sophisticated"? In other words - is this tutorial an example of how a real app si being built or is it divided into more sequential steps just for tutorial purposes?
    Thanks, this tutorial is awesome!

  • #114 Miguel Grinberg said

    @jonlew: I've made an effort to follow a natural order in this tutorial, but for a real app you would probably overlap some of the steps, not build them strictly in sequential order.

  • #115 Gab said

    Hi, I am following the steps, everything was well clear up until
    "app/templates/base.html: Redesigned base template.",
    I don't know how to incorporate the code within the HTML tags, block app_content also makes it a bit confusing, I guess I will proceed to the next chapter but.. base.html remains unclear for now.

  • #116 Miguel Grinberg said

    @Gab: you are supposed to use the code from GitHub, you don't have to figure this out yourself.

  • #117 Ersin Nurtin said

    Hello Miguel,
    Have you looked at Bootstrap-Flask which has a bs4 and bs5 support ? I believe it is maintained by a member of the Flask team. https://github.com/helloflask/bootstrap-flask

    Can we implement that instead of this unsupported version of flask bootstrap?

  • #118 Miguel Grinberg said

    @Ersin: you are welcome to adapt the tutorial project to use Bootstrap-Flask if that is what you like. I have answered this many times, I do not like that extension and will be planning to support Bootstrap 5 without any extensions in the next revision of this tutorial. Work is currently under way.

  • #119 Ersin Nurtin said

    Thank you for your reply Miguel. I have decided not to use the Bootstrap-Flask extension. I integrated bootstrap to the project without any extensions and it works great.

    I would like to make a suggestion to you about one thing. It would be great to receive an email once you reply to us, although, I am not sure if it's possible. Maybe something like subscribtion to a discussion, so we get emails every time someone posts ?

    Your tutorial is amazing and made possible for me to learn Flask. Thank you very much.
    I preordered your SQLAlchemy book on amazon. I will try to make a project with the latest version once the book is released, because SQLAlchemy documentation is very confusing to me.

  • #120 Edmund said

    Hi Miguel. I am using the wtf.quick_form method for my login form. After some inactivity, users get the CSRF warning. It confuses them. I'd like to override the default error message which is something like "The CSRF tokens don't match" to something like "Your session has expired, please refresh the page and try again". I've tried looking in the documentation but I can't get it to work. Many thanks for any assistance here.

  • #121 Miguel Grinberg said

    @Edmund: The error message is hardcoded in the Flask-WTF project, so the two options that you have are:
    1. Check the text of the error message after you run validation and replace it with your own message. You can do this directly in the template, or maybe you can also rewrite the error text in the form object, which is a bit cleaner.
    2. Override the CSRF validation method in the FlaskForm class, capture the ValidationError from the base class and re-raise the exception with your own error message.

  • #122 Edmund said

    Thank you Miguel. Could you please add some more detail about how I'd rewrite the error text in the form object? I suspect it's adding a validate_ method that raises a ValidationError, similar to your validate_email method on the RegistrationForm? Thanks very very much.

  • #123 Miguel Grinberg said

    @Edmund: I suggest you read the source code in the Flask-WTF project to become familiar with the current implementation. You will be creating a subclass of FlaskForm that overrides the validation logic, captures the ValidationError and then issues a different ValidationError with your own message.

  • #124 Viraj Nirbhavane said

    What happens to the form.hidden_tag() and the span errors?

  • #125 Miguel Grinberg said

    @Viraj: Can you please clarify your question? What errors are you asking about?

Leave a Comment