下町エンジニアの雑多ブログ

東京の端っこでITエンジニアやってます。テックブログを中心に書いていきたいと思います

Flask入門~公式ドキュメントを読み解く~ Day 6 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、MVTモデルのテンプレートについてみていきました。具体的には、画面描画のためのhtmlをJinjaテンプレートを使って実装し、CSSでスタイルを整えました。

前回までの記事で、基本的なMVTモデルについて全て説明したことになります。なので、今回はそれらの総復習ということで、今回のアプリの中心機能であるブログ表示/投稿/更新/削除機能を一気に作っていきます。

ブログ各機能の実装

Blueprint

まずはじめにブログBlueprintを実装していきます。認証用Blueprintでやったこととほとんど同じなので、簡単ですね!flaskrディレクトリの中に、blog.pyを作成します。

from flask import (
    Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort

from flaskr.auth import login_required
from flaskr.db import get_db

bp = Blueprint('blog', __name__)

ただし、認証用Blueprintと1箇所だけ違う点としては、ブログBlueprintはurl_prefixを指定していません。認証用Blueプリントでは、url_prefixにauthを設定していたため、/auth/registerや/auth/loginのようにアクセスしていましたが、ブログBlueprintでは/createや/updateのように各ビューにアクセスします。 これは、ブログ機能がこのアプリケーションのメイン機能であるためこのような設定としています。

ブログBlueprintの実装が完了したら、これをアプリケーションに登録していきます。

__init__.pyの最後のreturnの前に以下を入力します。

    from . import blog
    app.register_blueprint(blog.bp)
    app.add_url_rule('/', endpoint='index')

    return app    # 最終行

またここで1点注意なのですが、認証用Blueprintの時にはなかったapp.add_url_ruleがあります。app.add_url_ruleは、endpoint引数で指定した値とurl_forの引数が一致する場合、URLを第一引数に変換します。すなわち、url_for('index')は'/'に変換されます。url_for('index')はloginビューとlogoutビュー中に登場しています。以下の3つは表現は違いますが、indexページにリダイレクトするという挙動は同じになります。

redirect(url_for('index'))    # url_for('index')で生成されるURLを'/'に変換
redirect(url_for('blog.index'))    # url_for('index')で生成されるURLを'/'に変換
redirect('/')    # 直接'/'にリダイレクト

トップ画面表示機能

続いてインデックスビューとインデックステンプレートを作成します。blog.pyの最終行に以下を追加します。

@bp.route('/')
def index():
    db = get_db()
    posts = db.execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' ORDER BY created DESC'
    ).fetchall()
    return render_template('blog/index.html', posts=posts)

indexビューではDBからユーザーの記事を抽出し、抽出結果をindexテンプレートに渡しています。

templatesディレクトリにblogディレクトリを作り、index.htmlを作成します。

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Posts{% endblock %}</h1>
  {% if g.user %}
    <a class="action" href="{{ url_for('blog.create') }}">New</a>
  {% endif %}
{% endblock %}

{% block content %}
  {% for post in posts %}
    <article class="post">
      <header>
        <div>
          <h1>{{ post['title'] }}</h1>
          <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
        </div>
        {% if g.user['id'] == post['author_id'] %}
          <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
        {% endif %}
      </header>
      <p class="body">{{ post['body'] }}</p>
    </article>
    {% if not loop.last %}
      <hr>
    {% endif %}
  {% endfor %}
{% endblock %}

特段目新しい箇所はないかと思いますが、1箇所あげるとすれば、loop.lastでしょうか。これは、最後のループか否かを判定してくれます。ループ処理がまだ続くのであれば、記事と記事の間に水平線を引いて見やすくしています。

投稿機能

続いて投稿機能です。createビューをblog.pyの最終行に追加してください。

@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'INSERT INTO post (title, body, author_id)'
                ' VALUES (?, ?, ?)',
                (title, body, g.user['id'])
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/create.html')

ここでは@login_requiredを使用しています。これは、認証用Blueprintで作成したlogin_requiredメソッドを呼び出しています。ブログ記事の投稿なので、登録したユーザーでログインしている必要があります。ビューの前に@login_requiredとしておくことで、ビューメソッドに入る前にlogin_requiredを呼び出し、ログインしていなければ、loginビューにリダイレクトさせることができます。@login_requiredを使用するためには、flask.authモジュールのlogin_requiredをインポートしておく必要があることに注意してください。

