Flask入門~公式ドキュメントを読み解く~ Day 6 ~
前回の内容
前回の記事では、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アプリケーションの全機能が完成です。長かったですね。 基本的な使用法は学べたので、これらの知識を使って実際にアプリケーションを作ってみます。