チュートリアルに関する記事の5本目です。
参考サイト: Flask – Tutorial(公式ドキュメント)
- チュートリアル
- クイックスタート
7. ブログのblueprint
認証用のblueprintを記述した時と同じ方法でブログのblueprintも記述することができます。ブログページでは、全ての投稿を一覧で表示し、かつログインユーザは新規投稿を作成でき、投稿者は自身の投稿を編集、削除することができます。
それぞれのview関数を実装したように、サーバを走らせ続け、変更するごとにブラウザで変更点を確認します。
7-1. 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__)
blog
をインポートし、app.register_blueprint()
を使ってファクトリーにblueprintを登録します。ファクトリー関数の最後の方で、app
をreturnする前の部分に新しいコードを記述します。
#flaskr/__init__.py
def create_app():
app = ...
# existing code omitted
from . import blog
app.register_blueprint(blog.bp)
app.add_url_rule('/', endpoint='index')
return app
認証用のblueprntと異なり、ブログblueprintはprefix
を持ちません。そのため、インデックスは/
となり、ブログの作成は/create
となります。ブログはFlaskrの主要機能ですので、ブログのインデックスがメインインデックスとなることは理にかなった仕様となります。
しかし、下で定義されるindex
の名称はblog.index
となります。認証用view関数のいくつかは、まっさらなindex
を参照しています。app.add_url_rule()
はエンドポイントindex
とURL/
を関連付けるめため、url_for('index')
とurl_for('blog.index')
は両方とも同じ意味となり、URL/
を生成します。
別のアプリケーションを作る際にブログblueprintにurl_prefix
を付与し、アプリケーションファクトリー内でindex
view関数は別に定義することもあります。このとき、index
とblog.index
は異なるものとなります。
7-2. view関数:Index
インデックスでは、全ての投稿を表示し、最新の投稿が一番上にくる仕様となります。user
テーブルの投稿者情報をpost
テーブルの検索で利用できるようにするため、SQLクエリのJOIN
が使われています。
# flaskr/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)
# flaskr/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> {# createページへのリンクを加える #}
{% 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 %}
ユーザがログインしているときには、header
ブロックにcreate
ページへのリンクボタンが加わります。ログインユーザがブログの投稿者である場合、その投稿にはupdate
ページにつながる”Edit”ボタンが表示されます。loop.last
はJinja for loops内で利用可能な特別な変数です。各投稿を視覚的に区別するために最後の投稿を除くそれぞれの投稿の後にラインを表示するために利用されています。
※ 投稿がある場合は”Posts”の下に表示される
※ ログインされている場合 => 投稿作成用の”New”ボタンが見える
7-3. view関数:Create
create
view関数はregister
view関数と類似しています。フォームの表示、ポストされたデータの検証、その投稿のデータベースへの追加またはエラー表示と行った処理が行われます。
前に記述したlogin_required
デコレータはblogのview関数で使われます。ユーザはblog関連ページにアクセスするにはログインしている必要があり、そうでない場合ログインページにリダイレクトされます。
# flaskr/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')
# flaskr/templates/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 %}
7-4. view関数: Update
update
とdelete
はともに、ユーザのid
を指定してpost
を取得し、記事の投稿者がログインユーザかチェックされる必要があります。コードの重複を避けるためpost
を取得する関数を定義し、それぞれのview関数で呼び出すという方法をとっています。
# flaskr/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
abort()
は、HTTPのステータスコードを返す特別な例外を発生させます。エラーを表示する際にメッセージを表示させることもできます。メッセージを指定しない場合、デフォルトメッセージの”Not Found”を意味する404
や”Forbidden”を意味する403
が使われます(401
は”Unauthorized”を意味しますが、このステータスコードを返す代わりにこのアプリケーションではログインページへのリダイレクト処理が行われています)。
check_author
引数は、この関数が投稿者を確認せずともpost
を取得できるようにするためのものです。この引数は、一つのページでそれぞれの投稿を表示するview関数を書くときに便利で、ユーザ側から見てもその投稿を編集することにはならないため問題とはなりません。
#flaskr/blog.py
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id): # routeデコレータの<int:id>が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)
今まで書いてきたview関数と異なり、update
関数は引数id
を取ります。これはルートデコレータの<int:id>
に対応します。実際のURLは/1/update
のように表示されます。Flaskは「1」をキャッチし、int型であることを確認し、引数id
に引き渡します。int:
と指定せず<id>
とすると、「1」は文字列型になります。該当するアップデートページへのURLを生成するためにはurl_for
関数にid
を渡す必要がありますが、そのためにはurl_for('blog.update', id=post['id'])
を満たす必要があります。このコードはindex.html
ファイルに記述されています。
create
とupdate
はとても似ていますが、大きな違いはupdate
view関数はSQLクエリでpost
オブジェクトとINSERT
の代わりにUPDATE
クエリを使うところです。よりクレバーなリファクタリングをすれば、これらアクションを1つのview関数とテンプレートにすることもできますが、チュートリアルでは明確にするため分けています。
# flaskr/templates/blog/update.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つのフォームを持ちます。最初のフォームは、現在のページ(/<id>/update
)に編集したデータを送ります。もう1つのフォームはボタンだけを含み、delete
view関数に送るようaction
属性で指定されている。このボタンは、送信前の確認ダイアログを表示するためJavaScriptを利用しています。
パターン{{ request.form['title'] or post['title'] }}
は、フォームの中で表示されるデータを指定するために使われます。フォームが送信されなかった場合、オリジナルデータが表示されますが、無効なフォームデータが投稿された場合、ユーザがエラーを修正できるようにするため、代わりにrequest.form
が使われます。request
はテンプレート内で自動で使うことができるもう1つの変数です。
7-5. view関数: Delete
delete
view関数は自身のテンプレートを持ちません。削除ボタンはupdate.html
の一部であり、URL/<id>/delete
へとアクセスします。テンプレートはないので、POST
メソッドだけを処理し、index
ページにリダイレクトされます。
# flaskr/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'))
・投稿がある場合のインデックス画面
・”Edit”ボタンを押した時のアップデート画面
・”delete”ボタンを押した時の確認画面
これでアプリケーションに関するコードの記述が完了しました。ただし、プロジェクトが完了するにはするべきことがまだ残っています。段落