Skip to content

Commit eeea77c

Browse files
committed
refactor: Implement session-based flash messaging for improved user feedback
1 parent 1c36792 commit eeea77c

File tree

1 file changed

+145
-51
lines changed

1 file changed

+145
-51
lines changed

app.py

Lines changed: 145 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from flask import Flask, render_template, request, redirect, url_for, send_file
1+
from flask import Flask, render_template, request, redirect, url_for, send_file, session
22
import csv
33
import os
44
import random
5-
from datetime import datetime
5+
from datetime import datetime, timedelta
66
import io
77
import base64
8+
import time
89
from reportlab.lib.pagesizes import A4
910
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
1011
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
@@ -15,22 +16,74 @@
1516
from reportlab.lib import colors
1617

1718
app = Flask(__name__)
18-
# secret_key は不要です(flash()を使用せず、URLパラメータでメッセージを渡すため)
19+
app.secret_key = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
20+
app.permanent_session_lifetime = timedelta(minutes=30)
1921

2022
# データセット保存ディレクトリ
2123
DATASETS_DIR = 'datasets'
2224

25+
def set_flash_message(message, message_type='info'):
26+
"""セッションにメッセージを設定"""
27+
if 'flash_messages' not in session:
28+
session['flash_messages'] = {}
29+
if 'message_counter' not in session:
30+
session['message_counter'] = 0
31+
32+
# メッセージID生成
33+
session['message_counter'] += 1
34+
message_id = f"msg_{session['message_counter']:03d}"
35+
36+
# 期限切れメッセージをクリーンアップ
37+
cleanup_expired_messages()
38+
39+
# メッセージ数制限(10件まで)
40+
if len(session['flash_messages']) >= 10:
41+
# 最も古いメッセージを削除
42+
oldest_id = min(session['flash_messages'].keys())
43+
del session['flash_messages'][oldest_id]
44+
45+
# メッセージを保存
46+
current_time = time.time()
47+
session['flash_messages'][message_id] = {
48+
'text': message,
49+
'type': message_type,
50+
'timestamp': current_time,
51+
'expires_at': current_time + 300 # 5分後に期限切れ
52+
}
53+
session.permanent = True
54+
55+
return message_id
56+
57+
def get_flash_message():
58+
"""セッションからメッセージを取得して削除"""
59+
cleanup_expired_messages()
60+
61+
if 'flash_messages' not in session or not session['flash_messages']:
62+
return '', 'info'
63+
64+
# 最新のメッセージを取得
65+
message_id = max(session['flash_messages'].keys())
66+
message_data = session['flash_messages'].pop(message_id)
67+
68+
return message_data['text'], message_data['type']
69+
70+
def cleanup_expired_messages():
71+
"""期限切れメッセージを削除"""
72+
if 'flash_messages' not in session:
73+
return
74+
75+
current_time = time.time()
76+
expired_ids = [
77+
msg_id for msg_id, msg_data in session['flash_messages'].items()
78+
if current_time > msg_data['expires_at']
79+
]
80+
81+
for msg_id in expired_ids:
82+
del session['flash_messages'][msg_id]
83+
2384
def get_message_and_type(request):
24-
"""URLパラメータからメッセージとタイプを取得"""
25-
msg = request.args.get('msg')
26-
error = request.args.get('error')
27-
28-
if error:
29-
return error, 'error'
30-
elif msg:
31-
return msg, 'success'
32-
else:
33-
return '', ''
85+
"""セッションからメッセージとタイプを取得"""
86+
return get_flash_message()
3487

3588
# 日本語フォントの設定
3689
def setup_fonts():
@@ -305,22 +358,26 @@ def save_dataset_route():
305358
name = request.form.get('name')
306359

307360
if not name:
308-
return redirect(url_for('create_dataset', error='データセット名を入力してください。'))
361+
set_flash_message('データセット名を入力してください。', 'error')
362+
return redirect(url_for('create_dataset'))
309363

310364
filename = f"{name}.csv"
311365

312366
# 重複チェック
313367
if os.path.exists(os.path.join(DATASETS_DIR, filename)):
314-
return redirect(url_for('create_dataset', error=f'データセット "{name}" は既に存在します。別の名前を使用してください。'))
368+
set_flash_message(f'データセット "{name}" は既に存在します。別の名前を使用してください。', 'error')
369+
return redirect(url_for('create_dataset'))
315370

316371
# 拡張フォーマット: 番号,質問,回答,正解数,総試行回数,習熟度スコア
317372
fieldnames = ['番号', '質問', '回答', '正解数', '総試行回数', '習熟度スコア']
318373
data = []
319374

