【Flask】TODOアプリ作成その4:タスク詳細、編集、削除機能の実装

概要

Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。
flaskのGET/POSTの方法やセッション等の扱い、Jinjaテンプレートの使い方について、当アプリを作成しながら振り返ることが目的。

当記事ではTODOタスクの詳細表示、編集、削除について扱う。

あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱いについて、当アプリを作成しながら振り返ることが目的。 当記[…]

【Flask】TODOアプリ作成その1:ログイン機能の実装ガイド
あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱いについて、当アプリを作成しながら振り返ることが目的。 当記[…]

【Flask】TODOアプリ作成その2:ログイン後のタスク一覧機能の実装
あわせて読みたい

概要 Pythonのflaskフレームワークを使用して、TODOリストを管理するWEBアプリを作成したのでまとめた。 flaskのGET/POSTの方法やセッション等の扱い、Jinjaテンプレートの使い方について、当アプリを作成しながら[…]

【Flask】TODOアプリ作成その3:タスク登録機能の実装

 

TODO詳細、編集、削除機能

機能説明

以下の機能をもつTODO詳細、編集画面を作成した。

TODO詳細画面:
・TODOタスクの詳細表示機能
・TODOタスクの削除機能
TODO編集画面:
・画面初期値設定機能
・入力チェック機能
・TODOタスク内容の更新機能

アプリ概要

【flaskその4】アプリ概要
▲赤文字箇所を実装した

 

画面遷移

◆TODOトップ画面

【flaskその4】TODOトップ画面
▲タスク名リンクを押下すると、TODO詳細画面に遷移する

 

◆TODO詳細画面

【flaskその4】TODO詳細画面
▲タスク内容の詳細を表示する

 

◆TODO編集画面

【flaskその4】TODO編集画面
▲画面表示時、各フォーム部品に初期値を設定する
【flaskその4】TODO編集画面入力エラー時
▲入力エラーの場合
【flaskその4】DB更新エラー時
▲DB更新エラーの場合

 

◆更新成功時

【flaskその4】DB更新成功時
▲更新成功時、TODO詳細画面に遷移

 

◆削除失敗時

【flaskその4】削除失敗
▲削除失敗時、エラー画面に遷移

 

ファイル構成

ファイル構成は以下とする。
※赤文字箇所は前回からの追加ファイルとなる

【flaskその4】ファイル構成

 

TODOタスクの詳細表示機能

TODOトップ画面にてタスク名リンクを押下すると

【flaskその4】タスクリンク
▲TODO詳細画面に遷移するリンク

 

TODO詳細画面にタスク内容を表示する。

【flaskその4】タスク詳細画面コンテンツ
▲タスク詳細を表示

 

解説

サーバー側

data_access.py


def get_todo(t_id):
    """ タスク情報取得 """
    session = get_session()
    return session.get(Todo, t_id)

DBアクセスレイヤでは、タスクIDに紐づくタスククラスをDBから取得して返却する。

business_logic.py


def get_todo(t_id):
    """ タスク情報取得 """
    todo = data_access.get_todo(t_id)
    todo_item = {
        "id": todo.id,
        "title": todo.title,
        "category_id": str(todo.category.id),
        "category": todo.category.category_name,
        "content": todo.content,
        "memo": todo.memo,
        "due_date": f"{todo.due_date:%Y-%m-%d}",
        "status": "未完了" if todo.status == consts.STATUS_INCOMPLETE else "完了"
    }

    return todo_item

ビジネスロジックレイヤでは、DBアクセスレイヤからタスククラスを取得し、画面表示用に辞書データに変換する。

例えば、期日をdatetimeから文字列に変換したり、ステータスを「1/0」から「完了/未完了」に変換している。
また、category_idについては画面側でセレクトボックスの初期値を設定するために文字列型に変換している。

views.py


@app.route("/todo_apps/detail/")
def show_detail(t_id):
    """ タスク詳細画面を表示する """
    todo = business_logic.get_todo(t_id)
    return render_template("detail.html", todo=todo)

ルーティングレイヤでは、画面からタスクIDを受け取りビジネスロジックレイヤへ引数として渡す。
ビジネスロジックからタスク情報を取得し、画面に返却する。

 

画面側

top.html


<td><a href="{{ url_for('show_detail', t_id=todo_item['id']) }}">{{ todo_item["content"] }}</a></td>

TODOトップ画面では、タスク名にアンカータグを設定する。
ルーティングレイヤのTODO詳細画面表示処理へリクエストする。

 

detail.html