ブログ投稿ページとして、template/blog/create.htmlを作成します。

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title" value="{{ request.form['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
{% endblock %}

更新機能

次に更新機能です。投稿した記事を修正できるようにします。blog.pyの最終行に以下を追加します。

def get_post(id, check_author=True):
    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' WHERE p.id = ?',
        (id,)
    ).fetchone()

    if post is None:
        abort(404, "Post id {0} doesn't exist.".format(id))

    if check_author and post['author_id'] != g.user['id']:
        abort(403)

    return post

@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
    post = get_post(id)

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'UPDATE post SET title = ?, body = ?'
                ' WHERE id = ?',
                (title, body, id)
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/update.html', post=post)

まずはじめにget_postメソッドを定義しています。これは引数に投稿idを受け、そのidの投稿を取得します。ここでabortメソッドを使用していますが、これはHTTPステータスの例外をあげることができます。引数で受けたidで投稿が取得できない場合は404エラー(“Not Found”を表す)、投稿のauthor_idとログインユーザーのidが一致しない場合は403エラー("Forbidden” : 閲覧禁止を表す)を返します。

get_postメソッドの下にupdateビューを定義していますが、updateビューはこれまでのビューとは異なり、引数をとります。引数のidには@bp.route('/<int:id>/update', methods=('GET', 'POST'))のidが入ります。そのため、updateビューを呼び出すためのURLは"/1/update"のようにidを渡す必要があることに注意してください。これを実現しているのが、index.htmlの<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>の部分です。

続いてcreateテンプレートです。templates/blog/create.htmlを作成します。

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title"
      value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
  <hr>
  <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
  </form>
{% endblock %}

このテンプレートは、2つのフォームで形成されています。1つ目のフォームは投稿の編集機能で、タイトル、本文を入力します。2つ目のフォームはdeleteビューへのPOSTアクションボタンを作成します。このボタンは、送信する前に確認ダイアログを表示するためにJavaScriptを使用しています。

<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>{{ request.form['body'] or post['body'] }}では、フォームに本文が入力されていれば、request.form['body'] が、何も入力されていなければpost['body']が表示されます。

削除機能

最後の最後にdeleteビューを作成します。delete機能はテンプレートを持たないので、ビューのみとなります。blog.pyの最終行に以下を追加します。

@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
    get_post(id)
    db = get_db()
    db.execute('DELETE FROM post WHERE id = ?', (id,))
    db.commit()
    return redirect(url_for('blog.index'))

まとめ

ここまででflaskrアプリケーションの全機能が完成です。長かったですね。 基本的な使用法は学べたので、これらの知識を使って実際にアプリケーションを作ってみます。

Flask入門~公式ドキュメントを読み解く~ Day 5 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、Blueprintを使用して認証機能の実装を行いました。

今回はMVTモデルのTにあたる、Templateの実装を行います。

テンプレートの作成

前回の記事でも少し触れましたが、テンプレートはWebアプリケーションの画面描画を担当します。作成すべきものはhtmlファイルになります。Flaskでは、Jinjaというテンプレートライブラリを使用することで、htmlを動的に生成します。(余談ですが、Jinjaの名前の由来はTemplateがTempleに似ているからだそうです。Templeは神社ではなく寺なのですが・・・)

前回のauth.pyの中でrender_templateメソッドを使用しました。render_templateの引数で指定したhtmlを呼び出すことができます。

Jinjaの構文はシンプルで、変数のレンダリングには{{ 変数名 }} を使い、if や for などの制御構文は{% if %}のように使います。また、Flaskではアプリケーション配下のtemplatesディレクトリに入っているファイルは自動的にテンプレートファイルだと認識します。そのため、先にflaskr配下にtemplatesフォルダを作ってしまいましょう。

まずは全てのベースとなるテンプレートである、base.htmlをtemplatesディレクトリ配下に作成します。

<!doctype html>
<title>{% block title %}{% endblock %} - Flaskr</title>
<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 %}
  </header>
  {% for message in get_flashed_messages() %}
    <div class="flash">{{ message }}</div>
  {% endfor %}
  {% block content %}{% endblock %}
</section>

base.htmlには、<!doctype html>などのhtml宣言や、参照するCSSやJSについて、ヘッダー・フッダーなどの全ページで共通の内容を記述します。 基本的な文法はhtmlと同じですが、ところどころに先に説明した{{ }}や{% %}が出てきます。ここで特に重要なのは、以下の3つになります。

  • {% block title %}{% endblock %} : 他ページのtitleブロックで置き換え
  • {% block header %}{% endblock %} : 他ページのheaderブロックで置き換え
  • {% block content %}{% endblock %} : 他ページのcontentブロックで置き換え

これらのブロックはbase.htmlを参照するページ内に同じ名前のブロックがあれば、base.htmlのブロックがそのブロックに置き換えられます。下線を引いたブロック中の名前は自由に決めることができますが、参照元と先で揃えておく必要があります。

では次にbase.htmlを使用する登録画面を作成します。template/auth/register.htmlを作成してください。

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Register{% 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="Register">
  </form>
{% endblock %}

1行目の{% extends 'base.html' %}によって、base.htmlのブロックをこのテンプレートのブロックで置き換えることができます。次の{% block header %}では、その中に{% block title %}を含んでいます。このように、ブロックを入れ子にすることも可能です。 {% block content %}ではフォームタグでユーザー情報入力欄とサブミットボタンを作成しています。register.htmlが呼び出されると、base.htmlのブロックがregister.htmlのブロックで置き換えられるので、実際には以下のhtmlが作成されることになります。

<!doctype html>
<title>Register- Flaskr</title>
<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>
    <h1>Register</h1>
  </header>
  {% for message in get_flashed_messages() %}
    <div class="flash">{{ message }}</div>
  {% endfor %}
    <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>
</section>

続いてログイン画面も一気に作ってしまいます。template/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 %}

register.htmlとやっていることは同じですね。説明は割愛します。

登録とログインの画面ができたので、ここで一旦アプリを起動させてブラウザから接続してみましょう。flaskrの1つ上の階層(本記事に沿っているならflask-tutorial)で以下のコマンドを入力し、アプリを起動します。

export FLASK_APP=flaskr
export FLASK_ENV=develop
flask run --host=0.0.0.0

起動が完了したら、ブラウザからhttp://192.168.33.10/auth/registerに接続します。うまくいっていれば以下のような画面が表示されるはずです。ログインリンクからログインページも表示してみましょう。

f:id:usktkt:20181025225002p:plain

登録画面とログイン画面の作成が完了しましたが、このままでは少し味気ないですね・・・ ということで、続いて画面のスタイルを整えていきます。

スタイルシートの作成

皆様ご存知の通り、Webページのスタイルを整えるためには、CSSを使います。flaskでCSSを使うには、base.htmlに<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">を記載することで、flaskr/staticディレクトリのstyle.cssを参照するようになります。今回は扱いませんが、JavaScriptを使用したい場合も同様に<link rel="stylesheet" href="{{ url_for('static', filename='ファイル名.js') }}">のようにすれば使用できます。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; }

これでスタイルシートが適用されたはずなので、もう一度ブラウザから接続してみましょう。 正しくできていれば以下のように表示されます。

f:id:usktkt:20181025231513p:plain

まとめ

今回は登録、ログインテンプレートの作成と、CSSの適用を行いました。 次回はいよいよこのアプリのメイン機能であるブログ投稿機能、記事の編集・削除機能を作成していきます。 前回の記事のBlueprintから今回の記事のテンプレートまで含んだ盛りだくさんの内容となりますので、今一度復習しておきましょう。

Flask入門~公式ドキュメントを読み解く~ Day 4 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、データベース周りの設定と、初期化を行いました。

今回はBlueprintとビューを実装していきます。

MVTモデルについて

Webアプリケーションについて勉強したことのある方なら、MVCモデルというワードを聞いたことがあると思います。これはModel、View、Controllerの頭文字で、Ruby on Railsはこの形式を取っています。ざっくり説明するとそれぞれの役割は以下のようになります。

  • Model:ビジネスロジックを記述。データベースアクセスを伴うことが多い
  • View:画面描画を担当。ユーザからの入力も受け付ける
  • Controller:URLを振り分ける司令塔の役割。ビューからのデータをモデルに渡したり、その逆も行う

一方で、FlaskはMVCではなく、MVTという形式を取っています。これは、Model、View、Templateの頭文字となります。 MVCとMVTは、内容はほぼ同じですが、それぞれの役割が異なります。

  • Model : MVCモデルのModelと同じ役割
  • View : MVCモデルのControllerと同じ役割
  • Template : MVCモデルのViewと同じ役割

同じ名前で役割が違うのが、少しややこしいところです。ModelとViewについては今回の記事で、Templateについては次回の記事で見ていくことにします。

Blueprintとは

まずBlueprintとは何かというところからですが、日本語にすると、「青写真」です。「青写真を描く」とか言いますね。設計図や未来図といった意味で使われます。

FlaskでのBlueprintは、機能ごとにファイルを分割してグルーピングしたい場合に使われます。大きなプロジェクトでは機能数が多く、それに伴いコードも膨大になってしまいます。そんな時にそれらを整理して「青写真を描く」といった意味でBlueprintになったのではないかと推測しています。

Blueprintを用いた実装は以下の2段階で行われます。

  1. Blueprintの実装
  2. 実装したBlueprintをFlaskアプリケーションに登録

チュートリアルのflaskrアプリケーションでは、認証機能とブログ投稿機能の2つのBlueprintを作成します。

認証用Blueprintの実装

まずは認証用Blueprintを実装していきます。flaskrディレクトリ配下に、auth.pyファイルを作成し、以下を入力してください。

import functools
from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')    # Blueprintの実装

これにより、'auth'という名前のBlueprintが生成されます。Blueprintは定義された場所を知る必要があるため、第二引数に__name__を指定しています。url_prefixの値は、Blueprintと関連のあるURLの先頭につけられます。

Blueprintを生成したら、次にアプリケーションに登録します。__init__.pyを開き、最終行のreturn app の前に以下を追記します。

    from . import auth
    app.register_blueprint(auth.bp)    # Blueprintをアプリケーションに登録

    return app  # 最終行

app.register_blueprint()の引数にBlueprint名.bpを指定するとアプリケーションに登録することができます。

認証Blueprintは以下の3つのビューを持ちます。

  • Register (ユーザ登録)
  • Login
  • Logout

それぞれを実装していきましょう。

auth.pyの最終行に以下を追記します。

@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']    # ユーザーフォームからusernameを取り出す
        password = request.form['password']     # ユーザーフォームからpasswordを取り出す
        db = get_db()
        error = None

        if not username:    # ユーザ名が空だった場合
            error = 'Username is required.'
        elif not password:    # パスワードが空だった場合
            error = 'Password is required.'
        elif db.execute(
            'SELECT id FROM user WHERE username = ?', (username,)
        ).fetchone() is not None:    # ユーザ名を条件にデータベースからidを取り出し、すでにidが存在する場合
            error = 'User {} is already registered.'.format(username)

        if error is None:    # ユーザ名とパスワードが入力され、既存ユーザ出ない場合
            db.execute(    # ユーザをデータベースに登録
                'INSERT INTO user (username, password) VALUES (?, ?)',
                (username, generate_password_hash(password))    # パスワードはハッシュ化
            )
            db.commit()
            return redirect(url_for('auth.login')) # ログインページにリダイレクト

        flash(error)

    return render_template('auth/register.html')

@bp.routeはURL /registerとビュー関数(register)を関連づけます。Blueprintのurl_prefixに'/auth'を設定したので、/registerの前に/authがついて/auth/registerにリクエストを受け取った際に、戻り値を返すようになります。登録画面ではGETリクエストもPOSTリクエストもあり得るので、第二引数のmethodsにはタプルで'GET', 'POST'を設定しています。

リクエストメソッドが’POST’の場合、ユーザが入力したフォームからusernameとpasswordを取り出します。そしてusernameとpasswordが空でないことを検証し、usernameを条件にDBからidをselectするクエリを投げます。その結果が空でなければ、そのユーザー名は既に使われているということでエラーを返します。

db.execut内の?はプレースホルダと呼ばれ、usernameと置き換えられます。これにより、SQLインジェクションを防ぐことができます。

fetchone()はクエリの結果1行を返します。結果が無い場合はNoneを返すので、if文でerror変数に値が代入されます。

検証に成功すると、insert文が発行され、データベースにユーザーが登録されます。セキュリティの観点から、データベースに平文のままパスワードを格納するのはよろしく無いので、ハッシュ関数を通した状態で保存しておきます。

ユーザーの登録後、ログインページにリダイレクトしています。url_for()メソッドは第一引数のメソッドへのURLを生成します。つまりここでは、authモジュールのloginメソッドへのURLを生成しています。

ユーザー名とパスワードの検証に失敗(どちらかが空など)した場合、エラー内容が表示されます。flash()を使うことで、動的にエラーメッセージを表示することができます。

ユーザーからのリクエストがGETメソッドだった場合、render_template()メソッドでhtmlを返します。htmlの作成は次回の記事で行う予定です。

続いて、ログインビューを作成します。 auth.pyに以下を追記してください。

@bp.route('/login', methods=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()    # DBに接続
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):    # パスワードが誤っていた場合(入力したパスワードをハッシュ化して比較)
            error = 'Incorrect password.'

        if error is None:    # 何もエラーがなかった場合
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

ユーザからのリクエストがPOSTだった場合、フォームから取得したユーザー名を条件にDBにselectクエリを投げます。

そして取得したuser情報の検証を行い、問題がなければ新しいセッションににidを格納します。ここで、check_password_hashメソッドは第二引数で指定したパスワードをハッシュ化し、第一引数のDBから取得したハッシュ化されたパスワードを比較し、一致すればTrue、不一致であればFalseを返します。

セッションにidを格納することができたので、次はこのセッションを使ってリクエスト毎にユーザがログイン状態か否かを判定するメソッドを作成します。 auth.pyに以下を追記します。

@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user = None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

@bp.before_app_requestによって登録されたメソッド(ここではload_logged_in_user)は、ビューメソッド(@bp.routeで登録されたメソッド)が実行される前に実行されるようになります。load_logged_in_userはuser_idがセッションに格納されているかのチェックを行い、user_idが存在する場合DBからユーザ情報を取得し、存在しない場合はNoneを設定します。

ログイン機能ができたので、ログアウト機能も実装します。auth.pyに以下を追記してください。

@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

ログアウトメソッドは非常に簡単で、セッションをクリアして、indexメソッドへリダイレクトさせるだけの実装となっています。

次が最後のメソッドになりますが、今回はブログアプリなので、記事の投稿や削除を行います。記事の投稿や削除を行うにはユーザがログイン状態である必要があります。それらのビューメソッドの前にユーザのログイン状態について確認するメソッドを実装します。auth.pyに以下を追記します。

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view

このメソッドは上記の通り、ブログ記事の投稿や削除のビューメソッドの前に実行され、ユーザがログイン状態でなければログイン画面にリダイレクトさせるものとなります。詳しい使い方についてはブログビューを作成する際に解説しようと思います。

まとめ

今回は少し長くなりましたが、Blueprintの説明や認証周りの実装を行いました。認証の方法はどのアプリケーションでも大きく変わらないので、今回の内容を正しく理解できれば、オリジナルのアプリケーションを作るときにでも使いまわせると思います。

次回は認証時に実際に表示される画面を作っていきたいと思います。

Flask入門~公式ドキュメントを読み解く~ Day 3 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、チュートリアルの完成像を確認し、アプリケーションのセットアップを行いました。

今回は、データベース周りのあれこれをやっていきます。

データベース接続

早速、データベース接続と切断のためのメソッドを作成していきます。flaskrディレクトリ配下に、db.pyというファイルを作成し、以下を入力してください。このファイルにより、__init__.pyで設定したDATABASEへの接続と、切断ができるようになります。

import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext

def get_db():
    if 'db' not in g:
        g.db = sqlite3.connect(
            current_app.config['DATABASE'],
            detect_types=sqlite3.PARSE_DECLTYPES
        )
        g.db.row_factory = sqlite3.Row

    return g.db

def close_db(e=None):
    db = g.pop('db', None)

    if db is not None:
        db.close()

gはリクエスト毎に生成されるユニークなオブジェクトとなります。つまりリクエストの中で初めてDB接続要求があった場合、新たにDBコネクションを生成し、同じリクエストの中で複数回接続要求があった場合、最初に生成したオブジェクトを使いまわします。

current_app.config['DATABASE']は現在実行しているFlaskアプリケーションの設定値'DATABASE'を取り出します。 detect_typesをsqlite3.PARSE_DECLTYPESとしておくことで、戻り値のカラムの型を読み取ることが出来るようになります。 *1

sqlite3.connectによって、current_app.config['DATABASE']で取得した、'DATABASE'キーに設定されたデータベースに接続します。

row_factoryにsqlite3.Rowをセットしておくことで、クエリの結果にカラム名でアクセス出来るようになります。

最後のclose_dbメソッドでは、gオブジェクトから'db'を取り出し、それが空でなければ(接続があれば)、切断します。

テーブル作成

次にテーブルを生成するSQLを作成します。flaskrディレクトリ配下にschema.sqlを作成し、以下を入力してください。なお、SQL文については今回は詳細に解説しないので、不明点がある方は、入門書などで調べてみてください。

DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;

CREATE TABLE user (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL
);

CREATE TABLE post (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  author_id INTEGER NOT NULL,
  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  FOREIGN KEY (author_id) REFERENCES user (id)
);

このSQLファイルを起動するためのメソッドを、db.pyに追加します。db.pyの最終行に以下を追記してください。

def init_db():
    db = get_db()

    with current_app.open_resource('schema.sql') as f:
        db.executescript(f.read().decode('utf8'))

@click.command('init-db')
@with_appcontext
def init_db_command():
    """既存のデータを削除し、新たなテーブルを作成する"""
    init_db()
    click.echo('Initialized the database.')

open_resource()はflaskrディレクトリ内のファイルを開きます。そして次の行で開いたファイルをutf-8でデコードして読み込み実行します。

clickモジュールをインポートすることで、独自のflaskコマンドを作成することが出来ます。@click.command('init-db')によって'init-db'コマンドを定義し、@with_appcontextでflaskrアプリケーションと紐づけています。

アプリケーションへの登録

続いてclose_dbメソッドとinit_db_commandメソッドをアプリケーションに登録します。db.pyの最終行に以下を追加してください。

def init_app(app):
    app.teardown_appcontext(close_db)
    app.cli.add_command(init_db_command)

teardown_appcontextメソッドは、レスポンスを返した後に自動的に引数のメソッドを呼び出します。

app.cli.add_commandにinit_db_commandをセットすることで、flaskコマンドから呼び出すことが出来るようになります。

このinit_appメソッドを__init__.pyモジュールにインポートします。__init__.pyの最終行にあるreturn appの上の行に以下を追記してください。追記するコードは、return appとインデントが等しいことに注意してください。

    from . import db
    db.init_app(app)

    return app  # 最終行

データベースの初期化

それでは最後にデータベースを初期化しましょう。前回起動したサーバーが立ち上がったままの場合、control + Cで一度サーバーを止めましょう。 データベースの初期化は以下のコマンドで実行できます。

flask init-db

このコマンドを打ち込んだ際の挙動を示すと以下のような順で処理が進みます。

  1. __init__.pyが起動し、create_appメソッドが呼び出される。
  2. db.pyモジュールのinit_appメソッドが呼び出される。
  3. __init__.pyの処理が終了後、'init-db'コマンドと紐づけられたinit_db_commandが呼び出される
  4. init_db_commandメソッドからinit_dbメソッドが呼び出され、データベースの接続と、schema.sqlの実行が行われる。
  5. init_dbメソッドの処理が終了すると、文字列'Initialized the database.'が出力され、DB接続を切断し終了。

処理が終わった後に、instanceディレクトリをのぞいてみると、flaskr.sqliteファイルが出力されているはずです。

まとめ

今回はデータベースの接続・切断から、データベースの初期化まで行いました。clickでflaskコマンドを作成するあたりは少し難しいかもしれません。

次回は機能ごとにファイルの分割を可能にするBlueprintについてみていきます。

内容について、質問・指摘があればコメントよろしくお願いします。

Flask入門~公式ドキュメントを読み解く~ Day 2 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、Anacondaのインストールと最初のFlaskアプリケーション作成を行いました。

今回からいよいよ、公式サイトのチュートリアルを進めていきます。*1

完成イメージ

チュートリアルでは、以下のようなブログアプリが作成できます。

f:id:usktkt:20181020213143p:plain
インデックス画面

f:id:usktkt:20181020213227p:plain
ログイン画面

f:id:usktkt:20181020213326p:plain
編集画面

アプリケーションのセットアップ

まずは、アプリケーションのセットアップを行います。Ubuntu上でhomeディレクトリに「flask-tutorial」というディレクトリを作り、その中で「fraskr」というディレクトリを作ります。そして、flaskrディレクトリの中に__init__.pyを作ります。

cd ~
mkdir flask-tutorial
cd flask-tutorial
mkdir flaskr
vi ./flaskr/__init__.py

すると、viで__init__.pyの編集画面に移るので、以下を入力してください。viでの編集が苦手な方は、ホストOS側でAtomVisual Studio Codeなどのエディタでファイルを作成し、CyberDuckのようなFTPクライアントを利用してファイルを転送する方法も可能です。

__init__.pyはflaskrの初期化処理として実行されるので、ここにFlaskインスタンスを生成するメソッドを記述します。また、__init__.pyが配置されたディレクトリは、パッケージとしてみなされるようになります。

import os
from flask import Flask

def create_app(test_config=None):
    # アプリケーションの生成と設定を行う
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
    )

    if test_config is None:
        # testではない場合に、インスタンスの設定値が存在すれば読み込む
        app.config.from_pyfile('config.py', silent=True)
    else:
        # テストの設定を読み込む
        app.config.from_mapping(test_config)

    # instanceフォルダが存在することを保証する
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # helloを返すシンプルなページ
    @app.route('/hello')
    def hello():
        return 'Hello, World!'

    return app

以下、公式サイトの説明を翻訳していきます。直訳で分かりづらい箇所については適宜意訳しています。

  1. app = Flask(__name__, instance_relative_config=True) はFlaskインスタンスを生成する
    • __name__は現在のPythonモジュール名が格納される。アプリケーションは自分が置かれたパスを知る必要があるが、__name__によってそれを知ることができる。
    • instance_relative_config=Trueは、設定ファイルがinstanceフォルダに関連づけられていることを示す。 app.instance_pathはflaskrパッケージの外に配置され、秘密の設定値やデータベースファイルなどのバージョン管理しないローカルデータを保持しておくことができる。
  2. app.config.from_mapping() はアプリケーションが使用するデフォルト設定をセットする:
    • SECRET_KEYはFlaskや拡張機能でデータを安全に扱うために使用される。 ここでは開発用に 'dev'に設定されていますが、デプロイ時にはランダムな値で上書きする必要がある。
    • DATABASEは、SQLiteデータベースファイルが保存されるパスである。それはapp.instanceフォルダの配下にあります。データベースの詳細については次のセクションで学習します。
  3. app.config.from_pyfile()はinstanceフォルダにconfig.pyが存在すれば、デフォルト設定値をそこから得られる値で上書きする。例えば、デプロイの際に実際のSECRET_KEYに置き換えることができる。
    • test_configもアプリケーションに渡すことができ、インスタンスの設定値の代わりに使うことができる。これは、チュートリアルの後半で説明するテストで、開発時の設定値とは別の設定値を使用できるようにするためのものである。
  4. os.makedirs()はapp.instance_path が存在することを保証する。(instanceディレクトリ作成をtryし、ディレクトリが存在しない場合は作成、既に存在している場合は例外が発生しpassされる)
  5. @app.route()はシンプルなルートを生成し、残りのチュートリアルに進む前に動作確認をすることができる。 ここでは、URL/hello と関数を結びつけ、'Hello, World!' という文字列を返す。

ではここで、一度実行してみます。

export FLASK_APP=flaskr    # flaskrアプリケーションを指定する
export FLASK_ENV=development    # developmentモードで実行することを指定する
flask run --host=0.0.0.0    # ホストOSから確認したいので--host=0.0.0.0をつける

developmentモードで実行しておくと、例外が発生した場合に対話型のデバッガーを表示したり、コードを修正した場合に自動でサーバを再起動してくれるなどのメリットがあります。

ここまで出来たら、ホストOSのブラウザからhttp://192.168.33.10:5000/hello にアクセスしてみましょう。Hello, Worldと表示されれば成功となります。

まとめ

今回はチュートリアルの完成イメージの確認と、アプリケーションのセットアップを行いました。

次回はデータベースを作成していきます。

内容について、質問・指摘があればコメントよろしくお願いします。

Flask入門~公式ドキュメントを読み解く~ Day 1 ~

f:id:usktkt:20181019225022p:plain

前回の内容

前回の記事では、Flaskの特徴の説明、なぜFlaskを使うのか、環境のセットアップについて書きました。

今回は、前回構築したUbuntu上にPythonとFlaskをインストールし、最初のFlaskアプリケーションを作りたいと思います。

PythonとFlaskのインストール

公式サイトでは、venvでPythonをインストールしていますが、今回は便利なAnacondaディストリビューションを使用します。

Anacondaがどういったものなのかはこちらを参照してください。

まずはUbuntuに以下のコマンドを入力し、pyenvを使えるようにします。(何をやっているか分からない方は、とりあえずおまじないだと思って入力してもらって大丈夫です。)

git clone https://github.com/yyuu/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
source ~/.bashrc

これでpyenvが使えるようになるので、次にAnacondaをインストールします。 以下のコマンドを入力すると、使えるAnacondaのバージョン一覧が表示されます。

今回は表示された中で最新である、Anaconda3-5.3.0をインストールしますが、 実行したタイミングで最新のものをインストールすれば良いかと思います。

pyenv install -l | grep anaconda3
pyenv install anaconda3-5.3.0

Anacondaのインストールにはかなり時間がかかるので、ここで一息つきながら完了を待ちます。

完了後に以下のコマンドで、Anacondaのバージョンが表示されれば、インストール成功です。念の為、globalコマンドも入力しておきます。これにより、デフォルトのAnacodaのバージョンが5.3.0になります。

pyenv versions
pyenv global anaconda3-5.3.0

Anacondaのインストールに成功すると、以下のコマンドでAnacondaに同梱されているパッケージを確認することができます。なんと、AnacondaにはFlaskが元々含まれているので、旧バージョンのFlaskを使いたい等の理由が無ければ改めてインストールする必要はありません。

conda list

これでやっと環境の完成になります。

それでは、あらゆる言語のはじめの一歩であるHello worldを表示するWebアプリケーションを作っていきましょう。

初めてのFlaskアプリケーション

まずコードの全体を示します。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

たったこれだけで、Webアプリケーションの完成です。これをhello.pyなどの名前で保存してください。

上から解説していきます。

1行目ではFlaskモジュールのインポートを行っています。

2行目でFlaskクラスのインスタンスを生成しています。引数の__name__には現在実行中のモジュールの完全修飾名が格納されます。ただし、実行時のトップレベルのモジュールの場合は"__main__"という文字列が格納されます。ここではhello.pyから実行するため、__name__には__main__が入ることになります。なぜここで__name__が必要かというと、後々出てくるtemplates(htmlを格納)やstatic(CSSやJSを格納)の位置をFlaskに知らせるためです。

4行目の@app.route('/')は、URLが'/'のリクエストを受けた場合にその下のhello_world()メソッドを実行させます。 hello_world()は、'Hello, World'という文字列を返すだけのメソッドです。

では、このアプリケーションを実行し、ブラウザで表示させてみましょう。

export FLASK_APP=hello.py
flask run --host=0.0.0.0

1行目で、実行するファイルを指定しています。

2行目でビルトインサーバが立ち上がり、hello.pyが実行された状態になります。 *1

この状態で、ホストOSのブラウザから、http://192.168.33.10:5000/ *2にアクセスしてみてください。Hello, World!と表示されれば成功です。

まとめ

今回はVagrantUbuntu上にAnacondaをインストールし、最初のFlaskアプリケーションを作成しました。

次回からいよいよ公式サイトのチュートリアルに入っていきます。

*1:--host=0.0.0.0を指定しているのは、外部(ホストOS)から接続するためです。Vagrantを使用せず、ローカルに環境を構築している方は、flask run だけでアクセスできます。

*2:Vagrantのセットアップの際にIPアドレスを変更した場合、ご自身の環境に合わせて変更してください。

Flask入門~公式ドキュメントを読み解く~ Day 0 ~

f:id:usktkt:20181019225022p:plain

はじめに

昨今、機械学習やAIというワードを目にしない日は無いのではないでしょうか。巷ではこれらの技術書や記事がたくさんあり、ちょっとやってみようと試している方も大勢いらっしゃるかと思います。

ただ、これらの技術を試してみたものの、この先どうすればいいのかと迷われている方がたくさんいるような気がしています。

AIの使い道は色々と考えられますが、手っ取り早く応用する先がWebサービスです。 学習したモデルをWebサービスに組み込み公開することで、多くの人に使ってもらうことができます。

機械学習のモデルをWebサービスに組み込むという話の前に、PythonのWebフレームワークであるFlaskについて、公式ドキュメントのチュートリアルを題材に記事を書いていきます。 (Flask入門の記事の完走後に機械学習モデルを組み込む記事も書きたいと思います)

今回の一連の記事は以下の方を対象とします。

  • Pythonを使って機械学習について学習したものの、この先どうしていいかわからない方
  • Flaskを使ってみたいけど、公式ドキュメントが英語で読み進められない方

Flaskとは

Flaskの特徴として、公式サイトにも記載があるように、"microframework"であるということが言えます。これはどういうことかというと、Webアプリケーションを作る上で必要最低限の機能しかありません。色々と機能がある方がいいんじゃないかと思われる方もいるかと思いますが、機能が多いということは、その分知っておかないといけないことが多く、学習コストが大きくなります。また、Flaskは軽量なフレームワークなので、ちょっとしたアプリケーションを作りたい場合に向いています。

Flaskの公式サイトは以下になります。古いバージョンでは一部翻訳されたものがありそうですが、最新バージョンである1.0.2では全て英語です。

Flask公式サイト

なぜFlaskなのか

WebフレームワークといえばRuby on Railsを思い浮かべる方が大多数かと思います。 Ruby on Railsは非常に強力なフレームワークで、簡単にWebサービスが作れてしまいますが、今回は以下の点を考慮し、Flaskを選択しました。

  • Ruby on Railsのコード自動生成は強力すぎて、初学者の方は「よくわからんけどできた」に陥りやすい
  • 機械学習を学習している方はPythonの方が馴染みがある
  • django(Pythonの別のWebフレームワーク)と比べても学習コストが低い

環境セットアップ

今回使用する環境は以下の通りです。

まずはホストマシンにVirtualBoxVagrantをインストールします。 公式からインストールしても良いのですが、Macの場合、homebrew-caskでインストールした方がスマートでしょう。 ターミナルで以下のコマンドを入力します。

brew cask install virtualbox
brew cask install vagrant

Windowsの方、homebrew-caskの使い方が分からない方は公式サイトからダウンロードしてインストールしましょう。

次にUbuntuをインストールしていきます。 ホストOS上に任意のフォルダを作成します。

今回私は、/Users/usktkt/Develop/ubuntu-18.04 というフォルダを作成しました。

フォルダを作成した後に、ターミナルで作成したフォルダに移動します。

そして今回はUbuntu-18.04をインストールするので、以下のコマンドを入力します。

vagrant init ubuntu/bionic64

すると、フォルダにVagrantfileなるものができていると思います。

Vagrantfileをエディタで開くと色々と設定が書いてありますが、ひとまず以下の一行の#を削除するだけで良いかと思います。 Vagrantファイルを編集することでインストールするOSの設定をすることができるので、興味がある方は調べてみてください。

# config.vm.network "private_network", ip: "192.168.33.10"

Vagrantfileの設定が完了したら、以下のコマンドでVagrant Boxのダウンロードとインストールを行います。 ネットワーク環境によってはダウンロードにかなり時間がかかるので、一息つきながら待ちます。

vagrant up

インストールが完了したら、以下のコマンドでUbuntuに接続します。

vagrant ssh

コマンドを入力した後のプロンプトが以下のようになっていればインストール成功です。

vagrant@XXXX:~$

まとめ

今回はこのFlaskに関する記事を書く目的と、Vagrantのセットアップ方法について書きました。 次回はUbuntu上へのPythonとFlaskをインストールし、 最初のFlaskアプリケーションを作ってみたいと思います。