320375
if save_dataset(filename, data, fieldnames):
321-
return redirect(url_for('edit_dataset', filename=filename, msg='データセットを作成しました。'))
376+
set_flash_message('データセットを作成しました。', 'success')
377+
return redirect(url_for('edit_dataset', filename=filename))
322378
else:
323-
return redirect(url_for('create_dataset', error='データセットの作成に失敗しました。'))
379+
set_flash_message('データセットの作成に失敗しました。', 'error')
380+
return redirect(url_for('create_dataset'))
324381

325382
@app.route('/edit_dataset/<filename>')
326383
def edit_dataset(filename):
@@ -362,14 +419,17 @@ def add_item(filename):
362419

363420
# 空のフィールドチェック(必須フィールドのみ)
364421
if not new_item['質問'] or not new_item['回答']:
365-
return redirect(url_for('edit_dataset', filename=filename, error='質問と回答を入力してください。'))
422+
set_flash_message('質問と回答を入力してください。', 'error')
423+
return redirect(url_for('edit_dataset', filename=filename))
366424

367425
data.append(new_item)
368426

369427
if save_dataset(filename, data, fieldnames):
370-
return redirect(url_for('edit_dataset', filename=filename, msg='アイテムを追加しました。'))
428+
set_flash_message('アイテムを追加しました。', 'success')
429+
return redirect(url_for('edit_dataset', filename=filename))
371430
else:
372-
return redirect(url_for('edit_dataset', filename=filename, error='アイテムの追加に失敗しました。'))
431+
set_flash_message('アイテムの追加に失敗しました。', 'error')
432+
return redirect(url_for('edit_dataset', filename=filename))
373433

374434
@app.route('/delete_item/<filename>/<int:index>')
375435
def delete_item(filename, index):
@@ -387,11 +447,14 @@ def delete_item(filename, index):
387447
item['番号'] = i + 1
388448

389449
if save_dataset(filename, data, fieldnames):
390-
return redirect(url_for('edit_dataset', filename=filename, msg='アイテムを削除しました。'))
450+
set_flash_message('アイテムを削除しました。', 'success')
451+
return redirect(url_for('edit_dataset', filename=filename))
391452
else:
392-
return redirect(url_for('edit_dataset', filename=filename, error='アイテムの削除に失敗しました。'))
453+
set_flash_message('アイテムの削除に失敗しました。', 'error')
454+
return redirect(url_for('edit_dataset', filename=filename))
393455
else:
394-
return redirect(url_for('edit_dataset', filename=filename, error='無効なアイテムです。'))
456+
set_flash_message('無効なアイテムです。', 'error')
457+
return redirect(url_for('edit_dataset', filename=filename))
395458

396459
@app.route('/input_results/<filename>')
397460
def input_results(filename):
@@ -401,7 +464,8 @@ def input_results(filename):
401464
message, message_type = get_message_and_type(request)
402465

403466
if not data:
404-
return redirect(url_for('index', error='データセットが空です。'))
467+
set_flash_message('データセットが空です。', 'error')
468+
return redirect(url_for('index'))
405469

