メインコンテンツへスキップ

Flask で DB のレコードを追加・編集する機能を実装する

·4 分
ひとりアドベントカレンダー2024 Python Flask

はじめに
#

「OKAZAKI Shogo のひとりアドベントカレンダー2024」の10日目です。 ファイルアップロードすると同時に、 DB へのレコード登録をする機能と、登録したファイル情報の編集する機能を実装します。

アップロードされたファイル情報を登録する(INSERT)
#

regist_file.py に以下の機能を追加する:

  • アップロードされたファイルサイズを計算する(__get_file_size()
  • 現在時刻を取得し、登録日時と更新日時にセットする
  • 入力された情報と日時の情報を File オブジェクトにセットし、DB に登録する
    • db.session.add() で INSERT 文発行相当の処理を行う
    • db.session.commit() でトランザクションをコミットする
 1from typing import Optional
 2from datetime import datetime, timedelta, timezone
 3import os
 4from flask import Blueprint, request, flash, redirect, url_for
 5from werkzeug.utils import secure_filename
 6from werkzeug.datastructures import FileStorage
 7from app.models.file import File
 8from instance.config import dev
 9from app import db
10
11JST = timezone(timedelta(hours=+9), "JST")
12
13regist_file_bp = Blueprint("regist_file", __name__, url_prefix="/regist-file")
14
15
16def __get_file_extension_if_allowed(filename: str) -> Optional[str]:
17    # ファイル名に拡張子が含まれているかチェック
18    if "." in filename:
19        # 拡張子を取得(ドット以降)
20        extension = filename.rsplit(".", 1)[1].lower()
21        # 許容される拡張子かどうかを確認
22        if extension in dev.ALLOWED_EXTENSIONS:
23            return extension
24    return None
25
26# ---------- ここから新規追加 -----------
27def __get_file_size(uploaded_file: FileStorage) -> str:
28    file_size_bytes = len(uploaded_file.read())
29    # ファイルの読み取り位置をリセット
30    uploaded_file.stream.seek(0)
31
32    # ファイルサイズを適切な単位で表示
33    if file_size_bytes >= 1_000_000:
34        size_str = f"{file_size_bytes / 1_000_000:.2f} MB"
35    elif file_size_bytes >= 1_000:
36        size_str = f"{file_size_bytes / 1_000:.2f} KB"
37    else:
38        size_str = f"{file_size_bytes} bytes"
39
40    return size_str
41# ---------- ここまで新規追加 -----------
42
43@regist_file_bp.route("/", methods=["POST"])
44def index():
45    is_file_check_ok = True
46    file = request.files["file"]
47
48    if not file:
49        flash("ファイルが選択されていません")
50        is_file_check_ok = False
51
52    if request.form["title"] == "":
53        flash("タイトルが入力されていません")
54        is_file_check_ok = False
55
56    origin_filename = file.filename
57    ext = __get_file_extension_if_allowed(origin_filename)
58    if ext is None:
59        flash("ファイルの拡張子が正しくありません")
60        is_file_check_ok = False
61
62    if is_file_check_ok:
63        # ---------- ここから新規追加 -----------
64        now = datetime.now(JST)
65        filename = secure_filename(now.strftime("%Y%m%d_%H%M%S_") + origin_filename)
66        now_str = now.strftime("%Y-%m-%d %H:%M:%S")
67
68        title = request.form["title"]
69        description = request.form["description"]
70        file_size = __get_file_size(file)
71
72        upload_file = File(
73            file_name=filename,
74            display_name=title,
75            url=f"http://example.com/{filename}",
76            file_type=ext,
77            size=file_size,
78            description=description,
79            tag="example, test",
80            is_standard=0,
81            created_at=now_str,
82            created_by="admin",
83            updated_at=now_str,
84            updated_by="admin",
85        )
86
87        db.session.add(upload_file)
88        db.session.commit()
89        # ---------- ここまで新規追加 -----------
90
91        file.save(os.path.join(dev.UPLOAD_FOLDER, filename))
92        flash(f"{origin_filename}のアップロードが完了しました!")
93
94    return redirect(url_for("regist_file_form.index"))

ファイル情報の編集画面を作成する(SELECT, UPDATE)
#

管理者用画面で以下のような遷移で編集できるようにする

  1. ファイル一覧を表示
  2. 選択したファイルを選択するとそのファイル編集画面に遷移する
  3. ファイル編集画面で「送信」を押すと、ファイル情報を更新する

先に完成した時の動作例を示す:

動作例1

↑で「1」のリンクをクリック

動作例2

動作例3

説明を更新した上で「送信」をクリック

動作例4

一覧に戻ると説明が更新されている

動作例5

manage_file_list.html
#

管理者向けファイル一覧を表示するテンプレートを作成する。

ID のところは次のページに GET パラメータとして ID を渡して選択したファイルの情報を表示できるようにする。

 1{% extends "layout.html" %}
 2
 3{% block content %}
 4<div>
 5    <h2>ファイル一覧(管理者向け)</h2>
 6    <table>
 7        <thead>
 8            <tr>
 9            <th scope="col"><font>No</font></th>
10            <th scope="col"><font>登録日</font></th>
11            <th scope="col"><font>更新日</font></th>
12            <th scope="col"><font>ファイル種類</font><br><font>サイズ</font></th>
13            <th scope="col"><font>タイトル</font><br><font size="2">説明</font></th>
14            </tr>
15        </thead>
16        <tbody>
17            {% for file in files %}
18            <tr>
19                <td><a href="/manage-file?id={{ file.file_id }}">{{ file.file_id }}</a></td>
20                <td>{{ file.created_at }}</td>
21                <td>{{ file.updated_at }}</td>
22                <td>{{ file.file_type }}<br>{{ file.size }}</td>
23                <td><a href={{ file.url }}>{{ file.display_name }}</a><br>{{ file.description }}</td>
24            </tr>
25            {% endfor %}
26        </tbody>
27    </table>
28</div>
29{% endblock %}

manage_file_list.py
#

単純に、先ほどのページを表示できるようにしているだけ。

 1from flask import Blueprint, render_template
 2
 3from app.models.file import File
 4
 5
 6manage_file_list_bp = Blueprint("manage_file_list_bp", __name__, url_prefix="/manage-file-list")
 7
 8
 9@manage_file_list_bp.route("/", methods=["GET"])
10def index():
11    files = File.query.all()
12    return render_template("manage_file_list.html", files=files)

manage_file.html
#

選択したファイルを編集できる画面。 GET パラメータで受け取った ID のファイルを表示する。フォームのアクションは /update-file を指定する。

 1{% extends "layout.html" %}
 2
 3{% block content %}
 4<div>
 5    <h2>ファイル編集(管理者向け)</h2>
 6    <form method="post" action="/update-file" enctype=multipart/form-data class="update-file-form">
 7        {% with messages = get_flashed_messages() %}
 8        {% if messages %}
 9        <ul>
10          {% for message in messages %}
11          <li>{{ message }}</li>
12          {% endfor %}
13        </ul>
14        {% endif %}
15        {% endwith %}
16        
17        <div class="regist-file-form">
18            <input type="hidden" name="file_id" id="file_id" value={{ file.file_id }} />
19            <label for="file_id">ファイル ID: </label><font>{{ file.file_id }}</font>
20        </div>
21        <div class="regist-file-form">
22          <label for="title">タイトル: </label>
23          <input type="text" name="title" id="title" value={{ file.display_name }} />
24        </div>
25        <div class="regist-file-form">
26          <label for="description">説明: </label>
27          <input type="text" name="description" id="description" value={{ file.description }} />
28          <br>タイトルで内容が十分わかるときは、説明を入力しなくても構いません。
29        </div>
30        <div class="regist-file-form">
31          <input type="submit" value="送信" />
32        </div>
33    </form>
34</div>
35{% endblock %}

manage_file.py
#

request.args.get("id") で GET パラメータ id で指定された値を取得する。指定された値を元にファイル情報を取得する。

  • File.query.filter(File.file_id == file_id).all()[0] で指定した ID のファイル情報を取得する(SELECT 文を発行する処理に相当)。
    • 今回は 1 件しか取得できない前提。
 1from flask import Blueprint, render_template, request
 2
 3from app.models.file import File
 4
 5
 6manage_file_bp = Blueprint("manage_file_bp", __name__, url_prefix="/manage-file")
 7
 8
 9@manage_file_bp.route("/", methods=["GET"])
10def index():
11    file_id = request.args.get("id")
12    file = File.query.filter(File.file_id == file_id).all()[0]
13    return render_template("manage_file.html", file=file, file_id=id)

update_file.py
#

編集画面から送信された内容を処理する。

  • file.[パラメータ名] = [値] で当該フィールドの内容を更新する
  • 更新した内容は db.session.commit() でコミットされる
 1from datetime import datetime, timedelta, timezone
 2from flask import Blueprint, flash, redirect, render_template, request, url_for
 3
 4from app import db
 5from app.models.file import File
 6
 7
 8JST = timezone(timedelta(hours=+9), "JST")
 9
10
11update_file_bp = Blueprint("update_file", __name__, url_prefix="/update-file")
12
13
14@update_file_bp.route("/", methods=["POST"])
15def index():
16    # 更新したいデータを取得
17    file_id = request.form["file_id"]
18    print(file_id)
19    file = File.query.get(file_id)
20    if not file:
21        return "File not found", 404
22
23    # リクエストデータから更新内容を取得
24    title = request.form["title"]
25    description = request.form["description"]
26
27    # 更新内容でデータを更新
28    file.display_name = title
29    file.description = description
30    file.updated_at = datetime.now(JST).strftime("%Y-%m-%d %H:%M:%S")
31
32    # データベースにコミット
33    db.session.commit()
34
35    flash(f"ファイル ID: {file.file_id}の更新が完了しました!")
36
37    return redirect(url_for("manage_file_bp.index", id=file_id))

参考資料
#