<div class="row border p-2 mb-2">
  <div class="col-sm-3"><h5>カテゴリ:</h5></div>
  <div class="col-sm-9"><p>{{ todo["category"] }}</p></div>
</div>
(略)
<div class="row border p-2 mb-2">
  <div class="col-sm-3"><h5>メモ:</h5></div>
  <div class="col-sm-9">
    {% if todo["memo"] %}
    <p>{{ todo["memo"].replace("\r\n", "<br>") | safe }}</p>
    {% else %}
    <p></p>
    {% endif %}
  </div>
</div>

TODO詳細画面では、サーバー側から取得したtodo辞書データを表示する。

メモの内容についてはテキストエリアのため、改行コード(\r\n)を含む内容となる。
jinjaテンプレートでは、HTMLを安全に生成するために改行コード(\r\n)などは自動的にエスケープ処理が行われていた。

そのため、改行された状態でメモの内容を表示するために、str.replaceメソッドを使用して改行コードを<br>タグに変換した。
尚、safeフィルタをつけないと以下のようにHTMLがそのまま表示されてしまうため、safeフィルタが必須

 

【flaskその4】折り返されないメモ
▲safeフィルタを使用しない場合

 

削除機能

TODO詳細画面の【削除する】ボタンを押下すると、タスクをDBから物理削除できる。

【flaskその4】TODO詳細画面の削除ボタン
▲削除ボタンを押下する

 

・タスク削除成功時

対象のタスクが削除されてTODOトップ画面に遷移する。

【flaskその4】TODO詳細画面の削除ボタン押下後画面
▲対象のタスクが削除される

 

・タスク削除失敗時

タスクの削除に失敗すると、エラー画面に遷移する。

【flaskその4】削除失敗時の処理続行エラー画面
▲エラー画面に遷移

 

解説

サーバー側

data_access.py


def delete_task(t_id):
    """ タスクの削除 """
    is_valid = True

    session = get_session()
    try:
        session.query(Todo).filter_by(id=t_id).delete()
        session.commit()
    except Exception as e:
        print(e)
        session.rollback()
        is_valid = False
    finally:
        session.close()
    
    return is_valid

DBアクセスレイヤでは、タスクIDに紐づくタスク情報をDBから削除する。

削除に失敗した場合、is_valid変数にFalseを格納して返却する。
削除に成功した場合、is_valid変数にTrueを格納して返却する。

 

business_logic.py


def delete_task(t_id):
    """ タスクの削除 """
    return data_access.delete_task(t_id)

ビジネスロジックレイヤでは、DBアクセスレイヤの削除結果をそのまま返却する。

views.py


@app.route("/todo_apps/delete/")
def delete_todo(t_id):
    """ タスクを削除する """
    is_valid = business_logic.delete_task(t_id)
    if not is_valid:
        # 削除エラーの場合
        return redirect(url_for("show_error", error_id=consts.DB_ERROR))
    else:
        return redirect(url_for('top'))

ルーティングレイヤでは、画面からタスクIDを受け取りビジネスロジックレイヤへ引数として渡す。
削除結果を取得し、その結果を判定して画面遷移する。

タスク削除に失敗した場合、エラーIDを設定してエラー画面表示メソッドにリダイレクトする。
タスク削除に成功した場合、TODOトップ画面表示メソッドにリダイレクトする。

views.py


@app.route("/todo_apps/error")
def show_error():
    session.clear()
    error_id = request.args.get("error_id", "")
    if error_id == consts.DB_ERROR:
        # DBエラーの場合
        error_msg = "DBを更新中にエラーが発生しました。時間を空けて、再度ログインからやりなおしてください。"
    else:
        error_msg = "エラーが発生しました。時間を空けて、再度ログインからやりなおしてください。"
    
    return render_template("error.html", error_msg=error_msg)

エラー画面表示メソッド。
セッションクリアを行った後、エラーIDをもとにエラーメッセージを作成してエラー画面を返却する。

エラー画面に遷移する理由は、再度TODO詳細画面に遷移しても詳細情報をDBから取得できず、画面状態を再現できない可能性があるため

 

画面側

error.html


<div class="container mt-5 p-3 card border">
  <h2 class="text-center">処理続行エラー</h2>
  <div class="mt-4">
    <div class="card-body">
      <div class="p-2 mb-3 text-center">
        <p>{{ error_msg }}</p>
      </div>
      <div class="d-flex justify-content-center mt-3">
        <a href="{{ url_for('show_login') }}" class="btn btn-primary mb-3">ログイン</a>
      </div>
    </div>
  </div>
</div>

エラー画面では、サーバー側から取得したエラーメッセージを表示している。
また、ログインボタンを表示している。

 

