Python | 1 日 120 分 × 7 日アプリ学習:Flaskで作る超簡単Webアプリ(中級編)

Web APP Python
スポンサーリンク

7日目のゴール

7日目のテーマは
「Flask の Webサーバーとルーティングを、自分で“ゼロから設計できるレベル”まで持っていくこと」 です。

ここまでで、あなたはすでに

固定パス、動的パス、クエリパラメータ
GET と POST、フォーム送信、リダイレクト
一覧・詳細・作成・削除という一通りの流れ

を一度、自分の手で通っています。

7日目では、
「講師が用意した設計をなぞる」のではなく、
あなた自身が“どんなURLにして、どんな動きをさせるか”を考える側に回る ことを意識します。

題材として、6日目のメモアプリとよく似た
「タスク管理ミニアプリ」を作りながら、
ルーティング設計の考え方をもう一段深く固めていきます。


ここまでのルーティングを一言でまとめる

Flask のルーティングは、ずっとこの形でした。

URL(パス)を決める
そのURLに対して、どのHTTPメソッドを受け付けるかを決める
そのURLとメソッドの組み合わせに対して、どの関数を呼ぶかを決める
関数の返り値(文字列やHTML)が、そのままレスポンスになる

そして、URLの中に

パスパラメータ(/items/<int:id><int:id>
クエリパラメータ(?q=xxx&page=2qpage

を埋め込むことで、
「同じ種類のページだけど、中身が違う」ものを表現してきました。

7日目は、この考え方を
「一つのアプリ全体の設計」にまで引き上げます。


今日作るものを“言葉だけ”で設計してみる

タスク管理ミニアプリの仕様を文章で決める

まずは、コードを書かずに「どんなアプリにするか」を言葉で決めます。

やりたいことはシンプルです。

タスクの一覧を見たい
タスクを追加したい
タスクを「完了」にしたり「未完了」に戻したりしたい

これをURLに落とし込んでいきます。

タスク一覧
GET /tasks

タスク新規作成フォーム表示
GET /tasks/new

タスク新規作成送信
POST /tasks/new

タスク詳細
GET /tasks/<int:task_id>

タスクの完了状態を切り替え
POST /tasks/<int:task_id>/toggle

さらに、一覧ページで
「全部」「完了だけ」「未完了だけ」
を切り替えられるようにしたいので、クエリパラメータも使います。

GET /tasks?status=all
GET /tasks?status=done
GET /tasks?status=todo

ここまで言葉で書けたら、もう設計の8割は終わっています。
あとは、この設計図を Flask のコードに写していくだけです。


疑似データベースとしてのタスクリストを用意する

Pythonのリストと辞書でタスクを表現する

まずは、アプリの土台となるデータ構造を決めます。

from flask import Flask, request, redirect, url_for

app = Flask(__name__)

tasks = []
next_task_id = 1
Python

タスク1件は、次のような辞書で表現します。

task = {
    "id": 1,
    "title": "牛乳を買う",
    "done": False,
}
Python

id は一意な番号
title はタスクの内容
done は完了しているかどうか(True / False)

tasks は、こういう辞書を並べたリストです。
next_task_id は、新しいタスクを作るたびに1ずつ増やしていきます。

これは、6日目のメモアプリと同じ構造です。
「リソースがメモからタスクに変わっただけ」と捉えてOKです。


タスク一覧ページのルーティングと実装

GET /tasks と ?status= で絞り込み

まずは一覧ページから作ります。
ここで、クエリパラメータを使って絞り込みも実装します。

@app.route("/tasks")
def list_tasks():
    status = request.args.get("status", "all")

    if status == "done":
        filtered = [t for t in tasks if t["done"]]
        heading = "完了したタスク一覧"
    elif status == "todo":
        filtered = [t for t in tasks if not t["done"]]
        heading = "未完了のタスク一覧"
    else:
        filtered = tasks
        heading = "すべてのタスク一覧"

    lines = []
    lines.append(f"<h1>{heading}</h1>")
    lines.append('<p><a href="/tasks/new">新しいタスクを追加</a></p>')
    lines.append('<p>')
    lines.append('<a href="/tasks?status=all">すべて</a> | ')
    lines.append('<a href="/tasks?status=todo">未完了</a> | ')
    lines.append('<a href="/tasks?status=done">完了</a>')
    lines.append('</p>')

    if not filtered:
        lines.append("<p>該当するタスクはありません。</p>")
    else:
        lines.append("<ul>")
        for task in filtered:
            detail_url = url_for("show_task", task_id=task["id"])
            mark = "✅" if task["done"] else "⬜"
            lines.append(f'<li>{mark} <a href="{detail_url}">{task["title"]}</a></li>')
        lines.append("</ul>")

    lines.append('<p><a href="/">トップに戻る</a></p>')
    return "\n".join(lines)
Python

ここでの重要ポイントを深掘りします。

status = request.args.get("status", "all")
クエリパラメータ status を受け取り、指定がなければ "all" を使います。
これで、同じ /tasks というURLでも、
?status=todo?status=done で中身を切り替えられます。

filtered に「表示対象のタスクだけ」を入れる
status の値に応じて、tasks から絞り込みをしています。
この「一覧+絞り込み」は、実務でも頻出のパターンです。

url_for("show_task", task_id=task["id"])
詳細ページのURLを、関数名と引数から生成しています。
URLを文字列でベタ書きしないことで、後からルートを変えても壊れにくくなります。


タスク新規作成のルーティングと実装

GET /tasks/new と POST /tasks/new

次に、新しいタスクを追加するページを作ります。

@app.route("/tasks/new", methods=["GET", "POST"])
def new_task():
    global next_task_id

    if request.method == "GET":
        return """
        <h1>新しいタスクを追加</h1>
        <form method="post">
            <p><input type="text" name="title" placeholder="タスク内容"></p>
            <p><button type="submit">追加</button></p>
        </form>
        <p><a href="/tasks">タスク一覧に戻る</a></p>
        """

    title = request.form.get("title", "").strip()

    if not title:
        return """
        <h1>エラー</h1>
        <p>タスク内容は必須です。</p>
        <p><a href="/tasks/new">戻る</a></p>
        """

    task = {
        "id": next_task_id,
        "title": title,
        "done": False,
    }
    tasks.append(task)
    next_task_id += 1

    return redirect(url_for("list_tasks"))
Python

ここでの本質を整理します。

methods=["GET", "POST"] で、同じURLでフォーム表示と送信後処理を切り替えている
GET のときはフォームを返し、POST のときはデータを受け取って保存します。

request.form.get("title") でフォームの値を受け取る
HTML側の name="title" と対応しています。
strip() で前後の空白を削り、空文字を弾いています。

保存後は redirect(url_for("list_tasks"))
5日目でやった「POST/Redirect/GET」パターンです。
追加後にリロードしても、同じPOSTが二重送信されません。


タスク詳細ページのルーティングと実装

GET /tasks/<int:task_id>

次は、1件のタスクを表示するページです。

@app.route("/tasks/<int:task_id>")
def show_task(task_id):
    task = None
    for t in tasks:
        if t["id"] == task_id:
            task = t
            break

    if task is None:
        return """
        <h1>タスクが見つかりません</h1>
        <p>指定されたIDのタスクは存在しません。</p>
        <p><a href="/tasks">タスク一覧に戻る</a></p>
        """, 404

    toggle_url = url_for("toggle_task", task_id=task_id)

    status_text = "完了" if task["done"] else "未完了"
    button_label = "未完了に戻す" if task["done"] else "完了にする"

    return f"""
    <h1>タスク詳細</h1>
    <p>ID: {task["id"]}</p>
    <p>内容: {task["title"]}</p>
    <p>状態: {status_text}</p>
    <form method="post" action="{toggle_url}">
        <button type="submit">{button_label}</button>
    </form>
    <p><a href="/tasks">タスク一覧に戻る</a></p>
    """
Python

ここでの重要ポイントを深掘りします。

@app.route("/tasks/<int:task_id>")
URLの一部を「タスクID」として受け取っています。
/tasks/1 なら task_id = 1 です。

tasks リストから、対応するIDのタスクを探している
見つからなければ404を返します。
「存在しないIDにアクセスされたときの振る舞い」も、ルーティング設計の一部です。

完了状態を切り替えるフォームの action に、toggle_task のURLを埋め込んでいる
ここで、次に作る「完了状態切り替え用ルート」とつながります。


タスク完了状態の切り替えルーティングと実装

POST /tasks/<int:task_id>/toggle

完了・未完了の切り替えは、
「状態を変える操作」なので POST で行います。

@app.route("/tasks/<int:task_id>/toggle", methods=["POST"])
def toggle_task(task_id):
    task = None
    for t in tasks:
        if t["id"] == task_id:
            task = t
            break

    if task is None:
        return """
        <h1>タスクが見つかりません</h1>
        <p>すでに削除された可能性があります。</p>
        <p><a href="/tasks">タスク一覧に戻る</a></p>
        """, 404

    task["done"] = not task["done"]

    return redirect(url_for("show_task", task_id=task_id))
Python

ここでの本質を整理します。

methods=["POST"] で、このURLはPOST専用にしている
GET で直接叩いて状態を変えられないようにしています。
「状態を変える操作はPOST」というのは、Web設計の基本的な考え方です。

タスクが見つからなければ404
「すでに削除されたかもしれない」というメッセージを出しています。

状態を切り替えたあと、詳細ページにリダイレクト
ここでも「POST/Redirect/GET」パターンです。
切り替え後の状態を GET のページで確認できるようにしています。


トップページと404カスタムを足して“アプリの顔”にする

/ と 404 を整える

最後に、トップページと404ページを用意して、
アプリ全体の顔を整えます。

@app.route("/")
def index():
    return """
    <h1>Flask タスク管理アプリ(7日目)</h1>
    <p><a href="/tasks">タスク一覧を見る</a></p>
    """

@app.errorhandler(404)
def page_not_found(error):
    return """
    <h1>404 - ページが見つかりません</h1>
    <p>URLが間違っているか、このページは存在しません。</p>
    <p><a href="/">トップに戻る</a></p>
    """, 404
Python

これで、
存在しないURLにアクセスされたときも、
アプリらしいメッセージで案内できるようになります。


全体コードを一つにまとめる

ここまでのコードを一つにすると、こうなります。

from flask import Flask, request, redirect, url_for

app = Flask(__name__)

tasks = []
next_task_id = 1

@app.route("/")
def index():
    return """
    <h1>Flask タスク管理アプリ(7日目)</h1>
    <p><a href="/tasks">タスク一覧を見る</a></p>
    """

@app.route("/tasks")
def list_tasks():
    status = request.args.get("status", "all")

    if status == "done":
        filtered = [t for t in tasks if t["done"]]
        heading = "完了したタスク一覧"
    elif status == "todo":
        filtered = [t for t in tasks if not t["done"]]
        heading = "未完了のタスク一覧"
    else:
        filtered = tasks
        heading = "すべてのタスク一覧"

    lines = []
    lines.append(f"<h1>{heading}</h1>")
    lines.append('<p><a href="/tasks/new">新しいタスクを追加</a></p>')
    lines.append('<p>')
    lines.append('<a href="/tasks?status=all">すべて</a> | ')
    lines.append('<a href="/tasks?status=todo">未完了</a> | ')
    lines.append('<a href="/tasks?status=done">完了</a>')
    lines.append('</p>')

    if not filtered:
        lines.append("<p>該当するタスクはありません。</p>")
    else:
        lines.append("<ul>")
        for task in filtered:
            detail_url = url_for("show_task", task_id=task["id"])
            mark = "✅" if task["done"] else "⬜"
            lines.append(f'<li>{mark} <a href="{detail_url}">{task["title"]}</a></li>')
        lines.append("</ul>")

    lines.append('<p><a href="/">トップに戻る</a></p>')
    return "\n".join(lines)

@app.route("/tasks/new", methods=["GET", "POST"])
def new_task():
    global next_task_id

    if request.method == "GET":
        return """
        <h1>新しいタスクを追加</h1>
        <form method="post">
            <p><input type="text" name="title" placeholder="タスク内容"></p>
            <p><button type="submit">追加</button></p>
        </form>
        <p><a href="/tasks">タスク一覧に戻る</a></p>
        """

    title = request.form.get("title", "").strip()

    if not title:
        return """
        <h1>エラー</h1>
        <p>タスク内容は必須です。</p>
        <p><a href="/tasks/new">戻る</a></p>
        """

    task = {
        "id": next_task_id,
        "title": title,
        "done": False,
    }
    tasks.append(task)
    next_task_id += 1

    return redirect(url_for("list_tasks"))

@app.route("/tasks/<int:task_id>")
def show_task(task_id):
    task = None
    for t in tasks:
        if t["id"] == task_id:
            task = t
            break

    if task is None:
        return """
        <h1>タスクが見つかりません</h1>
        <p>指定されたIDのタスクは存在しません。</p>
        <p><a href="/tasks">タスク一覧に戻る</a></p>
        """, 404

    toggle_url = url_for("toggle_task", task_id=task_id)

    status_text = "完了" if task["done"] else "未完了"
    button_label = "未完了に戻す" if task["done"] else "完了にする"

    return f"""
    <h1>タスク詳細</h1>
    <p>ID: {task["id"]}</p>
    <p>内容: {task["title"]}</p>
    <p>状態: {status_text}</p>
    <form method="post" action="{toggle_url}">
        <button type="submit">{button_label}</button>
    </form>
    <p><a href="/tasks">タスク一覧に戻る</a></p>
    """

@app.route("/tasks/<int:task_id>/toggle", methods=["POST"])
def toggle_task(task_id):
    task = None
    for t in tasks:
        if t["id"] == task_id:
            task = t
            break

    if task is None:
        return """
        <h1>タスクが見つかりません</h1>
        <p>すでに削除された可能性があります。</p>
        <p><a href="/tasks">タスク一覧に戻る</a></p>
        """, 404

    task["done"] = not task["done"]

    return redirect(url_for("show_task", task_id=task_id))

@app.errorhandler(404)
def page_not_found(error):
    return """
    <h1>404 - ページが見つかりません</h1>
    <p>URLが間違っているか、このページは存在しません。</p>
    <p><a href="/">トップに戻る</a></p>
    """, 404

if __name__ == "__main__":
    app.run(debug=True)
Python

7日目で絶対に押さえてほしい本質

ルーティングは、
「URLに対してどの関数を呼ぶか」を決める仕組み、
というだけではありません。

/tasks
/tasks/new
/tasks/<id>
/tasks/<id>/toggle

のように、
URLそのものが「アプリの機能一覧」になっている。
そして、Flaskのルーティングは、その設計図をコードに落とすための言語です。

ここまで7日間やってきたあなたは、
もう「Flaskを触ったことがある人」ではなく、
自分で小さなWebアプリの構造を設計して、ルーティングに落とし込める人 です。

この先は、
テンプレートファイルを使って見た目を整えたり、
データベースを使って本当に保存できるようにしたり、
ログイン機能を足したり。

どこに進んでも、今日までに積み上げた
「URL・メソッド・ルーティング・リダイレクト・エラー」
の感覚が、ずっと土台になり続けます。

タイトルとURLをコピーしました