
【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】
2021.12.20
FastAPIチュートリアル: toDoアプリを作ってみよう~第4回~
前回の記事「【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】」の続きです。
今回も、toDoアプリを作る過程で、FastAPIの使い方を学んでいきましょう!
第1回はこちら
管理者ページをより使いやすく
現在、管理者ページ(ログインユーザごとのページ)は、登録した予定を羅列しているだけのダサいページです。
本来のtoDoアプリケーションで、最低でも以下の機能が欲しいところです。
欲しい機能
- カレンダーでパッと予定を把握できる
- カレンダーの日付から予定を追加・削除・終了ができる
- 直近の予定はわかりやすく
こんなところでしょうか。
これらは、やや難しそうに見えますが、Pythonなら簡単に実現できますので、本FastAPIベースのアプリケーションでも導入してみます。
カレンダーをつくる
Pythonでは「calendar」と呼ばれるモジュールがあり、とても簡単にカレンダーをテキスト形式やHTML形式で作成できます。
HTML形式であれば、以下のように簡単に取得できます。
1 2 3 4 | # 日本語版の日曜始まりのカレンダー cal = calendar.LocaleHTMLCalendar(firstweekday=6, locale='ja_jp') # 2019年のHTMLカレンダーを幅4月分で取得 cal = cal.formatyear(2019, 4) |
デフォルトのままでも良いのですが、カレンダーの日付をリンクさせたいので、 LocaleHTMLCalendar クラスを継承してマイカレンダーを作りましょう!
そのために、mycalendar.pyというファイルを作成しましょう!
MyCalendarクラス
このファイルでは、先ほども言ったように、自分好みのカレンダーを作成するために、元々のクラスを継承します。
そのため、まずは、以下のような大枠を作ります。
1 2 3 4 5 6 7 | import calendar from datetime import datetime # あとで使う class MyCalendar(calendar.LocaleHTMLCalendar): # ==== ここで親クラスの関数を自分好みに書き換えていく ==== # pass |
次に、コンストラクタ(Pythonではイニシャライザと言う)を作成します。
このとき、親クラスのコンストラクタも継承するため、以下のような書き方になります。
また、ここでは、新たな引数として「ログインユーザ名(username)」と「予定」を、辞書型変数(dict{'datetime': done})で渡されることを想定しておきます。
1 2 3 4 5 6 7 | def __init__(self, username, linked_date: dict): calendar.LocaleHTMLCalendar.__init__(self, firstweekday=6, locale='ja_jp') # 何か予定がある日付はリンクする self.username = username self.linked_date = linked_date # dict{'datetime': done} |
全体の体裁を少し変える
LocaleHTMLCalendarクラスで生成されるHTMLは、「年テーブルの中に月テーブル」のようなテーブルが入れ子(ネスト)されています。
まずは、見やすくするため、月ごとのテーブルに枠をつけます。
LocaleHTMLCalendar クラスのメソッドをオーバーライド(上書き)していきましょう。
以下のコードは複雑そうに見えますが、中身はほとんど親クラスの元々のメソッドと同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def formatmonth(self, theyear, themonth, withyear=True): """ 親クラスとほとんど同じ形で継承 (クラスだけ違う) """ v = [] a = v.append # = ここが違う = a('<table class="table table-bordered table-sm" style="table-layout: fixed;">') # == ここまで == a('\n') a(self.formatmonthname(theyear, themonth, withyear=withyear)) a('\n') a(self.formatweekheader()) a('\n') for week in self.monthdays2calendar(theyear, themonth): a(self.formatweek(week, theyear, themonth)) # ここも違う。年月日全て渡すようにする(後述) a('\n') a('</table><br>') a('\n') return ''.join(v) |
週メソッドと日メソッドも上書きする
週メソッドは、「自身の引数」と「日メソッドに渡す引数」を変更しています。
ちなみに、このように親クラスのメソッドと異なる引数を指定すると、PyCharm などの IDE では注意されます。