画面初期値設定機能

TODO編集画面を表示すると、各フォーム部品にDBから取得したタスク情報を初期値として設定する。

【flaskその4】TODO編集画面に初期値設定
▲登録画面と同じレイアウトだが、編集の場合は初期値を設定する

 

解説

サーバー側

◆data_access.py

TODOタスクの詳細表示機能と同様。
get_todoメソッドを呼び出す。

 

◆business_logic.py

TODOタスクの詳細表示機能と同様。
get_todoメソッドを呼び出す。

views.py


@app.route("/todo_apps/edit/")
def show_edit(t_id):
    """ 編集画面を表示する """
    categories = business_logic.get_category_all()
    todo = business_logic.get_todo(t_id)
    return render_template("edit.html", categories=categories, todo=todo)

ルーティングレイヤでは、画面からタスクIDを受け取りビジネスロジックレイヤへ引数として渡してタスク情報を取得する。
あわせてカテゴリ一覧もDBから取得する。

カテゴリ一覧リスト、タスク情報を画面に返却する。

 

画面側

edit.html


<div class="form-group">
  <label for="category">カテゴリ<span class="text-primary ml-3">※必須</span></label>
  <select class="form-control" id="category" name="category">
    <option value="">選択してください</option>
    {% for category in categories %}
    <option value="{{category.id}}" 
    {% if category.id|string()==todo.get('category_id') %} selected {% endif %}>{{ category.category_name }}</option>
    {% endfor %}
  </select>
</div>
(中略)
<div class="form-group mt-3">
  <label for="title">タイトル <span class="text-primary ml-3">※必須</span></label>
  <input type="text" class="form-control" id="title" name="title" value="{{ todo.get('title', '') }}">
</div>

TODO編集画面では、サーバー側から取得したtodo辞書データを表示する。

 

入力チェック機能

TODO編集画面にて入力エラーがある場合、form部品直下にエラーメッセージを表示する。
※TODO登録画面と同様の機能

【flaskその4】TODO編集画面エラーメッセージ仕様
▲TODO登録画面と同じメッセージ

 

解説

サーバー側

views.py


@app.route("/todo_apps/edit/done", methods=["POST"])
def edit_todo():
    """ タスクを編集する """
    # 入力値検証
    error_dict = {}
    is_valid = validate_views.validate_input_todo(request.form, error_dict)
    if not is_valid:
        # 入力エラーの場合
        categories = business_logic.get_category_all()
        todo = get_todo(request.form)
        return render_template("edit.html", categories=categories, error_dict=error_dict, todo=todo)

ルーティングレイヤでは、TODOタスクの更新リクエストがくると画面の入力値検証を行う。
入力エラーの場合、各フォームのエラーメッセージ内容が格納されたerror_dict辞書データを画面側に返却する。

入力チェックを行うメソッドについては、TODO登録画面と同じ入力チェックメソッドを使用するため以下のようにメソッド名を修正した。

is_valid = validate_views.validate_create_todo(request.form, error_dict)

 

is_valid = validate_views.validate_input_todo(request.form, error_dict)

validate_create_todoという名前の場合、意味的に登録画面の入力チェックとなってしまうため、より汎用的な名前のvalidate_input_todoに変更した。

 

エラーが発生した場合、TODO編集画面の画面状態を再現するため以下のようにリクエスト内容をtodo辞書データに変換している。

views.py


def get_todo(form):
    """ タスク情報を返却する """
    todo = {
        "id": form.get("id"),
        "title": form.get("title", ""),
        "category_id": form.get("category", ""),
        "content": form.get("content", ""),
        "memo": form.get("memo", ""),
        "due_date": form.get("due_date", "")
    }

    return todo

TODO登録画面では、このような辞書データに変換する処理は行わず、画面入力内容はrequest.formから再現している。

しかし、編集ルートの場合はTODO編集画面初期表示時にtodo辞書データを画面側に渡しているため、入力エラー時の画面状態再現のためには初期表示時と同じ形式で画面側にタスク内容を返却する必要がある。

そのため、request.formから入力値を取得してtodo辞書データに変換している。

 

画面側

edit.html


{% if error_dict and error_dict["content"] %}
<div id="error-message" class="text-danger mb-3">{{ error_dict["content"] }}</div>
{% endif %}

TODO編集画面では、TODO登録画面と同様のエラーメッセージ表示領域を作成した。

error_dictを最初に判定する理由は、サーバー側からerror_dictが画面に返却されていない状態で、オブジェクト内を参照しようとするとエラーになってしまうため。
まず、error_dict内に値があるか判定を行う必要がある。

