ブログの青写真(Blueprint) Blog Blueprint

ブログのblueprintを書くために、認証のblueprintを書いたときに学習したテクニックを同様に使用します。ブログは、すべての投稿記事を一覧にし、ログインしたユーザには投稿記事を作成できるようにし、そして投稿記事の作者はそれを編集または削除できるようにさせます。 You'll use the same techniques you learned about when writing the authentication blueprint to write the blog blueprint. The blog should list all posts, allow logged in users to create posts, and allow the author of a post to edit or delete it.

各viewを実装したように、開発サーバは実行したままにしてください。変更を保存するたびに、ブラウザでそのサーバのURLへ行き、それらの変更をテストしてください。 As you implement each view, keep the development server running. As you save your changes, try going to the URL in your browser and testing them out.

青写真(Blueprint) The Blueprint

blueprintを定義し、application factoryの中で登録します。 Define the blueprint and register it in the application factory.

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

factoryから、blueprintをimportしapp.register_blueprint()を使って登録します。新しいコードをfactory関数の最後でappを返す前の所に置いてください。 Import and register the blueprint from the factory using :meth:`app.register_blueprint() <Flask.register_blueprint>`. Place the new code at the end of the factory function before returning the app.

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

authのblueprintと異なり、blogのblueprintはurl_prefixを持ちません。従って、indexのviewの場所(URL)は/createのviewの場所(URL)は/create、のようになります。ブログはFlaskrの中心となる目玉機能であり、従って、ブログのindexがメインのindexになることは理にかなっています。 Unlike the auth blueprint, the blog blueprint does not have a ``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` view at ``/create``, and so on. The blog is the main feature of Flaskr, so it makes sense that the blog index will be the main index.

しかしながら、この後で定義するindexのviewに対するエンドポイントはblog.indexになります。認証のviewのいくつかは、単なるindexのエンドポイントを参照します。url_for('index')またはurl_for('blog.index')のどちらも機能し、いずれも同一の/をURLとして生成するように、app.add_url_rule()はエンドポイント名'index'をURLの/と関連付けます。 However, the endpoint for the ``index`` view defined below will be ``blog.index``. Some of the authentication views referred to a plain ``index`` endpoint. :meth:`app.add_url_rule() <Flask.add_url_rule>` associates the endpoint name ``'index'`` with the ``/`` url so that ``url_for('index')`` or ``url_for('blog.index')`` will both work, generating the same ``/`` URL either way.

他のアプリケーションでは、ブログのblueprintにurl_prefixを与え、application factoryの中で別にindexのviewを、helloのviewと似たように、定義するかもしれません。そうすると、indexblog.indexでエンドポイントとURLは異なるようになります。 In another application you might give the blog blueprint a ``url_prefix`` and define a separate ``index`` view in the application factory, similar to the ``hello`` view. Then the ``index`` and ``blog.index`` endpoints and URLs would be different.

インデックス(Index) Index

indexは最新の投稿記事を最初にして、投稿記事をすべて表示します。結果の中でuserテーブルから作者情報を使用するために、ここでは(SQL文の中で)JOINを使用しています。 The index will show all of the posts, most recent first. A ``JOIN`` is used so that the author information from the ``user`` table is available in the result.

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のviewへのリンクを付け加えます。ユーザが投稿記事の作者であったときは、その投稿記事に対するupdateのviewへリンクする「Edit」が見えるようになります。loop.lastJinjaのforループの内側で利用可能な特殊な変数です。それは、最後以外の各投稿記事の後で線を表示し、各行を表示上分離させるために使用されています When a user is logged in, the ``header`` block adds a link to the ``create`` view. When the user is the author of a post, they'll see an "Edit" link to the ``update`` view for that post. ``loop.last`` is a special variable available inside `Jinja for loops`_. It's used to display a line after each post except the last one, to visually separate them.

作成(Create) Create

createのviewは、authのregisterのviewと同じように機能します。formが表示されるか、postされたデータが検証されてから、データベースへその投稿記事の追加されるか、またはエラーが表示されます。 The ``create`` view works the same as the auth ``register`` view. Either the form is displayed, or the posted data is validated and the post is added to the database or an error is shown.

前の方で書いたlogin_requiredデコレータは、blogのviewで使用します。これらのviewへ訪れるためにはユーザはログインする必要があり、そうでなければログインページへとリダイレクトされます。 The ``login_required`` decorator you wrote earlier is used on the blog views. A user must be logged in to visit these views, otherwise they will be redirected to the login page.

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

updatedeleteのviewは両方とも、idを使ってpost(投稿記事)を取得し、ログインしたユーザと作者が一致しているかチェックする必要があります。コードの重複を避けるために、postを取得する関数を書いて各viewから呼び出すことが可能です。 Both the ``update`` and ``delete`` views will need to fetch a ``post`` by ``id`` and check if the author matches the logged in user. To avoid duplicating code, you can write a function to get the ``post`` and call it from each 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のステータスコードを返す特殊な例外を発生させます。それはエラーと一緒に表示されるメッセージをオプションで引き取り、そうでないときは標準設定のメッセージを使用します。404は「Not Found(見つからない)」を意味し、403は「Forbidden(禁止されている)」を意味します。(401は「Unauthorized(認証されていない)」を意味しますが、ステータスを返す代わりにログインページへリダイレクトさせます。) :func:`abort` will raise a special exception that returns an HTTP status code. It takes an optional message to show with the error, otherwise a default message is used. ``404`` means "Not Found", and ``403`` means "Forbidden". (``401`` means "Unauthorized", but you redirect to the login page instead of returning that status.)

check_author引数を定義しているのは、作者をチェックせずにpostを取得するときにこの関数を使用可能にするためです。これは、個々の投稿記事をページ上に表示する、投稿記事の変更はしないためユーザがだれであっても問題ないページのviewを書く場合、便利でしょう。 The ``check_author`` argument is defined so that the function can be used to get a ``post`` without checking the author. This would be useful if you wrote a view to show an individual post on a page, where the user doesn't matter because they're not modifying the 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)

ここまで書いてきたviewと異なり、update関数はid引数を受け取ります。それは、routeの中にある<int:id>部分に対応します。実際のURLは/1/updateのようになります。Flaskは1を捉えて、それがintであることを確認し、それをid引数として渡します。もしint:を指定せず代わりに<id>を使用した場合は、それは文字列になります。updateのページに対応するURLを生成するには、url_for()idを渡し、「url_for('blog.update', id=post['id'])」(のid部分)に何を埋めればよいか分かるようにする必要があります。これは前述のindex.htmlファイルでも同様です。 Unlike the views you've written so far, the ``update`` function takes an argument, ``id``. That corresponds to the ``<int:id>`` in the route. A real URL will look like ``/1/update``. Flask will capture the ``1``, ensure it's an :class:`int`, and pass it as the ``id`` argument. If you don't specify ``int:`` and instead do ``<id>``, it will be a string. To generate a URL to the update page, :func:`url_for` needs to be passed the ``id`` so it knows what to fill in: ``url_for('blog.update', id=post['id'])``. This is also in the ``index.html`` file above.

createupdateのviewはとても似ています。主な違いは、updateのviewはpostオブジェクトを使用し、INSERTの代わりにUPDATEの問合せ(query)を使用することです。いくらか賢いリファクタリングをすると、両方のアクションで1つのviewとテンプレートを使用できるようになるかもしれませんが、このチュートリアルでは分けたままにした方がより分かりやすくなります。 The ``create`` and ``update`` views look very similar. The main difference is that the ``update`` view uses a ``post`` object and an ``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, you could use one view and template for both actions, but for the tutorial it's clearer to keep them separate.

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つのformがあります。1つめは、いまのページ(/<id>/update)へ編集データをpost(訳注:HTMLのPOSTメソッドで送信)します。他方のformは1つのボタンだけを含んでいて、action属性を指定して、代わりにdeleteのviewへpostします。このボタンは、(formのデータを)提出(submit)する前に確認ダイアログを表示するために、いくらかJavaScriptを使用します。 This template has two forms. The first posts the edited data to the current page (``/<id>/update``). The other form contains only a button and specifies an ``action`` attribute that posts to the delete view instead. The button uses some JavaScript to show a confirmation dialog before submitting.

formの中でどのデータを表示するか選ぶために、{{ request.form['title'] or post['title'] }}パターンが使用されます。formが提出されないときは、元のpostデータが表示されますが、もし不正なformデータがpostされた場合は、ユーザがエラーを修正できるようにするために、不正なformデータを表示したくなるので、request.formが代わりに使われます。requestはテンプレートの中で自動的に利用可能になるもう一つの変数です。 The pattern ``{{ request.form['title'] or post['title'] }}`` is used to choose what data appears in the form. When the form hasn't been submitted, the original ``post`` data appears, but if invalid form data was posted you want to display that so the user can fix the error, so ``request.form`` is used instead. :data:`request` is another variable that's automatically available in templates.

消去(Delete) Delete

deleteのviewは自身のテンプレートを持たず、削除ボタンはupdate.htmlの一部になって、/<id>/deleteのURLへpostします。テンプレートがないため、それはPOSTメソッドだけを処理して、indexのviewへリダイレクトします。 The delete view doesn't have its own template, the delete button is part of ``update.html`` and posts to the ``/<id>/delete`` URL. Since there is no template, it will only handle the ``POST`` method and then redirect to the ``index`` view.

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

おめでとうございます、ここまでで自分のアプリケーションを書き上げました!いくらか時間を取ってブラウザですべて試してみてください。しかしながら、このプロジェクトが完成する前にもう少しやることが残っています。 Congratulations, you've now finished writing your application! Take some time to try out everything in the browser. However, there's still more to do before the project is complete.

インストール可能なプロジェクト(Make the Project Installable)へ続きます。 Continue to :doc:`install`.