Blow Up by Black Swan

Python-Flask で作る Web アプリケーション(チュートリアル編4)

チュートリアルに関する4本目の記事です。

参考サイト: Flask – Tutorial(公式ドキュメント)

5. テンプレート

アプリケーションの認証用view関数を記述しましたが、サーバを起動して、いずれかのURLにアクセスしてもTemplateNotFoundエラーが発生すると思います。これはview関数がrender_template()を呼び出しているにも関わらず、まだテンプレートが作成されていないからです。テンプレートファイルは、flaskrパッケージのtemplatesディレクトリに格納します。

テンプレートとは、静的データと動的データを置き換えるプレースホルダーをもつファイルである。テンプレートは、完成版のドキュメントを生成するための特定のデータとともに読み込まれます。Flaskはテンプレートを読み込むためにJinjaテンプレートライブラリを使用します。

アプリケーション内ではユーザのブラウザで表示するHTMLを読み込むためにテンプレートを使います。Flaskで、JinjaライブラリはHTMLテンプレートで読み取られるいかなるデータもエスケープ処理するよう設定されています。これによりユーザの入力値を安全に読み込むことができます。<>のような、ユーザの入力値とHTMLと混ざる文字は、安全な値にエスケープされます。ブラウザ上では同じように見えますが、これで望まない結果が生まれにくくなります。

JinjaはたいていPythonのようにふるまいます。テンプレートの静的データとJinjaのシンタックスを区別するために特別な区切り文字が使われます。{{}}の間のものは、最終形のドキュメントに挿入される、変数などのデータです。{%%}は、ifforのようなコントロールフローです。Pythonと異なり、ブロックはインデントではなく、始まりと終わりのタグによって示されます。これはブロック内の静的テキストがインデントを変更しうるからです。

5-1. ベースレイアウト: base.html

アプリケーションのそれぞれのページは、bodyが異なるとはいえ、同じようなレイアウトを持ちます。それぞれのテンプレートでHTML全てを記述する代わりに、それぞれのテンプレートは基本となるテンプレートを拡張し、特定のセクションを上書きします。

