ブログの青写真
認証の青写真を作成するときに学習したのと同じ手法を使用して、ブログの青写真を作成します。 ブログにはすべての投稿が一覧表示され、ログインしているユーザーが投稿を作成できるようにし、投稿の作成者が投稿を編集または削除できるようにする必要があります。
各ビューを実装するときは、開発サーバーを実行し続けます。 変更を保存するときは、ブラウザのURLにアクセスしてテストしてみてください。
ブループリント
ブループリントを定義し、アプリケーションファクトリに登録します。
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__)
app.register_blueprint()
を使用して、工場からブループリントをインポートして登録します。 アプリを返す前に、ファクトリ関数の最後に新しいコードを配置します。
def create_app():
app = ...
# existing code omitted
from . import blog
app.register_blueprint(blog.bp)
app.add_url_rule('/', endpoint='index')
return app
認証ブループリントとは異なり、ブログブループリントにはurl_prefix
がありません。 したがって、index
ビューは/
に、create
ビューは/create
になります。 ブログはFlaskrの主な機能であるため、ブログのインデックスがメインのインデックスになることは理にかなっています。
ただし、以下で定義するindex
ビューのエンドポイントは、blog.index
になります。 一部の認証ビューは、プレーンなindex
エンドポイントを参照していました。 app.add_url_rule()
は、エンドポイント名'index'
を/
URLに関連付けて、url_for('index')
またはurl_for('blog.index')
が両方とも機能し、同じ/
いずれかのURL。
別のアプリケーションでは、ブログの青写真にurl_prefix
を指定し、hello
ビューと同様に、アプリケーションファクトリで別のindex
ビューを定義できます。 その場合、index
とblog.index
のエンドポイントとURLは異なります。
索引
インデックスには、すべての投稿が最新のものから順に表示されます。 JOIN
は、user
テーブルの作成者情報を結果で利用できるようにするために使用されます。
@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)
{% 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 %}
ユーザーがログインすると、header
ブロックはcreate
ビューへのリンクを追加します。 ユーザーが投稿の作成者である場合、その投稿のupdate
ビューへの「編集」リンクが表示されます。 loop.last
は、 Jinjaのforループ内で使用できる特別な変数です。 これは、最後の投稿を除く各投稿の後に行を表示して、それらを視覚的に分離するために使用されます。
作成
create
ビューは、認証register
ビューと同じように機能します。 フォームが表示されるか、投稿されたデータが検証されて投稿がデータベースに追加されるか、エラーが表示されます。
以前に作成したlogin_required
デコレータは、ブログビューで使用されます。 これらのビューにアクセスするには、ユーザーがログインする必要があります。ログインしないと、ログインページにリダイレクトされます。
@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')
{% 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 %}
アップデート
update
ビューとdelete
ビューの両方で、post
をid
でフェッチし、作成者がログインユーザーと一致するかどうかを確認する必要があります。 コードの重複を避けるために、post
を取得し、各ビューから呼び出す関数を作成できます。
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ステータスコードを返す特別な例外を発生させます。 エラーとともに表示するにはオプションのメッセージが必要です。それ以外の場合は、デフォルトのメッセージが使用されます。 404
は「見つかりません」を意味し、403
は「禁止」を意味します。 (401
は「未承認」を意味しますが、そのステータスを返す代わりにログインページにリダイレクトします。)
check_author
引数は、作成者をチェックせずに関数を使用して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)
これまでに作成したビューとは異なり、update
関数は引数id
を取ります。 これは、ルートの<int:id>
に対応します。 実際のURLは/1/update
のようになります。 Flaskは1
をキャプチャし、それがint
であることを確認して、id
引数として渡します。 int:
を指定せず、代わりに<id>
を指定すると、文字列になります。 更新ページへのURLを生成するには、url_for()
にid
を渡して、何を入力するかがわかるようにする必要があります:url_for('blog.update', id=post['id'])
。 これは、上記のindex.html
ファイルにもあります。
create
ビューとupdate
ビューは非常によく似ています。 主な違いは、update
ビューがINSERT
の代わりにpost
オブジェクトとUPDATE
クエリを使用することです。 いくつかの巧妙なリファクタリングを使用すると、両方のアクションに1つのビューとテンプレートを使用できますが、チュートリアルでは、それらを別々に保つ方が明確です。
{% 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つ目は、編集したデータを現在のページ(/<id>/update
)に投稿します。 もう1つのフォームにはボタンのみが含まれ、代わりに削除ビューに投稿するaction
属性を指定します。 ボタンはJavaScriptを使用して、送信する前に確認ダイアログを表示します。
パターン{{ request.form['title'] or post['title'] }}
は、フォームに表示するデータを選択するために使用されます。 フォームが送信されていない場合、元のpost
データが表示されますが、無効なフォームデータが投稿された場合は、ユーザーがエラーを修正できるように表示するため、代わりにrequest.form
が使用されます。 request は、テンプレートで自動的に使用できるもう1つの変数です。
消去
削除ビューには独自のテンプレートがありません。削除ボタンはupdate.html
の一部であり、/<id>/delete
URLに投稿されます。 テンプレートがないため、POST
メソッドのみを処理してから、index
ビューにリダイレクトします。
@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'))
おめでとうございます。これでアプリケーションの作成が完了しました。 ブラウザですべてを試してみてください。 ただし、プロジェクトが完了するまでには、まだやるべきことがあります。
プロジェクトをインストール可能にするに進みます。