【第5回】FastAPIチュートリアル: toDoアプリを作ってみよう【予定詳細ページ作成編】
IT技術
FastAPIチュートリアル: toDoアプリを作ってみよう~第5回~
前回の記事「【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】」の続きです。
今回も、toDo アプリを作る過程で、FastAPI の使い方を学んでいきましょう!
第1回はこちら
予定の詳細ページ
前回、管理者ページを作り、/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なら…
のように表示されます。
ログインユーザのみ訪問可能にする
しかし、このままでは、先ほどのリンクに直接訪問してきた人がいた場合、予定が丸見えです。
プライバシーの保護のためにも、このページも管理者しか訪問できないようにする必要があります。
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 }}"> <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回の記事はこちら
ResponderとFastAPIを実際に使って比較してみた
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit