
【第5回】FastAPIチュートリアル: toDoアプリを作ってみよう【予定詳細ページ作成編】
2021.12.20
FastAPIチュートリアル: toDoアプリを作ってみよう~第5回~
前回の記事「【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】」の続きです。
今回も、toDo アプリを作る過程で、FastAPI の使い方を学んでいきましょう!
第1回はこちら
予定の詳細ページ
前回、管理者ページを作り、/todo/{username}/{year}/{month}/{day} というURLパターンを各日付にリンクさせました。
今回は、このURLパターンについて、FastAPI で処理を書いていきます。
最終的な目標は、予定の「登録」「削除」「終了」ができるようなページを作成することです。
URLパターンのルーティング
FastAPI では、以下のようにURLパターンはルーティングします。
とても直感的でわかりやすいです。
(一番最後のコードは、新たに templates/detail.html を作成して記述)
1 2 3 4 5 6 | # urls.py # FastAPIのルーティング用関数 app.add_api_route('/', index) app.add_api_route('/admin', admin) app.add_api_route('/register', register, methods=['GET', 'POST']) app.add_api_route('/todo/{username}/{year}/{month}/{day}', detail) # new |
1 2 3 4 5 6 7 8 9 10 | # controllers.py def detail(request: Request, username, year, month, day): """ URLパターンは引数で取得可能 """ return templates.TemplateResponse('detail.html', {'request': request, 'username': username, 'year': year, 'month': month, 'day': day}) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {% extends "layout.html" %} {% block content %} <br> <div class="row"> <div class="col-md-10"> <h1>{{ year }}年{{ month }}月{{ day }}日の予定</h1> </div> <div class="col-md-2"> <a href="/admin" class="btn btn-primary">もどる</a> </div> </div> <hr> <p>{{ username }}さん、予定を確認・登録・終了してください。</p> {% endblock %} |
このように、FastAPIではURLパターンは引数で取得可能です。
例えば、http://127.0.0.1:8000/todo/admin/2019/4/1なら…

URLパターンの表示テスト
のように表示されます。
ログインユーザのみ訪問可能にする
しかし、このままでは、先ほどのリンクに直接訪問してきた人がいた場合、予定が丸見えです。
プライバシーの保護のためにも、このページも管理者しか訪問できないようにする必要があります。
admin()関数のように「Basic認証」を導入すればOKです。
しかし、同じようなことを何回も書くのは面倒なので認証関数を作成しましょう。
新しく auth.py を作り、以下のように認証部分を取り出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import hashlib import db from models import User from starlette.status import HTTP_401_UNAUTHORIZED from fastapi import HTTPException def auth(credentials): """ Basic認証チェック """ # Basic認証で受け取った情報 username = credentials.username password = hashlib.md5(credentials.password.encode()).hexdigest() # データベースからユーザ名が一致するデータを取得 user = db.session.query(User).filter(User.username == username).first() db.session.close() # 該当ユーザがいない場合 if user is None or user.password != password: error = 'ユーザ名かパスワードが間違っています.' raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, detail=error, headers={"WWW-Authenticate": "Basic"}, ) return username |
すると、admin() も以下のように書き直すことができます。
( from auth import auth を忘れずに!)
1 2 3 4 5 6 7 8 9 10 11 12 13 | def admin(request: Request, credentials: HTTPBasicCredentials = Depends(security)): username = auth(credentials) """ new """ user = db.session.query(User).filter(User.username == username).first() task = db.session.query(Task).filter(Task.user_id == user.id).all() db.session.close() # 今日の日付と来週の日付 today = datetime.now() next_w = today + timedelta(days=7) # 1週間後の日付 # ~ 省略 ~ # |
detail()も修正
detail() も同様です。
以下のように書き直すことで、ログイン中のユーザのみ訪問可能となります。
(ログイン中のユーザが他人の予定を見ることも弾くようにします)
from starlette.responses import RedirectResponse を新たにインポートして…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def detail(request: Request, username, year, month, day, credentials: HTTPBasicCredentials = Depends(security)): """ URLパターンは引数で取得可能 """ # 認証OK? username_tmp = auth(credentials) if username_tmp != username: # もし他のユーザが訪問してきたらはじく return RedirectResponse('/') return templates.TemplateResponse('detail.html', {'request': request, 'username': username, 'year': year, 'month': month, 'day': day}) |
確認する時は、ブラウザのクッキーを削除してみたりして確認してください。
現在の予定一覧を取得する
次に、予定詳細ページで「現在入っている予定」を表示させるようにしましょう。
やることはさほど難しくありません。
URLパターンから受け取った情報から、該当タスクを取得しビューに反映させるだけです。
したがって
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | def detail(request: Request, username, year, month, day, credentials: HTTPBasicCredentials = Depends(security)): # 認証OK? username_tmp = auth(credentials) if username_tmp != username: # もし他のユーザが訪問してきたらはじく return RedirectResponse('/') """ ここから追記 """ # ログインユーザを取得 user = db.session.query(User).filter(User.username == username).first() # ログインユーザのタスクを取得 task = db.session.query(Task).filter(Task.user_id == user.id).all() db.session.close() # 該当の日付と一致するものだけのリストにする theday = '{}{}{}'.format(year, month.zfill(2), day.zfill(2)) # 月日は0埋めする task = [t for t in task if t.deadline.strftime('%Y%m%d') == theday] return templates.TemplateResponse('detail.html', {'request': request, 'username': username, 'task': task, # new 'year': year, 'month': month, 'day': day}) |
のように書き直せば、ビューに該当タスクを渡すことができます。
次に、ビューでタスクを展開しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <!-- ~~ 省略 ~~ --> <p>{{ username }}さん、予定を確認・登録・終了してください。</p> <!-- ここから追記 --> <br> <h3>予定一覧</h3> <p>予定を終了した場合はチェックボックスにチェックをしてください。</p> <form action="/done" method="post"> <table class="table"> <thead class="table-dark"> <tr> <th>内容</th> <th>締め切り</th> <th>終了</th> </tr> </thead> <tbody> {% for t in task %} <tr> <td>{{ t.content }}</td> <td>{{ t.deadline }}</td> <td> {% if t.done %} <span class="text-success">終了済</span> {% else %} <input type="checkbox" name="done[]" value="{{ t.id }}"> <span class="text-danger">終了する</span> {% endif %} </td> </tr> {% endfor %} </tbody> </table> <br> {% if task | length != 0 %} <button type="submit" class="btn btn-primary">更新する</button> {% else %} <button type="submit" class="btn btn-primary" disabled="disabled">更新する</button> {% endif %} </form> <!-- ここまで --> {% endblock %} |
確認
「予定を遂行したか」の処理は「/done」というURLに投げることにしました。

