7. Solutions to Exercises

7.1. Chapter: Basics

Listing 7.1 Movie list template with link to the home page (file: templates/movies.html, version: v0104a).
 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 - Movie list</title>
    <link rel="stylesheet" href="/static/mymovies.css"/>
  </head>
  <body>
    <h1>Movies</h1>

    <ul>
      <li><a href="/">Home</a></li>
    </ul>
  </body>
</html>

7.2. Chapter: Application Structure

Exercise 1

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

Listing 7.2 Movie list template with navigation and footer (file: templates/movies.html, version: v0203a).
1
2
3
4
5
{% extends "layout.html" %}
{% block title %}Movie list{% endblock %}
{% block content %}
    <h1>Movies</h1>
{% endblock %}

Exercise 2

Add Bulma to your base template and arrange your pages to use it.

Listing 7.3 Base template using Bulma (file: templates/layout.html, version: v0203b).
 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
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>My movies - {% block title %}{% endblock %}</title>
    <link rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css"/>
    <link rel="stylesheet"
          href="{{ url_for('static', filename='mymovies.css') }}"/>
  </head>
  <body>
    <header>
      <nav class="navbar" aria-label="main navigation">
        <div class="navbar-brand">
          <span class="navbar-item">
            <a class="button is-link" href="{{ url_for('home_page') }}">Home</a>
          </span>
          <span class="navbar-item">
            <a class="button is-link" href="{{ url_for('movies_page') }}">List movies</a>
          </span>
        </div>
      </nav>
    </header>

    <main>
      <section class="section">
        <div class="content">
          {% block content %}{% endblock %}
        </div>
      </section>
    </main>

    <footer class="footer">
      <div class="content">
        <small>&copy; 2015-2018, Kilgore Trout</small>
      </div>
    </footer>
  </body>
