7. Solutions to Exercises¶
7.1. Chapter: Basics¶
Exercise 1
Add a link from the movie list page to the home page.
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.
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.
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>© 2015-2018, Kilgore Trout</small>
</div>
</footer>
</body>
</html>
|
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 %}
|
1 2 3 4 5 | {% extends "layout.html" %}
{% block title %}Movie list{% endblock %}
{% block content %}
<h1 class="title">Movies</h1>
{% endblock %}
|
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¶
Exercise 1
Organize the movie list page so that the entries are links to pages that will display the selected movie.
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.
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:
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:
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).
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):
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:
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.
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),
],
)
|
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)
|
1 2 3 4 | DEBUG = True
PORT = 8080
SECRET_KEY = "secret"
WTF_CSRF_ENABLED = True
|
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 %}
|