2. Application Structure

The pages in a web application share some components such as headers, footers and navigation panels. In this chapter, we’ll see how to implement such components without repeating code. We’ll create a base template that will contain the components that all pages in the application will include. See Fig. 2.1 for the screenshot of the resulting home page.

Screenshot of home page with navigation panel and footer.

Fig. 2.1 Home page containing navigation panel and footer.

2.1. Base Templates

Manually adding shared components to multiple pages in an application is tedious and error-prone. We would prefer to write these once and let pages get them from a shared location. For instance, we want all pages to use the same footer that contains a copyright notice but we don’t want to add the footer markup to every page separately. Instead, we want to add it to a base template and derive all pages from this base template.

We’ll store our base template in the layout.html file with the contents given in Listing 2.1.

Listing 2.1 Initial base template (file: templates/layout.html, version: v0201).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>My movies - {% block title %}{% endblock %}</title>
    <link rel="stylesheet" href="/static/mymovies.css"/>
  </head>
  <body>
    {% block content %}{% endblock %}

    <footer>
      <small>&copy; 2015-2018, Kilgore Trout</small>
    </footer>
  </body>
</html>

The {% block ... %} and {% endblock %} directives in Jinja mark the parts that will be filled in by the pages that use this base template. Since there can be more than one such block in a template, blocks have to be named so that they can be distinguished from each other. The name is the identifier that follows the block keyword in the opening directive.

All remaining parts will be directly included. This means that there are two parts in this template that will be filled in by pages:

  • The block named title marks the page title after the common prefix (line 5).
  • The block named content marks the page content (line 9). Note that the footer is not part of the block, so it will be included in all pages (lines 11-13).

Now the home page template will extend this base template (Listing 2.2, line 1). The title block (line 2) will replace the title slot in the layout, and the content block (lines between 3 and 10) will replace the content slot. When processed, this template will give the same markup as the previous one without the base template (Listing 1.9).

Listing 2.2 Home page template extending the base template (file: templates/home.html, version: v0201).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{% extends "layout.html" %}
{% block title %}Home{% endblock %}
{% block content %}
    <h1>My movie collection</h1>
    <p>Have a nice {{ day }}!</p>

    <ul>
      <li><a href="/movies">List movies</a></li>
    </ul>
{% endblock %}

Let’s also add the following rule to the style sheet to format the footer:

footer {
    text-align: center;
}

2.2. URL Generation

In our HTML markup we’ve used fixed routes when writing links to pages:

  • / for the home page
  • /movies for the movies page
  • /static/mymovies.css for the style sheet

