はじめに#
「OKAZAKI Shogo のひとりアドベントカレンダー2024」の12日目です。 昨日導入した Flask-Login のライブラリを用いて、管理者向けログインページと、その先のページ技能を完成させていきますが、その前に、ここまで作成したものの整理を行なっていきます。
画面遷移と機能を整理する#
少し内容が増えてきたので、画面遷移と内容を整理する。
今回、新たに増やすのは「管理者向けログイン」のページ。あとは、これまで実装してきたものを表示するが、赤色で囲った範囲の画面はログインを必要とする。
少しここまで作成してきたものが命名などについてもややこしくなってきたので、整理する。 また、ヘッダーやフッターも設定して、画面遷移が想定通りできるようにリンクを張る。 ログイン・ログアウトのページなどについてはこのあと実装する。
構成(完成形)#
1.
2|-- Makefile
3|-- app
4| |-- __init__.py
5| |-- app.py
6| |-- index.cgi
7| |-- models
8| | |-- __init__.py
9| | |-- file.py
10| | `-- manager_user.py
11| |-- static
12| | `-- css
13| | `-- default.css
14| |-- templates
15| | |-- files.html
16| | |-- index.html
17| | |-- layout-general.html
18| | |-- layout-manager.html
19| | |-- login.html <= 新規追加
20| | |-- manage-file-list.html
21| | |-- manage-file.html
22| | |-- manage-menu.html
23| | `-- regist_file.html
24| `-- views
25| |-- files.py
26| |-- index.py
27| |-- login.py <= 新規追加
28| |-- logout.py <= 新規追加
29| |-- manage_file.py
30| |-- manage_file_list.py
31| |-- manage_menu.py
32| |-- regist_file.py
33| `-- update_file.py
34|-- db
35| `-- bshssa_member_sys.db
36|-- documents
37|-- instance
38| |-- __init__.py
39| `-- config
40| `-- dev.py
41|-- poetry.lock
42|-- pyproject.toml
43`-- tools
44 `-- generate_password_hash.py
アプリケーション全体の機能#
app/__init__.py
#
1import os
2
3import flask_login
4from flask import Flask
5from flask_sqlalchemy import SQLAlchemy
6
7db = SQLAlchemy()
8login_manager = flask_login.LoginManager()
9
10
11def create_app():
12 # appの設定
13 app = Flask(__name__, instance_relative_config=True)
14
15 # configファイルを読み込む
16 config_path = os.path.join("config", "dev.py")
17 app.config.from_pyfile(config_path)
18
19 # DB の設定
20 db.init_app(app)
21
22 # flask-login の初期化
23 login_manager.init_app(app)
24
25 # Blueprint の登録
26 from app.views.files import files_bp
27 from app.views.index import index_bp
28 from app.views.login import login_bp
29 from app.views.logout import logout_bp
30 from app.views.manage_file import manage_file_bp
31 from app.views.manage_file_list import manage_file_list_bp
32 from app.views.manage_menu import manage_menu_bp
33 from app.views.regist_file import regist_file_bp
34 from app.views.update_file import update_file_bp
35
36 app.register_blueprint(index_bp)
37 app.register_blueprint(files_bp)
38 app.register_blueprint(regist_file_bp)
39 app.register_blueprint(manage_file_bp)
40 app.register_blueprint(manage_file_list_bp)
41 app.register_blueprint(update_file_bp)
42 app.register_blueprint(manage_menu_bp)
43 app.register_blueprint(login_bp)
44 app.register_blueprint(logout_bp)
45
46 return app
app/app.py
#
1from flask import flash, redirect, url_for
2from werkzeug.exceptions import RequestEntityTooLarge
3
4from app import create_app
5
6app = create_app()
7
8
9if __name__ == "__main__":
10 app.run()
11
12
13@app.errorhandler(RequestEntityTooLarge)
14def handle_over_max_file_size(error):
15 flash("ファイルのサイズが大きすぎます。 50 MB 以下のファイルを選択してください。")
16 return redirect(url_for("regist_file_form.index"))
レイアウト(一般向け)#
app/templates/layout-general.html
1<!DOCTYPE html>
2<html lang="ja">
3 <head>
4 <meta charset="utf-8">
5 <title>{{ page_title }} | ボーイスカウト阪神さくら地区 加盟員向けページ 管理者画面</title>
6 <link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}"/>
7 </head>
8
9 <body>
10 <!----- ヘッダー ----->
11 <header>ボーイスカウト阪神さくら地区 加盟員向けページ 管理者画面</header>
12 <nav>
13 <ul>
14 <li><a href="{{ url_for('manage_menu.index') }}">管理者向けメニュー</a></li>
15 <li><a href="{{ url_for('regist_file.index') }}">文書登録</a></li>
16 <li><a href="{{ url_for('manage_file_list.index') }}">文書編集</a></li>
17 <li><a href="{{ url_for('logout.index') }}">ログアウト</a></li>
18 </ul>
19 </nav>
20 <!----- ヘッダー END ----->
21
22 {% block content %}{% endblock %}
23
24 <!----- フッター ----->
25 <footer>© Hanshin-Sakura District, Hyogo Scout Council, SAJ.</footer>
26 <!----- フッター END ----->
27 </body>
28</html>
レイアウト(管理者向け)#
app/templates/layout-manager.html
1<!DOCTYPE html>
2<html lang="ja">
3 <head>
4 <meta charset="utf-8">
5 <title>{{ page_title }} | ボーイスカウト阪神さくら地区 加盟員向けページ 管理者画面</title>
6 <link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}"/>
7 </head>
8
9 <body>
10 <!----- ヘッダー ----->
11 <header>ボーイスカウト阪神さくら地区 加盟員向けページ 管理者画面</header>
12 <nav>
13 <ul>
14 <li><a href="{{ url_for('manage_menu.index') }}">管理者向けメニュー</a></li>
15 <li><a href="{{ url_for('regist_file.index') }}">文書登録</a></li>
16 <li><a href="{{ url_for('manage_file_list.index') }}">文書編集</a></li>
17 <li><a href="{{ url_for('logout.index') }}">ログアウト</a></li>
18 </ul>
19 </nav>
20 <!----- ヘッダー END ----->
21
22 {% block content %}{% endblock %}
23
24 <!----- フッター ----->
25 <footer>© Hanshin-Sakura District, Hyogo Scout Council, SAJ.</footer>
26 <!----- フッター END ----->
27 </body>
28</html>
トップページ#
app/templates/index.html
#
1{% extends "layout-general.html" %}
2
3{% block content %}
4
5<!----- メインコンテンツ ----->
6<article>
7 <h2>ボーイスカウト阪神さくら地区 加盟員向けページ</h2>
8 <section>
9 <ul>
10 <li><a href="{{ url_for('files.index') }}">文書館</a></li>
11 <li><a href="{{ url_for('login.index') }}">管理者向け</a></li>
12 </ul>
13 </section>
14</article>
15<!----- メインコンテンツ END ----->
16
17{% endblock %}
app/views/index.py
#
1from flask import Blueprint, render_template
2
3index_bp = Blueprint("index", __name__, url_prefix="/")
4
5
6@index_bp.route("/", methods=["GET", "POST"])
7def index():
8 return render_template("index.html", page_title="TOP")
文書館(ファイル一覧表示)#
app/templates/files.html
#
1{% extends "layout-general.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>{{ file.file_id }}</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 %}
app/views/files.py
#
1from flask import Blueprint, render_template
2
3from app.models.file import File
4
5files_bp = Blueprint("files", __name__, url_prefix="/files")
6
7
8@files_bp.route("/", methods=["GET", "POST"])
9def index():
10 files = File.query.all()
11 return render_template("files.html", files=files, page_title="文書館")
管理者向けメニュー#
app/templates/manage-menu.html
#
1{% extends "layout-manager.html" %}
2
3{% block content %}
4<div>
5 <h2>{{ page_title }}</h2>
6 <ul>
7 <li><a href="{{ url_for('regist_file.index') }}">文書登録</a></li>
8 <li><a href="{{ url_for('manage_file_list.index') }}">文書編集</a></li>
9 <li><a href="{{ url_for('logout.index') }}">ログアウト</a></li>
10 </ul>
11</div>
12{% endblock %}
app/views/manage_menu.py
#
1from flask import Blueprint, render_template
2from flask_login import login_required
3
4manage_menu_bp = Blueprint("manage_menu", __name__, url_prefix="/manage-menu")
5
6
7@manage_menu_bp.route("/", methods=["GET"])
8def index():
9 return render_template("manage-menu.html", page_title="管理者向けメニュー")
文書登録画面#
app/templates/regist-file.html
#
1{% extends "layout-manager.html" %}
2
3{% block content %}
4<h2>{{ page_title }}</h2>
5<form method="post" action="/regist-file" enctype=multipart/form-data class="regist-file-form">
6 {% if filename %}
7 <div>
8 ファイル: {{ filename }} のアップロードが完了しました!
9 </div>
10 {% endif %}
11
12 {% with messages = get_flashed_messages() %}
13 {% if messages %}
14 <ul>
15 {% for message in messages %}
16 <li>{{ message }}</li>
17 {% endfor %}
18 </ul>
19 {% endif %}
20 {% endwith %}
21
22 <div class="regist-file-form">
23 <input type="file" name="file" id="file" />
24 <br>アップロードするファイル名を入力するか、ボタンを押してファイルを選択してください
25 </div>
26 <div class="regist-file-form">
27 <label for="title">タイトル: </label>
28 <input type="text" name="title" id="title" />
29 </div>
30 <div class="regist-file-form">
31 <label for="description">説明: </label>
32 <input type="text" name="description" id="description" />
33 <br>タイトルで内容が十分わかるときは、説明を入力しなくても構いません。
34 </div>
35 <div class="regist-file-form">
36 <input type="submit" value="送信" />
37 </div>
38</form>
39{% endblock %}
app/views/regist_file.py
#
1import os
2from datetime import datetime, timedelta, timezone
3from typing import Optional
4
5from flask import Blueprint, flash, render_template, request
6from flask_login import login_required
7from werkzeug.datastructures import FileStorage
8from werkzeug.utils import secure_filename
9
10from app import db
11from app.models.file import File
12from instance.config import dev
13
14JST = timezone(timedelta(hours=+9), "JST")
15
16regist_file_bp = Blueprint("regist_file", __name__, url_prefix="/regist-file")
17
18
19def __get_file_extension_if_allowed(filename: str) -> Optional[str]:
20 # ファイル名に拡張子が含まれているかチェック
21 if "." in filename:
22 # 拡張子を取得(ドット以降)
23 extension = filename.rsplit(".", 1)[1].lower()
24 # 許容される拡張子かどうかを確認
25 if extension in dev.ALLOWED_EXTENSIONS:
26 return extension
27 return None
28
29
30def __get_file_size(uploaded_file: FileStorage) -> str:
31 file_size_bytes = len(uploaded_file.read())
32 # ファイルの読み取り位置をリセット
33 uploaded_file.stream.seek(0)
34
35 # ファイルサイズを適切な単位で表示
36 if file_size_bytes >= 1_000_000:
37 size_str = f"{file_size_bytes / 1_000_000:.2f} MB"
38 elif file_size_bytes >= 1_000:
39 size_str = f"{file_size_bytes / 1_000:.2f} KB"
40 else:
41 size_str = f"{file_size_bytes} bytes"
42
43 return size_str
44
45
46@regist_file_bp.route("/", methods=["GET", "POST"])
47def index():
48 if request.method == "POST":
49 is_file_check_ok = True
50 file = request.files["file"]
51
52 if not file:
53 flash("ファイルが選択されていません")
54 is_file_check_ok = False
55
56 if request.form["title"] == "":
57 flash("タイトルが入力されていません")
58 is_file_check_ok = False
59
60 origin_filename = file.filename
61 ext = __get_file_extension_if_allowed(origin_filename)
62 if ext is None:
63 flash("ファイルの拡張子が正しくありません")
64 is_file_check_ok = False
65
66 if is_file_check_ok:
67 now = datetime.now(JST)
68 filename = secure_filename(now.strftime("%Y%m%d_%H%M%S_") + origin_filename)
69 now_str = now.strftime("%Y-%m-%d %H:%M:%S")
70
71 title = request.form["title"]
72 description = request.form["description"]
73 file_size = __get_file_size(file)
74
75 upload_file = File(
76 file_name=filename,
77 display_name=title,
78 url=f"http://example.com/{filename}",
79 file_type=ext,
80 size=file_size,
81 description=description,
82 tag="example, test",
83 is_standard=0,
84 created_at=now_str,
85 created_by="admin",
86 updated_at=now_str,
87 updated_by="admin",
88 )
89
90 db.session.add(upload_file)
91 db.session.commit()
92
93 file.save(os.path.join(dev.UPLOAD_FOLDER, filename))
94 flash(f"{origin_filename}のアップロードが完了しました!")
95
96 return render_template("regist-file.html", page_title="文書登録")
文書情報編集メニュー#
app/templates/manage-file-list.html
#
1{% extends "layout-manager.html" %}
2
3{% block content %}
4<div>
5 <h2>{{ page_title }}</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 %}
app/views/manage_file_list.py
#
1from flask import Blueprint, render_template
2from flask_login import login_required
3
4from app.models.file import File
5
6manage_file_list_bp = Blueprint("manage_file_list", __name__, url_prefix="/manage-file-list")
7
8
9@manage_file_list_bp.route("/", methods=["GET"])
10@login_required
11def index():
12 files = File.query.all()
13 return render_template("manage-file-list.html", files=files, page_title="文書一覧")
文書編集画面#
app/templates/manage-file.html
#
1{% extends "layout-manager.html" %}
2
3{% block content %}
4<div>
5 <h2>{{ page_title }}</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 %}
app/views/manage_file.py
#
1from flask import Blueprint, render_template, request
2from flask_login import login_required
3
4from app.models.file import File
5
6manage_file_bp = Blueprint("manage_file", __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, page_title="文書編集")
ここまでの成果物としては以上のとおり。明日、ログイン機能を作成する。