ブログの青写真—フラスコのドキュメント

提供:Dev Guides
< FlaskFlask/docs/1.0.x/tutorial/blog
移動先:案内検索

ブログの青写真

認証の青写真を作成するときに学習したのと同じ手法を使用して、ブログの青写真を作成します。 ブログにはすべての投稿が一覧表示され、ログインしているユーザーが投稿を作成できるようにし、投稿の作成者が投稿を編集または削除できるようにする必要があります。

各ビューを実装するときは、開発サーバーを実行し続けます。 変更を保存するときは、ブラウザのURLにアクセスしてテストしてみてください。

ブループリント

ブループリントを定義し、アプリケーションファクトリに登録します。

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__)

app.register_blueprint()を使用して、工場からブループリントをインポートして登録します。 アプリを返す前に、ファクトリ関数の最後に新しいコードを配置します。

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

認証ブループリントとは異なり、ブログブループリントには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ビューを定義できます。 その場合、indexblog.indexのエンドポイントとURLは異なります。


索引

インデックスには、すべての投稿が最新のものから順に表示されます。 JOINは、userテーブルの作成者情報を結果で利用できるようにするために使用されます。

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>
  {% 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デコレータは、ブログビューで使用されます。 これらのビューにアクセスするには、ユーザーがログインする必要があります。ログインしないと、ログインページにリダイレクトされます。

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 %}

アップデート

updateビューとdeleteビューの両方で、postidでフェッチし、作成者がログインユーザーと一致するかどうかを確認する必要があります。 コードの重複を避けるために、postを取得し、各ビューから呼び出す関数を作成できます。

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ステータスコードを返す特別な例外を発生させます。 エラーとともに表示するにはオプションのメッセージが必要です。それ以外の場合は、デフォルトのメッセージが使用されます。 404は「見つかりません」を意味し、403は「禁止」を意味します。 (401は「未承認」を意味しますが、そのステータスを返す代わりにログインページにリダイレクトします。)

check_author引数は、作成者をチェックせずに関数を使用してpostを取得できるように定義されています。 これは、ページに個々の投稿を表示するビューを作成した場合に役立ちます。ユーザーは投稿を変更していないため、ユーザーは関係ありません。

flaskr/blog.py

@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つのビューとテンプレートを使用できますが、チュートリアルでは、それらを別々に保つ方が明確です。

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つの形式があります。 1つ目は、編集したデータを現在のページ(/<id>/update)に投稿します。 もう1つのフォームにはボタンのみが含まれ、代わりに削除ビューに投稿するaction属性を指定します。 ボタンはJavaScriptを使用して、送信する前に確認ダイアログを表示します。

パターン{{ request.form['title'] or post['title'] }}は、フォームに表示するデータを選択するために使用されます。 フォームが送信されていない場合、元のpostデータが表示されますが、無効なフォームデータが投稿された場合は、ユーザーがエラーを修正できるように表示するため、代わりにrequest.formが使用されます。 request は、テンプレートで自動的に使用できるもう1つの変数です。


消去

削除ビューには独自のテンプレートがありません。削除ボタンはupdate.htmlの一部であり、/<id>/delete URLに投稿されます。 テンプレートがないため、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'))

おめでとうございます。これでアプリケーションの作成が完了しました。 ブラウザですべてを試してみてください。 ただし、プロジェクトが完了するまでには、まだやるべきことがあります。

プロジェクトをインストール可能にするに進みます。