406470
return render_template('input_results.html',
407471
dataset_name=dataset_name,
@@ -417,7 +481,8 @@ def save_results(filename):
417481
data = load_dataset(filename)
418482

419483
if not data:
420-
return redirect(url_for('input_results', filename=filename, error='データセットが空です。'))
484+
set_flash_message('データセットが空です。', 'error')
485+
return redirect(url_for('input_results', filename=filename))
421486

422487
# 各問題の結果を処理
423488
updated_count = 0
@@ -429,11 +494,11 @@ def save_results(filename):
429494
updated_count += 1
430495

431496
if updated_count > 0:
432-
return redirect(url_for('input_results', filename=filename,
433-
msg=f'{updated_count}問の結果を保存し、習熟度を更新しました。'))
497+
set_flash_message(f'{updated_count}問の結果を保存し、習熟度を更新しました。', 'success')
498+
return redirect(url_for('input_results', filename=filename))
434499
else:
435-
return redirect(url_for('input_results', filename=filename,
436-
error='結果が更新されませんでした。問題を選択してください。'))
500+
set_flash_message('結果が更新されませんでした。問題を選択してください。', 'error')
501+
return redirect(url_for('input_results', filename=filename))
437502

438503

439504

@@ -459,7 +524,8 @@ def create_quiz(filename):
459524
data = load_dataset(filename)
460525

461526
if not data:
462-
return redirect(url_for('generate_quiz', filename=filename, error='データセットが空です。'))
527+
set_flash_message('データセットが空です。', 'error')
528+
return redirect(url_for('generate_quiz', filename=filename))
463529

464530
try:
465531
num_questions = int(request.form.get('num_questions', 50))
@@ -476,13 +542,16 @@ def create_quiz(filename):
476542

477543
# 範囲の妥当性チェック
478544
if start_index < 0 or start_index >= len(data):
479-
return redirect(url_for('generate_quiz', filename=filename, error='開始位置が無効です。'))
545+
set_flash_message('開始位置が無効です。', 'error')
546+
return redirect(url_for('generate_quiz', filename=filename))
480547

481548
if end_index < 0 or end_index >= len(data):
482-
return redirect(url_for('generate_quiz', filename=filename, error='終了位置が無効です。'))
549+
set_flash_message('終了位置が無効です。', 'error')
550+
return redirect(url_for('generate_quiz', filename=filename))
483551

484552
if start_index > end_index:
485-
return redirect(url_for('generate_quiz', filename=filename, error='開始位置は終了位置以下にしてください。'))
553+
set_flash_message('開始位置は終了位置以下にしてください。', 'error')
554+
return redirect(url_for('generate_quiz', filename=filename))
486555

487556
# 指定範囲のデータを取得
488557
range_data = data[start_index:end_index + 1]
@@ -491,7 +560,8 @@ def create_quiz(filename):
491560
num_questions = min(max(1, num_questions), len(range_data))
492561

493562
if num_questions < 1:
494-
return redirect(url_for('generate_quiz', filename=filename, error='問題数は1以上にしてください。'))
563+
set_flash_message('問題数は1以上にしてください。', 'error')
564+
return redirect(url_for('generate_quiz', filename=filename))
495565

496566
# 問題の選択
497567
if selection_method == 'sequential':
@@ -502,9 +572,11 @@ def create_quiz(filename):
502572
selected_items = random.sample(range_data, num_questions)
503573

504574
except ValueError as e:
505-
return redirect(url_for('generate_quiz', filename=filename, error='入力値が正しくありません。'))
575+
set_flash_message('入力値が正しくありません。', 'error')
576+
return redirect(url_for('generate_quiz', filename=filename))
506577
except Exception as e:
507-
return redirect(url_for('generate_quiz', filename=filename, error='予期しないエラーが発生しました。'))
578+
set_flash_message('予期しないエラーが発生しました。', 'error')
579+
return redirect(url_for('generate_quiz', filename=filename))
508580

509581
# PDF生成
510582
pdf_buffer = create_test_pdf(selected_items, filename[:-4], quiz_type)
@@ -735,11 +807,14 @@ def delete_dataset(filename):
735807
try:
736808
if os.path.exists(filepath):
737809
os.remove(filepath)
738-
return redirect(url_for('index', msg='データセットを削除しました。'))
810+
set_flash_message('データセットを削除しました。', 'success')
811+
return redirect(url_for('index'))
739812
else:
740-
return redirect(url_for('index', error='データセットが見つかりません。'))
813+
set_flash_message('データセットが見つかりません。', 'error')
814+
return redirect(url_for('index'))
741815
except Exception as e:
742-
return redirect(url_for('index', error='データセットの削除に失敗しました。'))
816+
set_flash_message('データセットの削除に失敗しました。', 'error')
817+
return redirect(url_for('index'))
743818

744819
@app.route('/export_dataset/<filename>')
745820
def export_dataset(filename):
@@ -828,9 +903,11 @@ def export_dataset(filename):
828903
)
829904
except Exception as e:
830905
print(f"Export error: {e}")
831-
return redirect(url_for('index', error='エクスポートに失敗しました。'))
906+
set_flash_message('エクスポートに失敗しました。', 'error')
907+
return redirect(url_for('index'))
832908
else:
833-
return redirect(url_for('index', error='データセットが見つかりません。'))
909+
set_flash_message('データセットが見つかりません。', 'error')
910+
return redirect(url_for('index'))
834911

835912
@app.route('/import_dataset')
836913
def import_dataset_page():
@@ -846,10 +923,12 @@ def upload_dataset():
846923

847924
file = request.files['file']
848925
if file.filename == '':
849-
return redirect(url_for('import_dataset_page', error='ファイルが選択されていません。'))
926+
set_flash_message('ファイルが選択されていません。', 'error')
927+
return redirect(url_for('import_dataset_page'))
850928

851929
if not file.filename.endswith('.csv'):
852-
return redirect(url_for('import_dataset_page', error='CSVファイルを選択してください。'))
930+
set_flash_message('CSVファイルを選択してください。', 'error')
931+
return redirect(url_for('import_dataset_page'))
853932

