• トップ
  • ブログ一覧
  • 【第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...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background