</html>
Listing 7.4 Home page template using Bulma (file: templates/home.html, version: v0203b).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "layout.html" %}
{% block title %}Home{% endblock %}
{% block content %}
    <section class="hero">
      <div class="hero-body">
        <div class="container has-text-centered">
          <h1 class="title">My movie collection</h1>
          <p class="subtitle">Have a nice {{ day }}!</p>
        </div>
      </div>
    </section>
{% endblock %}
Listing 7.5 Movie list page template using Bulma (file: templates/movies.html, version: v0203b).
1
2
3
4
5
{% extends "layout.html" %}
{% block title %}Movie list{% endblock %}
{% block content %}
    <h1 class="title">Movies</h1>
{% endblock %}
Listing 7.6 Style sheet adjustment for Bulma (file: static/mymovies.css, version: v0203b).
1
2
3
4
5
6
7
8
--- /home/uyar/Projects/flask-tutorial/app/v0203a/static/mymovies.css
+++ /home/uyar/Projects/flask-tutorial/app/v0203b/static/mymovies.css
@@ -1,4 +1,4 @@
-h1 {
+h1.title {
     color: #e9601a;
 }
 

7.3. Chapter: Data Model

Listing 7.7 Movie list page template with links to movie pages (file: templates/movies.html, version: v0302a).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
--- /home/uyar/Projects/flask-tutorial/app/v0302/templates/movies.html
+++ /home/uyar/Projects/flask-tutorial/app/v0302a/templates/movies.html
@@ -8,8 +8,10 @@
       {% for movie_key, movie in movies %}
       <tr>
         <td>
-          {{ movie.title }}
-          {% if movie.year %} ({{ movie.year }}) {% endif %}
+          <a href="{{ url_for('movie_page', movie_key=movie_key) }}">
+            {{ movie.title }}
+            {% if movie.year %} ({{ movie.year }}) {% endif %}
+          </a>
         </td>
       </tr>
       {% endfor %}

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.

Listing 7.8 Movie view function that generates the 404 error if movie key is not found (file: views.py, version: v0302b).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
--- /home/uyar/Projects/flask-tutorial/app/v0302a/views.py
+++ /home/uyar/Projects/flask-tutorial/app/v0302b/views.py
@@ -1,6 +1,6 @@
 from datetime import datetime
 
-from flask import current_app, render_template
+from flask import abort, current_app, render_template
 
 
 def home_page():
@@ -18,4 +18,6 @@
 def movie_page(movie_key):
     db = current_app.config["db"]
     movie = db.get_movie(movie_key)
+    if movie is None:
+        abort(404)
     return render_template("movie.html", movie=movie)

7.4. Chapter: Forms

Exercise 1

Add a link to the movie display page which, when clicked, will take the user to the movie edit page. The URL of the edit page has to have the extra path /edit after the movie display page URL. That means, for example, the edit page of movie with key 1 has to be /movies/1/edit. After saving, the movie has to be updated in the collection, and not added a second time.

Add the URL rule for the movie edit page:

Listing 7.9 App creation with new URL rule for movie editing (file: server.py, version: v0402a).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    app.add_url_rule("/", view_func=views.home_page)
    app.add_url_rule(
        "/movies", view_func=views.movies_page, methods=["GET", "POST"]
    )
    app.add_url_rule("/movies/<int:movie_key>", view_func=views.movie_page)
    app.add_url_rule(
        "/movies/<int:movie_key>/edit",
        view_func=views.movie_edit_page,
        methods=["GET", "POST"],
    )
    app.add_url_rule(
        "/new-movie", view_func=views.movie_add_page, methods=["GET", "POST"]
    )

Add the link for the edit page to the movie page template:

Listing 7.10 Movie template with link to edit page (file: templates/movie.html, version: v0402a).
 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
{% 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>

    <div class="field is-grouped">
      <div class="control">
        <a class="button is-primary is-outlined is-small"
           href="{{ request.path }}/edit">Edit</a>
      </div>
    </div>
{% endblock %}

We want to use the same template as the one for adding a new movie, but in its current implementation, the template always shows an empty form. This is correct for the new movie case, but if we’re editing an existing movie, we need to be able to send the movie data to the template. We can achieve this by adding value attributes to the input boxes that will be set using the received data (the values dictionary).

Listing 7.11 Movie edit form with default values (file: templates/movie_edit.html, version: v0402a).
 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
{% extends "layout.html" %}
{% block title %}Edit movie{% endblock %}
{% block content %}
    <h1 class="title">Edit movie</h1>

    <form action="" method="post" name="movie_edit">
      <div class="field">
        <label for="title" class="label">Title</label>
        <div class="control">
          <input type="text" name="title" class="input" required="required"
                 value="{{ values['title'] }}"/>
        </div>
      </div>

      <div class="field">
        <label for="year" class="label">Year</label>
        <div class="control">
          <input type="number" name="year" class="input"
                 min="{{ min_year }}" max="{{ max_year }}"
                 value="{{ values['year'] }}"/>
        </div>
      </div>

      <div class="field is-grouped">
        <div class="control">
          <button class="button is-primary is-small">Save</button>
        </div>
      </div>
    </form>
{% endblock %}

The view function for adding a new movie now sends empty data to the template (lines 3-9) whereas the edit page handler gets the movie data from the database and sends that (lines 21-31):

Listing 7.12 Movie add page handler for updated template (file: views.py, version: v0402a).
 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
def movie_add_page():
    if request.method == "GET":
        values = {"title": "", "year": ""}
        return render_template(
            "movie_edit.html",
            min_year=1887,
            max_year=datetime.now().year,
            values=values,
        )
    else:
        form_title = request.form["title"]
        form_year = request.form["year"]
        movie = Movie(form_title, year=int(form_year) if form_year else None)
        db = current_app.config["db"]
        movie_key = db.add_movie(movie)
        return redirect(url_for("movie_page", movie_key=movie_key))


def movie_edit_page(movie_key):
    if request.method == "GET":
        db = current_app.config["db"]
        movie = db.get_movie(movie_key)
        if movie is None:
            abort(404)
        values = {"title": movie.title, "year": movie.year}
        return render_template(
            "movie_edit.html",
            min_year=1887,
            max_year=datetime.now().year,
            values=values,
        )
    else:
        form_title = request.form["title"]
        form_year = request.form["year"]
        movie = Movie(form_title, year=int(form_year) if form_year else None)
        db = current_app.config["db"]
        db.update_movie(movie_key, movie)
        return redirect(url_for("movie_page", movie_key=movie_key))

Finally, for the update operation in the view function (line 37 above), we have to implement the movie update operation in the database:

Listing 7.13 Database class with movie update operation (file: database.py, version: v0402a).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--- /home/uyar/Projects/flask-tutorial/app/v0402/database.py
+++ /home/uyar/Projects/flask-tutorial/app/v0402a/database.py
@@ -10,6 +10,9 @@
         self._last_movie_key += 1
         self.movies[self._last_movie_key] = movie
         return self._last_movie_key
+
+    def update_movie(self, movie_key, movie):
+        self.movies[movie_key] = movie
 
     def delete_movie(self, movie_key):
         if movie_key in self.movies:

Exercise 2

Replace the input validation mechanism with an implementation that uses the Flask-WTF plugin. Also make use of its CSRF protection features. For handling number type inputs, use the wtforms_components package.

Install Flask-WTF and wtforms_components. On your setup, you might need to put these into the requirements.txt file.

Listing 7.14 Movie edit form class with validation (file: forms.py, version: v0403a).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, NumberRange, Optional
from wtforms_components import IntegerField

from datetime import datetime


class MovieEditForm(FlaskForm):
    title = StringField("Title", validators=[DataRequired()])

    year = IntegerField(
        "Year",
        validators=[
            Optional(),
            NumberRange(min=1887, max=datetime.now().year),
        ],
    )
Listing 7.15 Handlers for WTF forms (file: views.py, version: v0403a).
 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
def movie_add_page():
    form = MovieEditForm()
    if form.validate_on_submit():
        title = form.data["title"]
        year = form.data["year"]
        movie = Movie(title, year=year)
        db = current_app.config["db"]
        movie_key = db.add_movie(movie)
        return redirect(url_for("movie_page", movie_key=movie_key))
    return render_template("movie_edit.html", form=form)


def movie_edit_page(movie_key):
    db = current_app.config["db"]
    movie = db.get_movie(movie_key)
    form = MovieEditForm()
    if form.validate_on_submit():
        title = form.data["title"]
        year = form.data["year"]
        movie = Movie(title, year=year)
        db.update_movie(movie_key, movie)
        return redirect(url_for("movie_page", movie_key=movie_key))
    form.title.data = movie.title
    form.year.data = movie.year if movie.year else ""
    return render_template("movie_edit.html", form=form)
Listing 7.16 Setting for CSRF and cookie protection (file: settings.py, version: v0403a).
1
2
3
4
DEBUG = True
PORT = 8080
SECRET_KEY = "secret"
WTF_CSRF_ENABLED = True
Listing 7.17 Movie editing template for WTF forms (file: templates/movie_edit.html.py, version: v0403a).
 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
{% extends "layout.html" %}
{% block title %}Edit movie{% endblock %}
{% block content %}
    <h1 class="title">Edit movie</h1>

    <form action="" method="post" name="movie_edit">
      {{ form.csrf_token }}

      <div class="field">
        <label for="title" class="label">Title</label>
        <div class="control">
          {{ form.title(required=True, autofocus=True, class='input') }}
        </div>
        {% for error in form.title.errors %}
        <p class="help has-background-warning">
            {{ error }}
        </p>
        {% endfor %}
      </div>

      <div class="field">
        <label for="year" class="label">Year</label>
        <div class="control">
          {{ form.year(class='input') }}
        </div>
        {% for error in form.year.errors %}
        <p class="help has-background-warning">
            {{ error }}
        </p>
        {% endfor %}
      </div>

      <div class="field is-grouped">
        <div class="control">
          <button class="button is-primary is-small">Save</button>
        </div>
      </div>
    </form>
{% endblock %}