854933
try:
855934
# ファイル内容を読み込んで検証(エンコーディング自動判定)
@@ -866,7 +945,8 @@ def upload_dataset():
866945
continue
867946

868947
if content is None:
869-
return redirect(url_for('import_dataset_page', error='ファイルの文字エンコーディングが認識できません。'))
948+
set_flash_message('ファイルの文字エンコーディングが認識できません。', 'error')
949+
return redirect(url_for('import_dataset_page'))
870950

871951
file.seek(0) # ファイルポインタを先頭に戻す
872952

@@ -889,7 +969,8 @@ def upload_dataset():
889969
# ヘッダーを取得
890970
header_fields = reader.fieldnames
891971
if not header_fields:
892-
return redirect(url_for('import_dataset_page', error='ヘッダー行が見つかりません。'))
972+
set_flash_message('ヘッダー行が見つかりません。', 'error')
973+
return redirect(url_for('import_dataset_page'))
893974

894975
# フィールド名を正規化(前後の空白を除去)
895976
header_fields = [field.strip() for field in header_fields]
@@ -901,27 +982,31 @@ def upload_dataset():
901982
has_proficiency = all(field in header_fields for field in ['正解数', '総試行回数', '習熟度スコア'])
902983

903984
if not (has_question and has_answer):
904-
return redirect(url_for('import_dataset_page', error='無効なCSV形式です。"質問"と"回答"(または"question"と"answer")の列が必要です。'))
985+
set_flash_message('無効なCSV形式です。"質問"と"回答"(または"question"と"answer")の列が必要です。', 'error')
986+
return redirect(url_for('import_dataset_page'))
905987

906988
# データ行の存在チェック
907989
data_rows = list(reader)
908990
if len(data_rows) == 0:
909-
return redirect(url_for('import_dataset_page', error='データ行が見つかりません。ヘッダー行のみのファイルです。'))
991+
set_flash_message('データ行が見つかりません。ヘッダー行のみのファイルです。', 'error')
992+
return redirect(url_for('import_dataset_page'))
910993

911994
print(f"CSV validation successful: {len(data_rows)} data rows found")
912995
print(f"Header fields: {header_fields}")
913996

914997
except Exception as csv_error:
915998
print(f"CSV parsing error: {csv_error}")
916-
return redirect(url_for('import_dataset_page', error=f'CSVファイルの解析に失敗しました: {str(csv_error)}'))
999+
set_flash_message(f'CSVファイルの解析に失敗しました: {str(csv_error)}', 'error')
1000+
return redirect(url_for('import_dataset_page'))
9171001

9181002
# ファイル名の重複チェック
9191003
base_name = file.filename[:-4] # .csvを除去
9201004
filename = file.filename
9211005
force_overwrite = request.form.get('force_overwrite')
9221006

9231007
if os.path.exists(os.path.join(DATASETS_DIR, filename)) and not force_overwrite:
924-
return redirect(url_for('import_dataset_page', error=f'データセット "{base_name}" は既に存在します。上書きする場合はチェックボックスを選択してください。'))
1008+
set_flash_message(f'データセット "{base_name}" は既に存在します。上書きする場合はチェックボックスを選択してください。', 'error')
1009+
return redirect(url_for('import_dataset_page'))
9251010

9261011
# ファイルを保存(エンコーディング処理を改善)
9271012
ensure_datasets_dir()
@@ -958,13 +1043,22 @@ def upload_dataset():
9581043
# データ数をカウント
9591044
data = load_dataset(filename)
9601045

961-
return redirect(url_for('edit_dataset', filename=filename, msg=f'データセット "{filename[:-4]}" をインポートしました。({len(data)}件)'))
1046+
set_flash_message(f'データセット "{filename[:-4]}" をインポートしました。({len(data)}件)', 'success')
1047+
return redirect(url_for('edit_dataset', filename=filename))
9621048

9631049
except Exception as e:
9641050
import traceback
9651051
error_details = traceback.format_exc()
9661052
print(f"Import error details: {error_details}")
967-
return redirect(url_for('import_dataset_page', error=f'ファイルのインポートに失敗しました: {str(e)}'))
1053+
set_flash_message(f'ファイルのインポートに失敗しました: {str(e)}', 'error')
1054+
return redirect(url_for('import_dataset_page'))
1055+
1056+
@app.before_request
1057+
def cleanup_on_request():
1058+
"""リクエスト前のクリーンアップ(期限切れメッセージの削除)"""
1059+
import random
1060+
if random.random() < 0.1: # 10%の確率でクリーンアップ実行
1061+
cleanup_expired_messages()
9681062

9691063
if __name__ == '__main__':
9701064
ensure_datasets_dir()

0 commit comments

Comments
 (0)