These routes assume that all our URLs start from the base of the server URL (http://localhost:8080). But in some cases -for example if we want to run multiple applications on the same port- we may want our application URLs to start from a subpath on the server, like http://localhost:8080/myapp. So we would want our URLs to be:

  • http://localhost:8080/myapp/
  • http://localhost:8080/myapp/movies
  • http://localhost:8080/myapp/static/mymovies.css

But using fixed routes wouldn’t work in this case because for example /movies would always link to http://localhost:8080/movies.

We can solve this issue using a Flask function called url_for which lets us look up the URL of a page in the application. Routing rules consist of three components:

  1. The route in the application, e.g. /movies.
  2. The view function that will handle the requests to this route, e.g. movies_page.
  3. The name of the endpoint, which is an identifier for the route. If not given, this will be the same as the name of the view function, e.g. movies_page. [1]

So for example, instead of writing the URL for the movies page as /movies, we call the url_for function and give it the endpoint name (movies_page) to get its URL (Listing 2.3, line 8).

Listing 2.3 Home page template with URL generation (file: templates/home.html, version: v0202).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{% extends "layout.html" %}
{% block title %}Home{% endblock %}
{% block content %}
    <h1>My movie collection</h1>
    <p>Have a nice {{ day }}!</p>

    <ul>
      <li><a href="{{ url_for('movies_page') }}">List movies</a></li>
    </ul>
{% endblock %}

For static files, we have to use the endpoint name static and give the relative path of the requested file (Listing 2.4, lines 6-7).

Listing 2.4 Base template with URL generation for static files (file: templates/layout.html, version: v0202).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>My movies - {% block title %}{% endblock %}</title>
    <link rel="stylesheet"
          href="{{ url_for('static', filename='mymovies.css') }}"/>
  </head>
  <body>
    {% block content %}{% endblock %}

    <footer>
      <small>&copy; 2015-2018, Kilgore Trout</small>
    </footer>
  </body>
</html>

Another improvement concerns the navigation. We might need links to the home page or the movie list page from many pages in the application. So, having a global navigation mechanism where all such links will be available in all pages would be a good idea.

We’ll add the navigation links as part of a header section in our base template (Listing 2.5, lines 10-17).

Listing 2.5 Base template with navigation links (file: templates/layout.html, version: v0203).
 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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>My movies - {% block title %}{% endblock %}</title>
    <link rel="stylesheet"
          href="{{ url_for('static', filename='mymovies.css') }}"/>
  </head>
  <body>
    <header>
      <nav>
        <ul>
          <li><a href="{{ url_for('home_page') }}">Home</a></li>
          <li><a href="{{ url_for('movies_page') }}">List movies</a></li>
        </ul>
      </nav>
    </header>

    {% block content %}{% endblock %}

    <footer>
      <small>&copy; 2015-2018, Kilgore Trout</small>
    </footer>
  </body>
</html>

Now the home page doesn’t need to add the navigation links anymore (Listing 2.6).

Listing 2.6 Home page template that gets navigation links from base template (file: templates/home.html, version: v0203).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--- /home/uyar/Projects/flask-tutorial/app/v0202/templates/home.html
+++ /home/uyar/Projects/flask-tutorial/app/v0203/templates/home.html
@@ -3,8 +3,4 @@
 {% block content %}
     <h1>My movie collection</h1>
     <p>Have a nice {{ day }}!</p>
-
-    <ul>
-      <li><a href="{{ url_for('movies_page') }}">List movies</a></li>
-    </ul>
 {% endblock %}

Exercise 1

Arrange the movie list page so that it will use the same navigation panel and footer. (Solution)

Exercise 2

Add Bulma to your base template and arrange your pages to use it. Try to achieve a screenshot as given in Fig. 2.2. (Solution)

Screenshot of home page with Bulma.

Fig. 2.2 Home page using Bulma.

2.3. Code Structure

As the project gets larger, we will want to separate the code into multiple source files. For example, in our application, it might be a good idea to move the view functions into a separate module, as given in Listing 2.7.

Listing 2.7 Module that contains the view functions (file: views.py, version: v0204).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from datetime import datetime

from flask import render_template


def home_page():
    today = datetime.today()
    day_name = today.strftime("%A")
    return render_template("home.html", day=day_name)


def movies_page():
    return render_template("movies.html")

Now the question is, how do we register the routes in the application? One way would be to import the app variable from the server.py module and use the route decorator as before:

from server import app

@app.route("/")
def home_page():
    ...

@app.route("/movies")
def movies_page():
    ...

If you don’t like using global variables this way, you can use the add_url_rule method of the application instance when creating the application. It’s also a good idea to handle application creation as a separate function instead of defining a global variable (Listing 2.8, lines 6-12 and 16).

Listing 2.8 Server module for application setup (file: server.py, version: v0204).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from flask import Flask

import views


def create_app():
    app = Flask(__name__)

    app.add_url_rule("/", view_func=views.home_page)
    app.add_url_rule("/movies", view_func=views.movies_page)

    return app


if __name__ == "__main__":
    app = create_app()
    app.run(host="0.0.0.0", port=8080, debug=True)

2.4. Application Configuration

Flask application instances keep their configuration as a mapping in an instance member named config. For example, the application can be configured to run in debug mode using the DEBUG key (line 3), in which case the corresponding parameter isn’t needed in the run method anymore (line 8):

1
2
3
4
5
6
7
8
def create_app():
    app = Flask(__name__)
    app.config["DEBUG"] = True
    ...

if __name__ == "__main__":
    app = create_app()
    app.run(host="0.0.0.0", port=8080)

You can also add your own settings to this mapping. For example, you can make the port number a configuration setting (line 4), get its value or an optional default (line 9) and use it (line 10):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def create_app():
    app = Flask(__name__)
    app.config["DEBUG"] = True
    app.config["PORT"] = 8080
    ...

if __name__ == "__main__":
    app = create_app()
    port = app.config.get("PORT", 5000)
    app.run(host="0.0.0.0", port=port)

It’s common practice to keep the configuration in a separate file. Flask allows you to load the configuration from a Python source file. For example, let us create a settings.py file (Listing 2.9) and put these settings into it:

Listing 2.9 Settings file with debug mode and port (file: settings.py, version: v0205).
1
2
DEBUG = True
PORT = 8080

Now use this file to configure the application (Listing 2.10, line 8):

Listing 2.10 Application configured through settings file (file: server.py, version: v0205).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from flask import Flask

import views


def create_app():
    app = Flask(__name__)
    app.config.from_object("settings")

    app.add_url_rule("/", view_func=views.home_page)
    app.add_url_rule("/movies", view_func=views.movies_page)

    return app


if __name__ == "__main__":
    app = create_app()
    port = app.config.get("PORT", 5000)
    app.run(host="0.0.0.0", port=port)
[1]

When using the @route decorator, you can use the endpoint parameter to change the endpoint name:

@app.route("/movies", endpoint="movies_endpoint")
def movies_page():
    ...