# flaskr/templates/base.html
<!doctype html>
<title>{% block title %}{% endblock %} - Flaskr</title>                        {# 1.ブラウザタブやウインドウに表示されるタイトル #}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

<nav>
  <h1>Flaskr</h1>
  <ul>
    {% if g.user %}
      <li><span>{{ g.user['username'] }}</span>
      <li><a href="{{ url_for('auth.logout') }}">Log Out</a>
    {% else %}
      <li><a href="{{ url_for('auth.register') }}">Register</a>
      <li><a href="{{ url_for('auth.login') }}">Log In</a>
    {% endif %}
  </ul>
</nav>

<section class="content">
  <header>
    {% block header %}{% endblock %}                                             {# 2.ページのヘッダー #}
  </header>
  {% for message in get_flashed_messages() %}
    <div class="flash">{{ message }}</div>
  {% endfor %}
  {% block content %}{% endblock %}                                              {# 3.ページのコンテンツ #}
</section>

gはテンプレート内で自動で使うことができます。g.userがセットされているか(load_logged_in_user)に基づいて、ユーザネームやログアウトリンクが表示されたり、登録やログインへのリンクが表示されます。url_for()もまた、自動で利用でき、手動でURLを記述する代わりにview関数へのURLを生成するために使用されます。

ページタイトルの後でかつコンテンツの前のテンプレートは、get_flashed_messages()で返された各メッセージをループ処理します。view関数の中でエラーメッセージを表示するためにflash()を使いましたが、ループ処理はそのメッセージを表示するための処理です。

ここで定義されたブロックは3つあり、他のテンプレートで上書きされます。

  1. {% block title %}は、ユーザのブラウザタブやウインドウタイトルで表示されるタイトルを変更する。
  2. {% block header %}は、titleに類似しているが、ページ上で表示されるタイトルを変更する。
  3. {% block content %}は、ログインフォームやブログの投稿といった各ページのコンテンツが挿入される場所。

ベースとなるテンプレートはtemplatesディレクトリの中に直接配置されます。他のテンプレートをわかりやすく管理するために、blueprint用のテンプレートはblueprintと同じ名前を持つディレクトリの中に配置します。

5-2. テンプレート: register.html

# flaskr/templates/auth/register.html
{% extends 'base.html' %}

{% block header %}                                                  {# base.htmlの"{% block header %}"に埋め込まれる #}
  <h1>{% block title %}Register{% endblock %}</h1>                  {# base.htmlの"title"タグと共有される #}
{% endblock %}

{% block content %}                                                 {# base.htmlの "{% block content %}"に埋め込まれる #}
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>                  {# 入力必須項目 #}
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>  {# 入力必須項目 #}
    <input type="submit" value="Register">
  </form>
{% endblock %}

{% extends 'base.html' %}は、Jinjaにこのブロックをベーステンプレート(base.html)に置き換えるよう指示するものです。読み込まれたコンテンツの全てが、ベーステンプレートで書き換えられて、{% block %}の場所に埋め込まれます。

ここで行われた便利な方法は、{% block title %}{% block header %}におくことです。これはタイトルブロックをセットし、その値をヘッダーのブロックの中に吐き出します。これにより、ウインドウもページもともに同じタイトルをシェアし、タイトルブロックを二度書く必要がなくなります。

inputタグはここでrequired属性を使っています。これはこのフィールドの入力を必須とし、入力されずにデータが送信されるのを防ぎます。ユーザがこの属性をサポートしていない古いブラウザを使っている場合、またはブラウザ以外の方法でリクエストを送る場合、Flaskのview関数の中で送られたきたデータを検証する必要があります。クライアント側で検証していたとしても、サーバ上でしっかりとデータを検証することは重要です。

tutorial2

5-3. テンプレート: Login.html

このテンプレートは、タイトルとサブミットボタン以外はregisterテンプレートと同一です。

# flaskr/templates/auth/login.html

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Log In">
  </form>
{% endblock %}
tutorial3

5-4. ユーザーの登録

これで認証用テンプレートが記載されたため、ユーザの登録を行うことができるようになりました。サーバが起動していることを確認し(flask runを実行)、URLを入力すると上記の画面がそれぞれ表示されます。

フォームを満たさずに”Register”ボタンを押すと、ブラウザにエラーメッセージが表示されます。register.htmlテンプレートからrequired属性を削除して”Register”を押すと、エラーメッセージが表示される代わりに、ページがリロードされ、view関数のflash()に格納されたエラーが表示されます。

ユーザネームとパスワードを入力すると、ログインページにリダイレクトされます。ログインページで誤ったユーザネームかパスワードを入力するとflash()によってエラーメッセージが表示されます。ただログインした場合は、まだリダイレクトするindexviewが存在しないので、エラーが発生します。

・register: required属性が設定されている場合

tutorial4

・register: required属性が設定されていない場合

tutorial5

・login: 不正確なユーザ情報を入力した場合

tutorial6

6. Staticファイル

今のところ認証用view関数とテンプレートが機能しましたが、とても蛋白なサイトです。構築したHTMLのレイアウトにスタイルを加えるため、CSSが使われます。スタイルは変わらないため、テンプレートよりも静的ファイルとして扱われます。

Flaskはflaskr/staticから相対パスを取るstaticビュー(CSS等の静的ファイル)を自動的に追加する機能を提供しています。base.htmlテンプレートには既にstyle.cssファイルへのリンクが記載されています。

{{ url_for('static', filename='style.css') }}

他のタイプの静的ファイルには、CSSの他にJavaScriptの関数やロゴイメージなどがあります。これらはすべて、flaskr/staticの下に格納されることで、url_for('static', filename='...')で参照できます。

チュートリアルでは、CSSの記法についてはフォーカスしないため、flaskr/static/style.cssファイルに下記のCSSをコピーして下さい。

# flaskr/static/style.css

html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul  { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }

githubのページstyle.cssのコンパクトでない(一般的な記法の)バージョンを見ることができます。

“http://127.0.0.1:5000/auth/login”にアクセスすると次のスクリーンショットのようなページが表示されます。

tutorial7

MozillaのドキュメンテーションでCSSについて多くのことを知ることができます。CSSを変更した場合、ブラウザ上のページを更新する必要がありますが、変更が反映されない場合、ブラウザのキャッシュを削除すると反映されます。

チュートリアル5