【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】
IT技術
FastAPIチュートリアル: toDoアプリを作ってみよう~第4回~
前回の記事「【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】」の続きです。
今回も、toDoアプリを作る過程で、FastAPIの使い方を学んでいきましょう!
第1回はこちら
管理者ページをより使いやすく
現在、管理者ページ(ログインユーザごとのページ)は、登録した予定を羅列しているだけのダサいページです。
本来のtoDoアプリケーションで、最低でも以下の機能が欲しいところです。
欲しい機能
- カレンダーでパッと予定を把握できる
- カレンダーの日付から予定を追加・削除・終了ができる
- 直近の予定はわかりやすく
こんなところでしょうか。
これらは、やや難しそうに見えますが、Pythonなら簡単に実現できますので、本FastAPIベースのアプリケーションでも導入してみます。
カレンダーをつくる
Pythonでは「calendar」と呼ばれるモジュールがあり、とても簡単にカレンダーをテキスト形式やHTML形式で作成できます。
HTML形式であれば、以下のように簡単に取得できます。
1# 日本語版の日曜始まりのカレンダー
2cal = calendar.LocaleHTMLCalendar(firstweekday=6, locale='ja_jp')
3# 2019年のHTMLカレンダーを幅4月分で取得
4cal = cal.formatyear(2019, 4)
デフォルトのままでも良いのですが、カレンダーの日付をリンクさせたいので、LocaleHTMLCalendar クラスを継承してマイカレンダーを作りましょう!
そのために、mycalendar.pyというファイルを作成しましょう!
MyCalendarクラス
このファイルでは、先ほども言ったように、自分好みのカレンダーを作成するために、元々のクラスを継承します。
そのため、まずは、以下のような大枠を作ります。
1import calendar
2from datetime import datetime # あとで使う
3
4
5class MyCalendar(calendar.LocaleHTMLCalendar):
6 # ==== ここで親クラスの関数を自分好みに書き換えていく ==== #
7 pass
次に、コンストラクタ(Pythonではイニシャライザと言う)を作成します。
このとき、親クラスのコンストラクタも継承するため、以下のような書き方になります。
また、ここでは、新たな引数として「ログインユーザ名(username)」と「予定」を、辞書型変数(dict{'datetime': done})で渡されることを想定しておきます。
1def __init__(self, username, linked_date: dict):
2 calendar.LocaleHTMLCalendar.__init__(self,
3 firstweekday=6,
4 locale='ja_jp')
5 # 何か予定がある日付はリンクする
6 self.username = username
7 self.linked_date = linked_date # dict{'datetime': done}
全体の体裁を少し変える
LocaleHTMLCalendarクラスで生成されるHTMLは、「年テーブルの中に月テーブル」のようなテーブルが入れ子(ネスト)されています。
まずは、見やすくするため、月ごとのテーブルに枠をつけます。
LocaleHTMLCalendar クラスのメソッドをオーバーライド(上書き)していきましょう。
以下のコードは複雑そうに見えますが、中身はほとんど親クラスの元々のメソッドと同じです。
1def formatmonth(self, theyear, themonth, withyear=True):
2 """ 親クラスとほとんど同じ形で継承 (クラスだけ違う) """
3 v = []
4 a = v.append
5 # = ここが違う =
6 a('<table class="table table-bordered table-sm" style="table-layout: fixed;">')
7 # == ここまで ==
8 a('\n')
9 a(self.formatmonthname(theyear, themonth, withyear=withyear))
10 a('\n')
11 a(self.formatweekheader())
12 a('\n')
13 for week in self.monthdays2calendar(theyear, themonth):
14 a(self.formatweek(week, theyear, themonth)) # ここも違う。年月日全て渡すようにする(後述)
15 a('\n')
16 a('</table><br>')
17 a('\n')
18 return ''.join(v)
週メソッドと日メソッドも上書きする
週メソッドは、「自身の引数」と「日メソッドに渡す引数」を変更しています。
ちなみに、このように親クラスのメソッドと異なる引数を指定すると、PyCharm などの IDE では注意されます。
これは、Pythonでは、オーバーロード(引数違いであれば同じ名前の関数を定義できること)がサポートされていないためです。
しかし、元々の関数を別で使うわけでもないのでOKです。
1def formatweek(self, theweek, theyear, themonth):
2 """
3 オーバーライド (引数を変えるのはPythonでは多分非推奨)
4 引数で year と month を渡すようにした。
5 """
6 s = ''.join(self.formatday(d, wd, theyear, themonth) for (d, wd) in theweek)
7 return '<tr>%s</tr>' % s
日を取得するメソッドで
日を取得するメソッドでは少し工夫を加えます。
本アプリは以下の要素を加えてみました。
- 空白の日は背景色を薄いグレーに
- 終了した予定は背景色を緑、テキストカラーを白に
- 過去の予定で終了していないものは背景色を濃いグレー、テキストカラーを白に
- これからの予定は背景色を黄色にする
- 全ての日付を「/todo/{username}/{year}/{month}/{day}」というURLでリンクする
実装
これらを実装すると以下のようになります。
1def formatday(self, day, weekday, theyear, themonth):
2 """
3 オーバーライド (引数を変えるのはPythonでは多分非推奨)
4 引数で year と month を渡すようにした。
5 """
6 if day == 0:
7 return '<td style="background-color: #eeeeee"> </td>' # 空白
8 else:
9 html = '<td class="text-center {highlight}"><a href="{url}" style="color:{text}">{day}</a></td>'
10 text = 'blue'
11 highlight = ''
12 # もし予定があるなら強調
13 date = datetime(year=theyear, month=themonth, day=day)
14 date_str = date.strftime('%Y%m%d')
15 if date_str in self.linked_date:
16 if self.linked_date[date_str]: # 終了した予定
17 highlight = 'bg-success'
18 text = 'white'
19 elif date < datetime.now(): # 過去の予定
20 highlight = 'bg-secondary'
21 text = 'white'
22 else: # これからの予定
23 highlight = 'bg-warning'
24 return html.format( # url を /todo/{username}/year/month/day に
25 url='/todo/{}/{}/{}/{}'.format(self.username, theyear, themonth, day),
26 text=text,
27 day=day,
28 highlight=highlight)
少し長々として複雑ですが、これでMyCalendarは完成です。
adminコントローラを修正する
次に、このカレンダーを使うために関数を修正しましょう。
その前に、作ったカレンダーをインポートするのを忘れずに!
1from mycalendar import MyCalendar
2from datetime import datetime # これもあとで使う
大々的にadmin() を修正します。
しかし、大方の作業は、先ほどのMyCalendarクラスでやっていますので、コード自体は大したことありません。
1def admin(request: Request, credentials: HTTPBasicCredentials = Depends(security)):
2 # Basic認証で受け取った情報
3 username = credentials.username
4 password = hashlib.md5(credentials.password.encode()).hexdigest()
5
6 """ [new] 今日の日付と来週の日付"""
7 today = datetime.now()
8 next_w = today + timedelta(days=7) # 1週間後の日付
9
10 # データベースからユーザ名が一致するデータを取得
11 user = db.session.query(User).filter(User.username == username).first()
12 db.session.close()
13
14 # 該当ユーザがいない場合
15 if user is None or user.password != password:
16 error = 'ユーザ名かパスワードが間違っています.'
17 raise HTTPException(
18 status_code=HTTP_401_UNAUTHORIZED,
19 detail=error,
20 headers={"WWW-Authenticate": "Basic"},
21 )
22
23 task = db.session.query(Task).filter(Task.user_id == user.id).all()
24 db.session.close()
25
26 """ [new] カレンダー関連 """
27 # カレンダーをHTML形式で取得
28 cal = MyCalendar(username,
29 {t.deadline.strftime('%Y%m%d'): t.done for t in task}) # 予定がある日付をキーとして渡す
30
31 cal = cal.formatyear(today.year, 4) # カレンダーをHTMLで取得
32
33 # 直近のタスクだけでいいので、リストを書き換える
34 task = [t for t in task if today <= t.deadline <= next_w]
35 links = [t.deadline.strftime('/todo/'+username+'/%Y/%m/%d') for t in task] # 直近の予定リンク
36
37 return templates.TemplateResponse('admin.html',
38 {'request': request,
39 'user': user,
40 'task': task,
41 'links': links,
42 'calender': cal})
新しく追加した分の前には「"""」でコメントを添えています。
内包表記をたくさん使用していますが、もしこの辺りの知識が曖昧であれば「Pythonの内包表記」を復習しましょう!
ビューを修正する
最後に、ビュー(templates/admin.html)を修正します。
主な修正ポイントは、後半のカレンダー部分だけですが、{% autoescape false %} でタグをエスケープしておく必要があります。
1{% extends "layout.html" %}
2{% block content %}
3<br>
4<h2>Administrator page</h2>
5<hr>
6<p>Hi, {{ user['username'] }}.</p>
7<p><a href="/" class="btn btn-primary">もどる</a></p>
8<br>
9<h3> - 直近の予定 - </h3>
10<table class="table">
11 <thead class="table-dark">
12 <tr>
13 <th>#</th>
14 <th>内容</th>
15 <th>締め切り</th>
16 <th>掲載日</th>
17 <th>終了</th>
18 </tr>
19 </thead>
20 <tbody>
21 {% for t in task %}
22 <tr>
23 <td>{{ t['id'] }}</td>
24 <td><a href="{{links[loop.index-1]}}">{{ t['content'] }}</a></td> <!-- 修正 -->
25 <td>{{ t['deadline'] }}</td>
26 <td>{{ t['date'] }}</td>
27 <td>{% if t['done'] %}
28 <div class="text-success">済</div>
29 {% else %}
30 <div class="text-danger">未</div>
31 {% endif %}
32 </td>
33 </tr>
34 {% endfor %}
35 </tbody>
36</table>
37<br><br>
38<h3> - カレンダー - </h3>
39<p>
40 <span class="text-warning">■</span> ... 予定があります。
41 <span class="text-secondary">■</span> ... 過去の予定。
42 <span class="text-success">■</span> ... 終了した予定。</p>
43
44<br>
45<p>↓ 日付をクリックして予定を追加・確認などができます。</p>
46{% autoescape false%}
47{{ calender }}
48{% endautoescape%}
49<br>
50
51{% endblock %}
動作確認
ここまで長かったですが、動作確認してみましょう!
以下は、表示例です。
(皆さんがテーブルに追加した内容でビューも変わるので以下が正解ということではありません)。
確認
多少、カレンダーの上端が揃っていないのが気になるところではあります。
が、良しとしましょう(笑)
うまく、「カレンダーの表示」と「直近の予定」が表示されていることがわかります。
また、各リンクが正しいリンク(/todo/{username}/{year}/{month}/{day})になっているかも確認してください。
第4回:さいごに
今回は、管理者ページを主に修正しました。
少しFastAPIとは別の部分の解説ばかりになってしまいましたが、次回は少しFastAPI寄りの話題に戻します。
次回は、先ほどリンク付けした /todo/{username}/{year}/{month}/{day} を作成していきましょう!
URLパターンによって、処理を変える術を解説します!
第5回の記事はこちら
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
おすすめ記事
immichを知ってほしい
2024.10.31