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