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
22import csv
33import os
44import random
5- from datetime import datetime
5+ from datetime import datetime , timedelta
66import io
77import base64
8+ import time
89from reportlab .lib .pagesizes import A4
910from reportlab .platypus import SimpleDocTemplate , Paragraph , Spacer , Table , TableStyle
1011from reportlab .lib .styles import getSampleStyleSheet , ParagraphStyle
1516from reportlab .lib import colors
1617
1718app = 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# データセット保存ディレクトリ
2123DATASETS_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+
2384def 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# 日本語フォントの設定
3689def 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>' )
326383def 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>' )
375435def 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>' )
397460def 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>' )
745820def 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' )
836913def 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
9691063if __name__ == '__main__' :
9701064 ensure_datasets_dir ()
0 commit comments