error_dict内に格納されたエラーメッセージは、各フォーム部品の名前をキーに表示する。

 

TODOタスク内容の更新機能

TODO編集画面にて、タスク内容を更新できる。

【flaskその4】TODO編集画面にてメモ更新
▲メモ内容を変更

 

【flaskその4】TODO編集画面にて更新成功の場合
▲更新成功の場合、TODO詳細画面に遷移

 

【flaskその4】TODO編集画面にて更新失敗の場合
▲更新失敗の場合、入力内容を保持してエラーメッセージ表示

 

解説

サーバー側

data_access.py


def update_todo(todo_item):
    """ タスクの更新 """
    is_valid = True

    session = get_session()
    todo = session.get(Todo, todo_item["id"])
    if not todo:
        is_valid = False
    else:
        try:
            todo.title = todo_item["title"]
            todo.content = todo_item["content"]
            todo.memo = todo_item["memo"]
            todo.due_date = todo_item["due_date"]
            todo.category_id = todo_item["category_id"]

            session.add(todo)
            session.commit()
        except Exception as e:
            print(e)
            session.rollback()
            is_valid = False
        finally:
            session.close()

    return is_valid

DBアクセスレイヤでは、タスク辞書データを取得して以下の順序で更新する。

①タスクIDをキーにDBからTodoクラスを取得
②取得したTodoクラスに画面入力値を設定してコミット

DB更新に失敗した場合、is_valid変数にFalseを格納して返却する。
DB更新に成功した場合、is_valid変数にTrueを格納して返却する。

 

business_logic.py


def update_todo(form):
    """ タスクの更新 """    
    todo_item = {
        "id": form.get("id"),
        "title": form.get("title"),
        "content": form.get("content"),
        "memo": form.get("memo", ""),
        "due_date": datetime.strptime(form.get("due_date"), consts.DATE_FORMAT),
        "category_id": int(form.get("category"))
    }

    return data_access.update_todo(todo_item)

ビジネスロジックレイヤでは、画面入力値をDB更新できる値に変換してDBアクセスレイヤに渡す。
DBアクセスレイヤの結果をそのまま返却する。

views.py


# タスク更新
is_valid = business_logic.update_todo(request.form)
if not is_valid:
    # 入力エラーの場合
    categories = business_logic.get_category_all()
    error_msg = "タスクの更新に失敗しました。時間を空けて再度実行してください。"
    todo = get_todo(request.form)
    return render_template("edit.html", categories=categories, error_msg=error_msg, todo=todo) 
else:
    return redirect(url_for("show_detail", t_id=request.form["id"]))

ルーティングレイヤでは、ビジネスロジックレイヤから取得したDB更新結果を判定する。

DB更新に失敗した場合、再度カテゴリ一覧を取得,タスク情報を画面表示用に変換して取得,エラーメッセージを設定して画面に返却する。
DB更新に成功した場合、TODO詳細画面表示メソッドにリダイレクトする。

 

画面側

edit.html


<div class="card-body">
  <div id="error-message" class="text-danger mb-3">{{ error_msg }}</div>

DB更新エラーの場合、画面上にエラーメッセージを表示する。

 

ファイル詳細

data_access.py

import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import User, TodoCategory, Todo
import consts


def get_session():
    """ セッション情報を返却 """
    db_path = os.path.join('sqlite:///', 'db', 'todo.db')
    engine = create_engine(db_path)
    Session = sessionmaker(bind=engine)
    session = Session()

    return session

def find_user(input_name, input_password):
    """ 利用者検索 """
    session = get_session()
    user = session.query(User).filter_by(name=input_name, password=input_password).first()

    return user

def find_todo_all(u_id):
    """ 利用者IDをもとにTODOタスク一覧を取得する """
    session = get_session()
    todo_list = session.query(Todo).filter(Todo.user_id == u_id, Todo.status == consts.STATUS_INCOMPLETE).order_by(Todo.due_date.asc()).all()

    return todo_list

def update_todo_status(t_id):
    """ タスクのステータスを1(完了)に更新する """
    is_valid = True

    session = get_session()
    try:
        todo = session.get(Todo, t_id)
        todo.status = consts.STATUS_COMPLETE
        session.add(todo)
        session.commit()
    except:
        session.rollback()
        is_valid = False
    finally:
        session.close()

    return is_valid

def get_category_all():
    """ カテゴリ一覧取得 """
    session = get_session()
    return session.query(TodoCategory).all()

