• トップ
  • ブログ一覧
  • 【第5回】FastAPIチュートリアル: toDoアプリを作ってみよう【予定詳細ページ作成編】
  • 【第5回】FastAPIチュートリアル: toDoアプリを作ってみよう【予定詳細ページ作成編】

    メディアチームメディアチーム
    2019.12.13

    IT技術

    FastAPIチュートリアル: toDoアプリを作ってみよう~第5回~

    前回の記事「【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】」の続きです。

    今回も、toDo アプリを作る過程で、FastAPI の使い方を学んでいきましょう!

    第1回はこちら

    featureImg2019.11.14【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】FastAPIチュートリアル: toDoアプリを作ってみるFastAPI は、最近注目を集めている WebAPIフレー...

    予定の詳細ページ

    前回、管理者ページを作り、/todo/{username}/{year}/{month}/{day} というURLパターンを各日付にリンクさせました。

    今回は、このURLパターンについて、FastAPI で処理を書いていきます。

    最終的な目標は、予定の「登録」「削除」「終了」ができるようなページを作成することです。

    URLパターンのルーティング

    FastAPI では、以下のようにURLパターンはルーティングします。

    とても直感的でわかりやすいです。

    (一番最後のコードは、新たに templates/detail.html を作成して記述)

    1# urls.py
    2# FastAPIのルーティング用関数
    3app.add_api_route('/', index)
    4app.add_api_route('/admin', admin)
    5app.add_api_route('/register', register, methods=['GET', 'POST'])
    6app.add_api_route('/todo/{username}/{year}/{month}/{day}', detail)  # new
    1# controllers.py
    2def detail(request: Request, username, year, month, day):
    3    """ URLパターンは引数で取得可能 """
    4
    5    return templates.TemplateResponse('detail.html',
    6                                      {'request': request,
    7                                       'username': username,
    8                                       'year': year,
    9                                       'month': month,
    10                                       'day': day})
    1{% extends "layout.html" %}
    2{% block content %}
    3<br>
    4<div class="row">
    5    <div class="col-md-10">
    6        <h1>{{ year }}年{{ month }}月{{ day }}日の予定</h1>
    7    </div>
    8    <div class="col-md-2">
    9        <a href="/admin" class="btn btn-primary">もどる</a>
    10    </div>
    11</div>
    12<hr>
    13<p>{{ username }}さん、予定を確認・登録・終了してください。</p>
    14
    15
    16{% endblock %}

    このように、FastAPIではURLパターンは引数で取得可能です。

    例えば、http://127.0.0.1:8000/todo/admin/2019/4/1なら…

    URLパターンの表示テスト

    のように表示されます。

    ログインユーザのみ訪問可能にする

    しかし、このままでは、先ほどのリンクに直接訪問してきた人がいた場合、予定が丸見えです。

    プライバシーの保護のためにも、このページも管理者しか訪問できないようにする必要があります。

    admin()関数のように「Basic認証」を導入すればOKです。

    しかし、同じようなことを何回も書くのは面倒なので認証関数を作成しましょう。

    新しく auth.py を作り、以下のように認証部分を取り出します。

    1import hashlib
    2import db
    3from models import User
    4from starlette.status import HTTP_401_UNAUTHORIZED
    5from fastapi import HTTPException
    6
    7
    8def auth(credentials):
    9    """ Basic認証チェック """
    10    # Basic認証で受け取った情報
    11    username = credentials.username
    12    password = hashlib.md5(credentials.password.encode()).hexdigest()
    13    # データベースからユーザ名が一致するデータを取得
    14    user = db.session.query(User).filter(User.username == username).first()
    15    db.session.close()
    16
    17    # 該当ユーザがいない場合
    18    if user is None or user.password != password:
    19        error = 'ユーザ名かパスワードが間違っています.'
    20        raise HTTPException(
    21            status_code=HTTP_401_UNAUTHORIZED,
    22            detail=error,
    23            headers={"WWW-Authenticate": "Basic"},
    24        )
    25    return username

    すると、admin() も以下のように書き直すことができます。

    (from auth import auth を忘れずに!)

    1def admin(request: Request, credentials: HTTPBasicCredentials = Depends(security)):
    2
    3    username = auth(credentials)  """ new """
    4
    5    user = db.session.query(User).filter(User.username == username).first()
    6    task = db.session.query(Task).filter(Task.user_id == user.id).all()
    7    db.session.close()
    8
    9    # 今日の日付と来週の日付
    10    today = datetime.now()
    11    next_w = today + timedelta(days=7)  # 1週間後の日付
    12   
    13    # ~ 省略 ~ #

    detail()も修正

    detail() も同様です。

    以下のように書き直すことで、ログイン中のユーザのみ訪問可能となります。

    (ログイン中のユーザが他人の予定を見ることも弾くようにします)

    from starlette.responses import RedirectResponse  を新たにインポートして…

    1def detail(request: Request, username, year, month, day,
    2           credentials: HTTPBasicCredentials = Depends(security)):
    3    """ URLパターンは引数で取得可能 """
    4    # 認証OK?
    5    username_tmp = auth(credentials)
    6
    7    if username_tmp != username:  # もし他のユーザが訪問してきたらはじく
    8        return RedirectResponse('/')
    9
    10    return templates.TemplateResponse('detail.html',
    11                                      {'request': request,
    12                                       'username': username,
    13                                       'year': year,
    14                                       'month': month,
    15                                       'day': day})

    確認する時は、ブラウザのクッキーを削除してみたりして確認してください。

    現在の予定一覧を取得する

    次に、予定詳細ページで「現在入っている予定」を表示させるようにしましょう。

    やることはさほど難しくありません。

    URLパターンから受け取った情報から、該当タスクを取得しビューに反映させるだけです。

    したがって

    1def detail(request: Request, username, year, month, day,
    2           credentials: HTTPBasicCredentials = Depends(security)):
    3    # 認証OK?
    4    username_tmp = auth(credentials)
    5
    6    if username_tmp != username:  # もし他のユーザが訪問してきたらはじく
    7        return RedirectResponse('/')
    8
    9    """ ここから追記 """
    10    # ログインユーザを取得
    11    user = db.session.query(User).filter(User.username == username).first()
    12    # ログインユーザのタスクを取得
    13    task = db.session.query(Task).filter(Task.user_id == user.id).all()
    14    db.session.close()
    15
    16    # 該当の日付と一致するものだけのリストにする
    17    theday = '{}{}{}'.format(year, month.zfill(2), day.zfill(2))  # 月日は0埋めする
    18    task = [t for t in task if t.deadline.strftime('%Y%m%d') == theday]
    19
    20    return templates.TemplateResponse('detail.html',
    21                                      {'request': request,
    22                                       'username': username,
    23                                       'task': task,  # new
    24                                       'year': year,
    25                                       'month': month,
    26                                       'day': day})

    のように書き直せば、ビューに該当タスクを渡すことができます。

    次に、ビューでタスクを展開しましょう。

    1<!-- ~~ 省略 ~~  -->
    2
    3<p>{{ username }}さん、予定を確認・登録・終了してください。</p>
    4
    5<!-- ここから追記 -->
    6<br>
    7<h3>予定一覧</h3>
    8<p>予定を終了した場合はチェックボックスにチェックをしてください。</p>
    9<form action="/done" method="post">
    10    <table class="table">
    11        <thead class="table-dark">
    12        <tr>
    13            <th>内容</th>
    14            <th>締め切り</th>
    15            <th>終了</th>
    16        </tr>
    17        </thead>
    18        <tbody>
    19        {% for t in task %}
    20        <tr>
    21            <td>{{ t.content }}</td>
    22            <td>{{ t.deadline }}</td>
    23            <td>
    24                {% if t.done %}
    25                <span class="text-success">終了済</span>
    26                {% else %}
    27                <input type="checkbox" name="done[]" value="{{ t.id }}"> &nbsp; <span class="text-danger">終了する</span>
    28                {% endif %}
    29            </td>
    30        </tr>
    31        {% endfor %}
    32        </tbody>
    33    </table>
    34    <br>
    35    {% if task | length != 0 %}
    36    <button type="submit" class="btn btn-primary">更新する</button>
    37    {% else %}
    38    <button type="submit" class="btn btn-primary" disabled="disabled">更新する</button>
    39    {% endif %}
    40</form>
    41<!-- ここまで -->
    42
    43{% endblock %}

    確認

    「予定を遂行したか」の処理は「/done」というURLに投げることにしました。

    動作確認

    /done をコーディングする

    次に、先ほどのURLをコーディングします。

    内容はタスクの「終了したか(done)」を変更させるだけですが、このURLもログインユーザ限定にするためにこれも認証関数などを挟みます

    1# urls.py
    2# FastAPIのルーティング用関数
    3app.add_api_route('/', index)
    4app.add_api_route('/admin', admin, methods=['GET', 'POST'])  # POSTリダイレクトもOKにする
    5app.add_api_route('/register', register, methods=['GET', 'POST'])
    6app.add_api_route('/todo/{username}/{year}/{month}/{day}', detail)
    7app.add_api_route('/done', done, methods=['POST'])  # new!
    1# controllers.py
    2async def done(request: Request, credentials: HTTPBasicCredentials = Depends(security)):
    3    # 認証OK?
    4    username = auth(credentials)
    5
    6    # ユーザ情報を取得
    7    user = db.session.query(User).filter(User.username == username).first()
    8
    9    # ログインユーザのタスクを取得
    10    task = db.session.query(Task).filter(Task.user_id == user.id).all()
    11
    12    # フォームで受け取ったタスクの終了判定を見て内容を変更する
    13    data = await request.form()
    14    t_dones = data.getlist('done[]')  # リストとして取得
    15
    16    for t in task:
    17        if str(t.id) in t_dones:  # もしIDが一致すれば "終了した予定" とする
    18            t.done = True
    19
    20    db.session.commit()  # update!!
    21    db.session.close()
    22
    23    return RedirectResponse('/admin')  # 管理者トップへリダイレクト

    ポイントは、フォームでデータを受け取るので、関数はasync をつける必要があります。

    また、他のログインユーザから、他人の予定を無差別に「終了」とされるのを防ぐ工夫も施す必要があります。

    今回は「ログインユーザIDで取得したタスクの中から、受け取ったIDが一致した場合」にしましたが、URLパターンで実装するのもアリだと思います。

    確認

    うまく動作すれば、このような表示に変わります。

    終了したタスク

    第5回:さいごに

    今回は、予定詳細ページの充実を図りました。

    しかし、まだまだ機能としては不十分です。

    新たな予定の登録」や「予定の削除」機能があってこそtoDoアプリケーションです。

    では、次回はこの辺りを実装していきたいと思います!

    第6回の記事はこちら

    featureImg2019.12.26【第6回】FastAPIチュートリアル: toDoアプリを作ってみよう【予定の追加・削除編】FastAPIチュートリアル: toDoアプリを作ってみよう~第6回~前回の記事「【第5回】FastAPIチュートリア...

    ResponderとFastAPIを実際に使って比較してみた

    featureImg2020.01.10ResponderとFastAPIを実際に使って比較してみたResponderとFastAPIを比較したい!Webアプリケーションといえば、PHPの「CakePHP」、Pytho...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background