PyCharmでは注意される
これは、Pythonでは、オーバーロード(引数違いであれば同じ名前の関数を定義できること)がサポートされていないためです。
しかし、元々の関数を別で使うわけでもないのでOKです。
1 2 3 4 5 6 7 | def formatweek(self, theweek, theyear, themonth): """ オーバーライド (引数を変えるのはPythonでは多分非推奨) 引数で year と month を渡すようにした。 """ s = ''.join(self.formatday(d, wd, theyear, themonth) for (d, wd) in theweek) return '<tr>%s</tr>' % s |
日を取得するメソッドで
日を取得するメソッドでは少し工夫を加えます。
本アプリは以下の要素を加えてみました。
- 空白の日は背景色を薄いグレーに
- 終了した予定は背景色を緑、テキストカラーを白に
- 過去の予定で終了していないものは背景色を濃いグレー、テキストカラーを白に
- これからの予定は背景色を黄色にする
- 全ての日付を「/todo/{username}/{year}/{month}/{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 27 28 | def formatday(self, day, weekday, theyear, themonth): """ オーバーライド (引数を変えるのはPythonでは多分非推奨) 引数で year と month を渡すようにした。 """ if day == 0: return '<td style="background-color: #eeeeee"> </td>' # 空白 else: html = '<td class="text-center {highlight}"><a href="{url}" style="color:{text}">{day}</a></td>' text = 'blue' highlight = '' # もし予定があるなら強調 date = datetime(year=theyear, month=themonth, day=day) date_str = date.strftime('%Y%m%d') if date_str in self.linked_date: if self.linked_date[date_str]: # 終了した予定 highlight = 'bg-success' text = 'white' elif date < datetime.now(): # 過去の予定 highlight = 'bg-secondary' text = 'white' else: # これからの予定 highlight = 'bg-warning' return html.format( # url を /todo/{username}/year/month/day に url='/todo/{}/{}/{}/{}'.format(self.username, theyear, themonth, day), text=text, day=day, highlight=highlight) |
少し長々として複雑ですが、これでMyCalendarは完成です。
adminコントローラを修正する
次に、このカレンダーを使うために関数を修正しましょう。
その前に、作ったカレンダーをインポートするのを忘れずに!
1 2 | from mycalendar import MyCalendar from datetime import datetime # これもあとで使う |
大々的に admin() を修正します。
しかし、大方の作業は、先ほどのMyCalendarクラスでやっていますので、コード自体は大したことありません。
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 | def admin(request: Request, credentials: HTTPBasicCredentials = Depends(security)): # Basic認証で受け取った情報 username = credentials.username password = hashlib.md5(credentials.password.encode()).hexdigest() """ [new] 今日の日付と来週の日付""" today = datetime.now() next_w = today + timedelta(days=7) # 1週間後の日付 # データベースからユーザ名が一致するデータを取得 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"}, ) task = db.session.query(Task).filter(Task.user_id == user.id).all() db.session.close() """ [new] カレンダー関連 """ # カレンダーをHTML形式で取得 cal = MyCalendar(username, {t.deadline.strftime('%Y%m%d'): t.done for t in task}) # 予定がある日付をキーとして渡す cal = cal.formatyear(today.year, 4) # カレンダーをHTMLで取得 # 直近のタスクだけでいいので、リストを書き換える task = [t for t in task if today <= t.deadline <= next_w] links = [t.deadline.strftime('/todo/'+username+'/%Y/%m/%d') for t in task] # 直近の予定リンク return templates.TemplateResponse('admin.html', {'request': request, 'user': user, 'task': task, 'links': links, 'calender': cal}) |
新しく追加した分の前には「"""」でコメントを添えています。
内包表記をたくさん使用していますが、もしこの辺りの知識が曖昧であれば「Pythonの内包表記」を復習しましょう!
ビューを修正する
最後に、ビュー(templates/admin.html)を修正します。
主な修正ポイントは、後半のカレンダー部分だけですが、 {% autoescape false %} でタグをエスケープしておく必要があります。
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 44 45 46 47 48 49 50 51 | {% extends "layout.html" %} {% block content %} <br> <h2>Administrator page</h2> <hr> <p>Hi, {{ user['username'] }}.</p> <p><a href="/" class="btn btn-primary">もどる</a></p> <br> <h3> - 直近の予定 - </h3> <table class="table"> <thead class="table-dark"> <tr> <th>#</th> <th>内容</th> <th>締め切り</th> <th>掲載日</th> <th>終了</th> </tr> </thead> <tbody> {% for t in task %} <tr> <td>{{ t['id'] }}</td> <td><a href="{{links[loop.index-1]}}">{{ t['content'] }}</a></td> <!-- 修正 --> <td>{{ t['deadline'] }}</td> <td>{{ t['date'] }}</td> <td>{% if t['done'] %} <div class="text-success">済</div> {% else %} <div class="text-danger">未</div> {% endif %} </td> </tr> {% endfor %} </tbody> </table> <br><br> <h3> - カレンダー - </h3> <p> <span class="text-warning">■</span> ... 予定があります。 <span class="text-secondary">■</span> ... 過去の予定。 <span class="text-success">■</span> ... 終了した予定。</p> <br> <p>↓ 日付をクリックして予定を追加・確認などができます。</p> {% autoescape false%} {{ calender }} {% endautoescape%} <br> {% endblock %} |
動作確認
ここまで長かったですが、動作確認してみましょう!
以下は、表示例です。
(皆さんがテーブルに追加した内容でビューも変わるので以下が正解ということではありません)。
確認

adminページの表示例
多少、カレンダーの上端が揃っていないのが気になるところではあります。
が、良しとしましょう(笑)
うまく、「カレンダーの表示」と「直近の予定」が表示されていることがわかります。
また、各リンクが正しいリンク(/todo/{username}/{year}/{month}/{day})になっているかも確認してください。
第4回:さいごに
今回は、管理者ページを主に修正しました。
少しFastAPIとは別の部分の解説ばかりになってしまいましたが、次回は少しFastAPI寄りの話題に戻します。
次回は、先ほどリンク付けした /todo/{username}/{year}/{month}/{day} を作成していきましょう!
URLパターンによって、処理を変える術を解説します!
第5回の記事はこちら
ResponderとFastAPIを実際に使って比較してみた
こちらの記事もオススメ!
書いた人はこんな人

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