def insert_todo(todo):
    """ タスクの新規登録 """
    is_valid = True
    
    session = get_session()
    try:
        todo = Todo(
            title = todo["title"],
            content = todo["content"],
            memo = todo["memo"],
            due_date = todo["due_date"],
            category_id = todo["category_id"],
            user_id = todo["user_id"]
        )
        session.add(todo)
        session.commit()
    except Exception as e:
        print(e)
        session.rollback()
        is_valid = False
    finally:
        session.close()

    return is_valid

def get_todo(t_id):
    """ タスク情報取得 """
    session = get_session()
    return session.get(Todo, t_id)

def update_todo(todo_item):
    """ タスクの更新 """
    is_valid = True

    session = get_session()
    todo = session.get(Todo, todo_item["id"])
    if not todo:
        is_valid = False
    else:
        try:
            todo.title = todo_item["title"]
            todo.content = todo_item["content"]
            todo.memo = todo_item["memo"]
            todo.due_date = todo_item["due_date"]
            todo.category_id = todo_item["category_id"]

            session.add(todo)
            session.commit()
        except Exception as e:
            print(e)
            session.rollback()
            is_valid = False
        finally:
            session.close()

    return is_valid

def delete_task(t_id):
    """ タスクの削除 """
    is_valid = True

    session = get_session()
    try:
        session.query(Todo).filter_by(id=t_id).delete()
        session.commit()
    except Exception as e:
        print(e)
        session.rollback()
        is_valid = False
    finally:
        session.close()
    
    return is_valid

 

business_logic.py

import data_access
import consts
from datetime import datetime

def find_user(name, password):
  """ userを取得する """
  # 名前とパスワードからuserオブジェクトを取得
  return data_access.find_user(name, password)

def find_todo_all(u_id):
    """ タスク一覧を取得する """
    # userに紐づくタスク一覧の取得
    todo_list = data_access.find_todo_all(u_id)
    todos = []
    for todo in todo_list:
        # 画面表示用データに整形
        todo_item = {
            "id": todo.id,
            "title": todo.title,
            "category": todo.category.category_name,
            "content": todo.content,
            "due_date": f"{todo.due_date:%Y-%m-%d}",
            "status": "未完了" if todo.status == consts.STATUS_INCOMPLETE else "完了"
        }
        todos.append(todo_item)

    return todos

def update_todo_status(t_id):
    """ タスクを完了にする """
    return data_access.update_todo_status(t_id)

def get_category_all():
    """ カテゴリ一覧取得 """
    return data_access.get_category_all()

def insert_todo(form, u_id):
    """ タスクの新規登録 """
    todo = {
        "title": form.get("title"),
        "content": form.get("content"),
        "memo": form.get("memo", ""),
        "due_date": datetime.strptime(form.get("due_date"), consts.DATE_FORMAT),
        "user_id": u_id,
        "category_id": int(form.get("category"))
    }

    return data_access.insert_todo(todo)

def get_todo(t_id):
    """ タスク情報取得 """
    todo = data_access.get_todo(t_id)
    todo_item = {
        "id": todo.id,
        "title": todo.title,
        "category_id": str(todo.category.id),
        "category": todo.category.category_name,
        "content": todo.content,
        "memo": todo.memo,
        "due_date": f"{todo.due_date:%Y-%m-%d}",
        "status": "未完了" if todo.status == consts.STATUS_INCOMPLETE else "完了"
    }

    return todo_item

def update_todo(form):
    """ タスクの更新 """    
    todo_item = {
        "id": form.get("id"),
        "title": form.get("title"),
        "content": form.get("content"),
        "memo": form.get("memo", ""),
        "due_date": datetime.strptime(form.get("due_date"), consts.DATE_FORMAT),
        "category_id": int(form.get("category"))
    }

    return data_access.update_todo(todo_item)

def delete_task(t_id):
    """ タスクの削除 """
    return data_access.delete_task(t_id)

 

validate_views.py

import business_logic
from datetime import datetime
import consts

def validate_login(form, errors):
    """ ログイン情報の入力チェックを行う """
    is_valid = True
    name = form.get("name")
    password = form.get("password")
    if not name or not password:
        errors.append("名前またはパスワードを入力してください。")
        is_valid = False

    return is_valid

