• トップ
  • ブログ一覧
  • 【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】
  • 【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】

    広告メディア事業部広告メディア事業部
    2019.12.03

    IT技術

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

    前回の記事「【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】」の続きです。

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

    第1回はこちら

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

    管理者ページをより使いやすく

    現在、管理者ページ(ログインユーザごとのページ)は、登録した予定を羅列しているだけのダサいページです。

    本来のtoDoアプリケーションで、最低でも以下の機能が欲しいところです。

    欲しい機能

    1. カレンダーでパッと予定を把握できる
    2. カレンダーの日付から予定を追加・削除・終了ができる
    3. 直近の予定はわかりやすく

    こんなところでしょうか。

    これらは、やや難しそうに見えますが、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 では注意されます。

    PyCharmでは注意される

    これは、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

    日を取得するメソッドで

    日を取得するメソッドでは少し工夫を加えます。

    本アプリは以下の要素を加えてみました。

    1. 空白の日は背景色を薄いグレーに
    2. 終了した予定は背景色を緑、テキストカラーを白に
    3. 過去の予定で終了していないものは背景色を濃いグレー、テキストカラーを白に
    4. これからの予定は背景色を黄色にする
    5. 全ての日付を「/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">&nbsp;</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> ... 予定があります。&nbsp;
    41    <span class="text-secondary"></span> ... 過去の予定。 &nbsp;
    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 %}

    動作確認

    ここまで長かったですが、動作確認してみましょう!

    以下は、表示例です。

    (皆さんがテーブルに追加した内容でビューも変わるので以下が正解ということではありません)。

    確認

    adminページの表示例

    多少、カレンダーの上端が揃っていないのが気になるところではあります。

    が、良しとしましょう(笑)

    うまく、「カレンダーの表示」と「直近の予定」が表示されていることがわかります。

    また、各リンクが正しいリンク(/todo/{username}/{year}/{month}/{day}になっているかも確認してください。

    第4回:さいごに

    今回は、管理者ページを主に修正しました。

    少しFastAPIとは別の部分の解説ばかりになってしまいましたが、次回は少しFastAPI寄りの話題に戻します。

    次回は、先ほどリンク付けした /todo/{username}/{year}/{month}/{day} を作成していきましょう!

    URLパターンによって、処理を変える術を解説します!

    第5回の記事はこちら

    featureImg2019.12.13【第5回】FastAPIチュートリアル: toDoアプリを作ってみよう【予定詳細ページ作成編】FastAPIチュートリアル: toDoアプリを作ってみよう~第5回~前回の記事「【第4回】FastAPIチュートリア...

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

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

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

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

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

    広告メディア事業部

    広告メディア事業部

    おすすめ記事