動作確認
/done をコーディングする
次に、先ほどのURLをコーディングします。
内容はタスクの「終了したか(done)」を変更させるだけですが、このURLもログインユーザ限定にするためにこれも認証関数などを挟みます。
1 2 3 4 5 6 7 | # urls.py # FastAPIのルーティング用関数 app.add_api_route('/', index) app.add_api_route('/admin', admin, methods=['GET', 'POST']) # POSTリダイレクトもOKにする app.add_api_route('/register', register, methods=['GET', 'POST']) app.add_api_route('/todo/{username}/{year}/{month}/{day}', detail) app.add_api_route('/done', done, methods=['POST']) # new! |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # controllers.py async def done(request: Request, credentials: HTTPBasicCredentials = Depends(security)): # 認証OK? username = auth(credentials) # ユーザ情報を取得 user = db.session.query(User).filter(User.username == username).first() # ログインユーザのタスクを取得 task = db.session.query(Task).filter(Task.user_id == user.id).all() # フォームで受け取ったタスクの終了判定を見て内容を変更する data = await request.form() t_dones = data.getlist('done[]') # リストとして取得 for t in task: if str(t.id) in t_dones: # もしIDが一致すれば "終了した予定" とする t.done = True db.session.commit() # update!! db.session.close() return RedirectResponse('/admin') # 管理者トップへリダイレクト |
ポイントは、フォームでデータを受け取るので、関数は async をつける必要があります。
また、他のログインユーザから、他人の予定を無差別に「終了」とされるのを防ぐ工夫も施す必要があります。
今回は「ログインユーザIDで取得したタスクの中から、受け取ったIDが一致した場合」にしましたが、URLパターンで実装するのもアリだと思います。
確認
うまく動作すれば、このような表示に変わります。

終了したタスク
第5回:さいごに
今回は、予定詳細ページの充実を図りました。
しかし、まだまだ機能としては不十分です。
「新たな予定の登録」や「予定の削除」機能があってこそtoDoアプリケーションです。
では、次回はこの辺りを実装していきたいと思います!
第6回の記事はこちら
ResponderとFastAPIを実際に使って比較してみた
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来