def validate_input_todo(form, error_dict):
    """ タスク登録または編集の入力チェックを行う """
    is_valid = True
    # カテゴリ 必須
    c_id = form.get("category")
    if not c_id:
        error_dict["category"] = "カテゴリを選択してください。"
        is_valid = False
    else:
        # カテゴリ 範囲
        categories = business_logic.get_category_all()
        id_list = list(map(str, [category.id for category in categories]))
        if not c_id in id_list:
            error_dict["category"] = "不正なカテゴリが選択されています。"
            is_valid = False
    # タイトル 必須
    title = form.get("title")
    if not title:
        error_dict["title"] = "タイトルを入力してください。"
        is_valid = False
    # タスク内容 必須
    content = form.get("content")
    if not content:
        error_dict["content"] = "タスク内容を入力してください。"
        is_valid = False
    # タスク期日 必須
    due_date = form.get("due_date")
    if not due_date:
        error_dict["due_date"] = "タスク期日を入力してください。"
        is_valid = False
    else:
        # タスク期日 存在する日付
        try:
            datetime.strptime(due_date, consts.DATE_FORMAT)
        except ValueError:
            error_dict["due_date"] = "存在する日付を入力してください。"
            is_valid = False

    return is_valid

 

views.py

from flask import Flask, render_template, request, redirect, url_for, session
import business_logic
import validate_views
import consts

app = Flask(__name__)
app.secret_key = "yiYKQmFC6MTVKs5THpKkD"

@app.route("/")
def index():
    return redirect(url_for("show_login"))

@app.route("/todo_apps")
def show_login():
    """ ログイン画面表示 """
    return render_template("login.html")

@app.route("/todo_apps/login", methods=["POST"])
def login():
    """ ログイン処理 """
    # 入力値検証
    errors = []
    if not validate_views.validate_login(request.form, errors):
        return render_template("login.html", errors=errors)
    
    # ログイン確認
    name = request.form["name"]
    password = request.form["password"]
    user = business_logic.find_user(name, password)
    if user:
        # ログイン成功
        session["name"] = user.name
        session["u_id"] = user.id
        return redirect(url_for("top"))
    else:
        # ログイン失敗
        errors.append("名前またはパスワードが正しくありません。")
        return render_template("login.html", errors=errors)

@app.route("/todo_apps/top")
def top():
    """ TODOタスク一覧画面表示 """
    # タスク一覧を取得
    todos = business_logic.find_todo_all(session["u_id"])

    return render_template("top.html", todos=todos)

@app.route("/logout")
def logout():
    """ ログアウト """
    session.clear()
    return redirect(url_for("show_login"))

@app.route("/todo_apps/top/task/<int:t_id>")
def update_todo_done(t_id):
    """ タスクを完了にする """
    is_valid = business_logic.update_todo_status(t_id)
    if not is_valid:
        errors = ["更新に失敗しました。時間を空けて再度実行してください。"]
        # タスク一覧を取得
        todos = business_logic.find_todo_all(session["u_id"])

        return render_template("top.html", todos=todos, errors=errors)
    else:
        return redirect(url_for("top"))

@app.route("/todo_apps/create_input")
def show_create():
    """ タスク新規登録画面を表示する """
    categories = business_logic.get_category_all()
    return render_template("create.html", categories=categories)

@app.route("/todo_apps/create_todo", methods=["POST"])
def create_todo():
    """ タスクを新規登録する """
    # 入力値検証
    error_dict = {}
    is_valid = validate_views.validate_input_todo(request.form, error_dict)
    if not is_valid:
        # 入力エラーの場合
        categories = business_logic.get_category_all()
        return render_template("create.html", categories=categories, error_dict=error_dict) 
    else:
        # タスク登録
        is_valid = business_logic.insert_todo(request.form, session["u_id"])
        if not is_valid:
            # DBエラーの場合
            categories = business_logic.get_category_all()
            error_msg = "タスクの登録に失敗しました。時間を空けて再度実行してください。"
            return render_template("create.html", categories=categories, error_msg=error_msg) 
        else:
            return redirect(url_for("top"))

@app.route("/todo_apps/detail/<int:t_id>")
def show_detail(t_id):
    """ タスク詳細画面を表示する """
    todo = business_logic.get_todo(t_id)
    return render_template("detail.html", todo=todo)

@app.route("/todo_apps/edit/<int:t_id>")
def show_edit(t_id):
    """ 編集画面を表示する """
    categories = business_logic.get_category_all()
    todo = business_logic.get_todo(t_id)
    return render_template("edit.html", categories=categories, todo=todo)

@app.route("/todo_apps/edit/done", methods=["POST"])
def edit_todo():
    """ タスクを編集する """
    # 入力値検証
    error_dict = {}
    is_valid = validate_views.validate_input_todo(request.form, error_dict)
    if not is_valid:
        # 入力エラーの場合
        categories = business_logic.get_category_all()
        todo = get_todo(request.form)
        return render_template("edit.html", categories=categories, error_dict=error_dict, todo=todo) 
    else:
        # タスク更新
        is_valid = business_logic.update_todo(request.form)
        if not is_valid:
            # DBエラーの場合
            categories = business_logic.get_category_all()
            error_msg = "タスクの更新に失敗しました。時間を空けて再度実行してください。"
            todo = get_todo(request.form)
            return render_template("edit.html", categories=categories, error_msg=error_msg, todo=todo) 
        else:
            return redirect(url_for("show_detail", t_id=request.form["id"]))

