3. Data Model

In this chapter, we’ll create the Python classes in our data model, i.e. the classes for movies and movie collections. And then we’ll populate the movie list page with some test data. The resulting page will look as in Fig. 3.1. [1]

Screenshot of movie list page with test data.

Fig. 3.1 Movie list page populated with test data.

3.1. Model Classes

To represent movies in the application, we implement a simple Movie class (Listing 3.1). Initially, a movie has only a title and an optional year.

Listing 3.1 Initial implementation of the movie class (file: movie.py, version: v0301).
1
2
3
4
class Movie:
    def __init__(self, title, year=None):
        self.title = title
        self.year = year

Next, we implement a class to manage our database. It stores the movie objects in a dictionary (line 6) and provides methods for interacting with it, such as for adding or deleting movies (Listing 3.2). To keep track of the movies, it keeps a counter (line 7) that gets incremented whenever a new movie is added to the collection and the next counter value is used as the key of the added movie (lines 10-11). This key value is also returned so that the caller knows which key was assigned (line 12).

Listing 3.2 Initial implementation of the database class (file: database.py, version: v0301).
 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
from movie import Movie


class Database:
    def __init__(self):
        self.movies = {}
        self._last_movie_key = 0

    def add_movie(self, movie):
        self._last_movie_key += 1
        self.movies[self._last_movie_key] = movie
        return self._last_movie_key

    def delete_movie(self, movie_key):
        if movie_key in self.movies:
            del self.movies[movie_key]

    def get_movie(self, movie_key):
        movie = self.movies.get(movie_key)
        if movie is None:
            return None
        movie_ = Movie(movie.title, year=movie.year)
        return movie_

    def get_movies(self):
        movies = []
        for movie_key, movie in self.movies.items():
            movie_ = Movie(movie.title, year=movie.year)
            movies.append((movie_key, movie_))
        return movies

Note that the implementation returns newly created movie objects instead of the ones in the collection. This is to make sure that the data can not be modified by the caller. And the result of the get_movies method is a list of pairs where each pair consists of the movie’s key and the movie object itself.

When creating the application (Listing 3.3), we also initialize the database (line 15). Since we don’t have a data source to supply us with movie data at the moment, we add some sample data so that we can test the application (lines 16-17). And we store the database object in the configuration to make it accessible to all components in the application (line 18).

Listing 3.3 Application creation with data store (file: server.py, version: v0301).
 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
from flask import Flask

import views
from database import Database
from movie import Movie


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)

    db = Database()
    db.add_movie(Movie("Slaughterhouse-Five", year=1972))
    db.add_movie(Movie("The Shining"))
    app.config["db"] = db

    return app


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

3.2. Control Flow in Templates

Now that we have a database in the application, the movies_page view function can send movie data to the template to be displayed (Listing 3.4). Since the database is stored in the application’s configuration we need a way of accessing the application object from the view function. We do this by importing current_app (line 3). After that, the database can be accessed through the configuration (line 13).

Listing 3.4 Movies page handler getting data from application (file: views.py, version: v0301).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from datetime import datetime

from flask import current_app, render_template


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


def movies_page():
    db = current_app.config["db"]
    movies = db.get_movies()
    return render_template("movies.html", movies=sorted(movies))

We pass the list of (movie key, movie) pairs that are returned by the get_movies method to the template in sorted order (line 15). Since the default sorting mechanism on pairs sorts on the first element, the movies will be sorted in ascending order of their keys.

To display the movies, we need an iteration mechanism in the movie list template (Listing 3.5). The {% for ... %} and {% endfor %} directives let us iterate over the elements of a collection and generate the markup between these directives for each element. In our example, this will generate a tr element for each movie in the movies sequence (lines 8-15).

For every movie, we want to show the year in parentheses if the year is set; but if not, we don’t want to display empty parentheses. So we use an {% if ... %} directive to include that markup only if the year attribute of the movie contains some data (line 12). Also, we don’t want to generate an empty table element if there are no movies in the collection (lines 6 and 17).

Listing 3.5 Movie list template using iteration (file: templates/movies.html, version: v0301).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{% extends "layout.html" %}
{% block title %}Movie list{% endblock %}
{% block content %}
    <h1 class="title">Movies</h1>

    {% if movies %}
    <table class="table is-striped is-fullwidth">
      {% for movie_key, movie in movies %}
      <tr>
        <td>
          {{ movie.title }}
          {% if movie.year %} ({{ movie.year }}) {% endif %}
        </td>
      </tr>
      {% endfor %}
    </table>
    {% endif %}
{% endblock %}

3.3. Parametric Routes

Next, we want to add a page that will display a movie. To identify a movie in the collection, we will use its key value as part of its URL. For example, the route /movies/1 will refer to the movie with key 1. The movie page will look like in Fig. 3.2.

Screenshot of movie display page.

Fig. 3.2 The movie display page.

Let’s start with the movie page template. Assuming it gets the movie object as a parameter named movie, the template code will be as in Listing 3.6.

Listing 3.6 Movie page template (file: templates/movie.html, version: v0302).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{% extends "layout.html" %}
{% block title %}Movie{% endblock %}
{% block content %}
    <h1 class="title">Movie</h1>

    <table class="table">
      <tr>
        <th>Title:</th>
        <td>{{ movie.title }}</td>
      </tr>
      {% if movie.year %}
      <tr>
        <th>Year:</th>
        <td>{{ movie.year }}</td>
      </tr>
      {% endif %}
    </table>
{% endblock %}

The view function (Listing 3.7, lines 18-21) takes the movie key as a parameter and uses it to get the movie from the database and send it to the template.

Listing 3.7 Movie page view function with movie key parameter (file: views.py, version: v0302).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from datetime import datetime

from flask import current_app, render_template


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


def movies_page():
    db = current_app.config["db"]
    movies = db.get_movies()
    return render_template("movies.html", movies=sorted(movies))


def movie_page(movie_key):
    db = current_app.config["db"]
    movie = db.get_movie(movie_key)
    return render_template("movie.html", movie=movie)

For this to work, we have to tell Flask to send the movie key part of the route (e.g. the 1 in /movies/1) as a parameter to the handler. Also note that we have to make sure that the movie key is sent as an integer and not as a string. Flask handles such parametric routes by marking parts of the route in angular brackets. The added line in Listing 3.8 states that the second part of the URL will be mapped to the movie_key parameter in the function after getting converted to an integer.

Listing 3.8 URL setup with parametric route (file: server.py, version: v0302).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--- /home/uyar/Projects/flask-tutorial/app/v0301/server.py
+++ /home/uyar/Projects/flask-tutorial/app/v0302/server.py
@@ -11,6 +11,7 @@
 
     app.add_url_rule("/", view_func=views.home_page)
     app.add_url_rule("/movies", view_func=views.movies_page)
+    app.add_url_rule("/movies/<int:movie_key>", view_func=views.movie_page)
 
     db = Database()
     db.add_movie(Movie("Slaughterhouse-Five", year=1972))

Exercise 1

Organize the movie list page so that the entries are links to pages that will display the selected movie. Hint: You will need to check the documentation for the url_for function about handling parametric routes. (Solution)

Exercise 2

Organize the code so that if a movie with the given key doesn’t exist the application will generate an HTTP “Not Found” (404) error. (Solution)

[1]The screenshots after this point assume that the Bulma exercise in the previous chapter has been completed.