def get_todo(form):
    """ タスク情報を返却する """
    todo = {
        "id": form.get("id"),
        "title": form.get("title", ""),
        "category_id": form.get("category", ""),
        "content": form.get("content", ""),
        "memo": form.get("memo", ""),
        "due_date": form.get("due_date", "")
    }

    return todo

@app.route("/todo_apps/delete/<int:t_id>")
def delete_todo(t_id):
    """ タスクを削除する """
    is_valid = business_logic.delete_task(t_id)
    if not is_valid:
        # 削除エラーの場合
        return redirect(url_for("show_error", error_id=consts.DB_ERROR))
    else:
        return redirect(url_for('top'))

@app.route("/todo_apps/error")
def show_error():
    session.clear()
    error_id = request.args.get("error_id", "")
    if error_id == consts.DB_ERROR:
        # DBエラーの場合
        error_msg = "DBを更新中にエラーが発生しました。時間を空けて、再度ログインからやりなおしてください。"
    else:
        error_msg = "エラーが発生しました。時間を空けて、再度ログインからやりなおしてください。"
    
    return render_template("error.html", error_msg=error_msg)

if __name__ == '__main__':
    # 8080ポートで起動
    app.run(port=8080, debug=True)

 

consts.py

# ステータス:未完了
STATUS_INCOMPLETE = 0
# ステータス:完了
STATUS_COMPLETE = 1

# 日付変換フォーマット
DATE_FORMAT = "%Y-%m-%d"

# エラー画面フラグ
DB_ERROR = "1"

 

top.html

<!DOCTYPE html>
<html>

<head>
  <title>TODOトップ</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css">
</head>

<body>
  <div class="container mt-5">
    <h2 class="text-center">{{ session["name"] }} さんのタスク一覧</h2>
    <a href="{{ url_for('show_create') }}" class="btn btn-primary mr-3">タスク登録</a>
    <a href="{{ url_for('logout') }}" class="btn btn-primary">ログアウト</a>
    <div class="card mt-4">
      <div class="card-body">
        {% for msg in errors %}
        <div id="error-message" class="text-danger mb-3">{{ msg }}</div>
        {% endfor %}
        <table class="table table-striped">
          <thead>
            <tr>
              <th>カテゴリ</th>
              <th>タイトル</th>
              <th>タスク名</th>
              <th>期日</th>
              <th>ステータス</th>
              <th>アクション</th>
            </tr>
          </thead>
          <tbody>
            {% for todo_item in todos %}
            <tr>
              <td>{{ todo_item["category"] }}</td>
              <td>{{ todo_item["title"] }}</td>
              <td><a href="{{ url_for('show_detail', t_id=todo_item['id']) }}">{{ todo_item["content"] }}</a></td>
              <td>{{ todo_item["due_date"] }}</td>
              <td>{{ todo_item["status"] }}</td>
              <td><a href="{{ url_for('update_todo_done', t_id=todo_item['id']) }}" class="btn btn-success">完了</a></td>
            </tr>
            {% endfor %}
          </tbody>
        </table>
      </div>
    </div>
  </div>
</body>

</html>

 

detail.html

<!DOCTYPE html>
<html>

<head>
  <title>TODO詳細</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css">
</head>

<body>
  <div class="container mt-2">
    <a href="{{ url_for('top') }}" class="btn btn-primary mb-3">戻る</a>
    <h2 class="text-center">TODO詳細</h2>
    <div class="card mt-4 border">
      <div class="card-body">
        <div class="row border p-2 mb-2">
          <div class="col-sm-3"><h5>カテゴリ:</h5></div>
          <div class="col-sm-9"><p>{{ todo["category"] }}</p></div>
        </div>
        <div class="row border p-2 mb-2">
          <div class="col-sm-3"><h5>タイトル:</h5></div>
          <div class="col-sm-9"><p>{{ todo["title"] }}</p></div>
        </div>
        <div class="row border p-2 mb-2">
          <div class="col-sm-3"><h5>タスク内容:</h5></div>
          <div class="col-sm-9"><p>{{ todo["content"] }}</p></div>
        </div>
        <div class="row border p-2 mb-2">
          <div class="col-sm-3"><h5>ステータス:</h5></div>
          <div class="col-sm-9"><p>{{ todo["status"] }}</p></div>
        </div>
        <div class="row border p-2 mb-2">
          <div class="col-sm-3"><h5>メモ:</h5></div>
          <div class="col-sm-9">
            {% if todo["memo"] %}
            <p>{{ todo["memo"].replace("\r\n", "<br>") | safe }}</p>
            {% else %}
            <p></p>
            {% endif %}
          </div>
        </div>
        <div class="row border p-2 mb-2">
          <div class="col-sm-3"><h5>期日:</h5></div>
          <div class="col-sm-9"><p>{{ todo["due_date"] }}</p></div>
        </div>
        <div class="d-flex justify-content-center mt-2">
          <a href="{{ url_for('show_edit', t_id=todo['id']) }}" class="btn btn-primary mb-3 mr-2">編集する</a>
          <a href="{{ url_for('delete_todo', t_id=todo['id']) }}" class="btn btn-danger mb-3">削除する</a>
        </div>
      </div>
    </div>
  </div>
</body>

</html>

 

edit.html

<!DOCTYPE html>
<html>

<head>
  <title>タスク編集</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css">
</head>

<body>
  <div class="container">
    <div class="row justify-content-center mt-3">
      <div class="col-md-8">
        <a href="{{ url_for('show_detail', t_id=todo.get('id')) }}" class="btn btn-primary mb-3">戻る</a>
        <div class="card">
          <div class="card-header text-center">
            <h4>タスク編集</h4>
          </div>
          <div class="card-body">
            <div id="error-message" class="text-danger mb-3">{{ error_msg }}</div>
            <form method="post" action="{{ url_for('edit_todo') }}">
              <div class="form-group">
                <label for="category">カテゴリ<span class="text-primary ml-3">※必須</span></label>
                <select class="form-control" id="category" name="category">
                  <option value="">選択してください</option>
                  {% for category in categories %}
                  <option value="{{category.id}}" 
                  {% if category.id|string()==todo.get('category_id') %} selected {% endif %}>{{ category.category_name }}</option>
                  {% endfor %}
                </select>
              </div>
              {% if error_dict and error_dict["category"] %}
              <div id="error-message" class="text-danger mb-3">{{ error_dict["category"] }}</div>
              {% endif %}
              <div class="form-group mt-3">
                <label for="title">タイトル <span class="text-primary ml-3">※必須</span></label>
                <input type="text" class="form-control" id="title" name="title"
                  value="{{ todo.get('title', '') }}">
              </div>
              {% if error_dict and error_dict["title"] %}
              <div id="error-message" class="text-danger mb-3">{{ error_dict["title"] }}</div>
              {% endif %}
              <div class="form-group mt-3">
                <label for="task">タスク内容 <span class="text-primary ml-3">※必須</span></label>
                <input type="text" class="form-control" id="content" name="content"
                  value="{{ todo.get('content', '') }}">
              </div>
              {% if error_dict and error_dict["content"] %}
              <div id="error-message" class="text-danger mb-3">{{ error_dict["content"] }}</div>
              {% endif %}
              <div class="form-group mt-3">
                <label for="memo">メモ</label>
                <textarea class="form-control" id="memo" name="memo"
                  rows="3">{{ todo.get('memo', '') }}</textarea>
              </div>
              <div class="form-group mt-3">
                <label for="due_date">タスク期日<span class="text-primary ml-3">※必須</span></label>
                <input type="date" class="form-control" id="due_date" name="due_date"
                  value="{{ todo.get('due_date', '') }}">
              </div>
              {% if error_dict and error_dict["due_date"] %}
              <div id="error-message" class="text-danger mb-3">{{ error_dict["due_date"] }}</div>
              {% endif %}
              <button type="submit" class="btn btn-primary mt-3">更新</button>
              <input type="hidden" name="id" value="{{ todo.get('id') }}">
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>

</html>

 

error.html

<!DOCTYPE html>
<html>

<head>
  <title>処理続行エラー</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css">
</head>

<body>
  <div class="container mt-5 p-3 card border">
    <h2 class="text-center">処理続行エラー</h2>
    <div class="mt-4">
      <div class="card-body">
        <div class="p-2 mb-3 text-center">
          <p>{{ error_msg }}</p>
        </div>
        <div class="d-flex justify-content-center mt-3">
          <a href="{{ url_for('show_login') }}" class="btn btn-primary mb-3">ログイン</a>
        </div>
      </div>
    </div>
  </div>
</body>

</html>

 

 

スポンサーリンク