Skip to content

Commit 3b66530

Browse files
committed
feat: Add answer output options for quiz generation in the UI and PDF
1 parent 0343f31 commit 3b66530

File tree

3 files changed

+217
-18
lines changed

3 files changed

+217
-18
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
- **シンプル**: 統一フォーマット「番号,質問,回答」で管理が簡単(習熟度データは自動管理)
1616
- **オンラインテスト機能**: ブラウザ上でのリアルタイムテスト実行と自己判定
1717
- **印刷対応**: A4サイズの表形式PDFで出力、そのまま印刷して使用可能
18+
- **柔軟な回答出力**: 3つのモード(回答なし/下部表示/赤字表示)から選択可能
19+
- **赤シート対応**: 薄い赤字での回答表示で暗記学習を効率化
1820
- **範囲指定出題**: データセットの特定範囲から問題を生成
1921
- **柔軟な選択方法**: ランダム選択と順番選択の両方に対応
22+
- **コンパクトレイアウト**: 最適化されたPDF生成で用紙を効率的に活用
2023
- **日本語完全対応**: 漢字・ひらがな・カタカナが正しく表示
2124
- **レスポンシブ**: スマートフォンやタブレットでも使用可能
2225

@@ -60,6 +63,10 @@
6063
- デフォルト50問(1~データセット全体まで設定可能)
6164
- **出題対象範囲の指定**: 開始位置と終了位置を指定して特定の範囲から出題
6265
- **選択方法の選択**: ランダム選択または順番選択を選択可能
66+
- **柔軟な回答出力設定**: 3つの出力モードから選択可能
67+
- 回答なし: 通常のテスト用(問題のみ)
68+
- 回答を下部に表示: ページ下部に回答一覧を薄い赤字で表示
69+
- 回答を表示: 回答欄に薄い赤字で表示
6370
- A4サイズでPDF出力(印刷対応、表形式)
6471
- 問題タイプの選択可能
6572
- 質問→回答(デフォルト)
@@ -161,10 +168,14 @@ http://localhost:5000
161168
4. **問題選択方法を選択**
162169
- ランダム選択:指定範囲からランダムに問題を選択
163170
- 順番選択:指定範囲から順番に問題を選択
164-
5. 問題タイプを選択
171+
5. **回答出力設定を選択**
172+
- 回答なし:通常のテスト用(問題のみ)
173+
- 回答を下部に表示:ページ下部に回答一覧を薄い赤字で表示
174+
- 回答を表示:回答欄に薄い赤字で表示(**赤シート対応**で学習効果UP)
175+
6. 問題タイプを選択
165176
- 質問→回答(デフォルト)
166177
- 回答→質問
167-
6. 「PDFを生成・ダウンロード」をクリック
178+
7. 「PDFを生成・ダウンロード」をクリック
168179

169180
### 5. オンラインテストの実行
170181
1. データセットの「オンラインテスト」をクリック
@@ -294,8 +305,11 @@ number,question,answer
294305
- **オンラインテスト機能**: ブラウザ上でのリアルタイムテスト実行と自己判定システム
295306
- **手軽なデータ管理**: CSV形式なのでExcelやテキストエディタで編集可能
296307
- **印刷対応**: A4サイズの表形式PDFで出力、そのまま印刷して使用可能
308+
- **柔軟な回答出力**: 3つのモード(回答なし/下部表示/赤字表示)から選択可能
309+
- **赤シート学習対応**: 薄い赤字での回答表示で効率的な暗記学習
297310
- **範囲指定出題**: データセットの特定範囲から問題を生成可能
298311
- **柔軟な選択方法**: ランダム選択と順番選択の両方に対応
312+
- **最適化されたレイアウト**: コンパクトなPDF生成で用紙を効率的に活用
299313
- **大量問題対応**: 50問を超える場合は自動で複数ページに分割
300314
- **柔軟な問題設定**: 問題数や出題方向を自由に設定
301315
- **日本語完全対応**: 漢字・ひらがな・カタカナが正しく表示

app.py

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ def create_quiz(filename):
925925
num_questions = int(request.form.get('num_questions', 50))
926926
quiz_type = request.form.get('quiz_type', 'question_to_answer')
927927
selection_method = request.form.get('selection_method', 'random')
928+
include_answers = request.form.get('include_answers', 'no')
928929

929930
# 範囲設定の取得
930931
range_start = request.form.get('range_start')
@@ -973,7 +974,7 @@ def create_quiz(filename):
973974
return redirect(url_for('generate_quiz', filename=filename))
974975

975976
# PDF生成
976-
pdf_buffer = create_test_pdf(selected_items, filename[:-4], quiz_type)
977+
pdf_buffer = create_test_pdf(selected_items, filename[:-4], quiz_type, include_answers)
977978

978979
# PDFをファイルとして返す
979980
return send_file(
@@ -983,14 +984,15 @@ def create_quiz(filename):
983984
mimetype='application/pdf'
984985
)
985986

986-
def create_test_pdf(items, dataset_name, quiz_type):
987+
def create_test_pdf(items, dataset_name, quiz_type, include_answers='no'):
987988
"""問題のPDFを作成(統一フォーマット:質問,回答)"""
988989
buffer = io.BytesIO()
989990

990991
# フォント設定
991992
font_available = setup_fonts()
992993

993-
doc = SimpleDocTemplate(buffer, pagesize=A4, topMargin=20*mm, bottomMargin=20*mm)
994+
doc = SimpleDocTemplate(buffer, pagesize=A4, topMargin=10*mm, bottomMargin=10*mm,
995+
leftMargin=10*mm, rightMargin=10*mm)
994996
story = []
995997

996998
# スタイル設定
@@ -1014,7 +1016,7 @@ def escape_japanese(text):
10141016
parent=styles['Title'],
10151017
fontName='Japanese',
10161018
fontSize=16,
1017-
spaceAfter=20
1019+
spaceAfter=5
10181020
)
10191021

10201022
question_style = ParagraphStyle(
@@ -1029,7 +1031,7 @@ def escape_japanese(text):
10291031
'CustomTitle',
10301032
parent=styles['Title'],
10311033
fontSize=16,
1032-
spaceAfter=20
1034+
spaceAfter=5
10331035
)
10341036

10351037
question_style = ParagraphStyle(
@@ -1048,7 +1050,7 @@ def escape_japanese(text):
10481050

10491051
title_paragraph = Paragraph(title_text, title_style)
10501052
story.append(title_paragraph)
1051-
story.append(Spacer(1, 10*mm))
1053+
story.append(Spacer(1, 3*mm))
10521054

10531055
# 全ての問題を処理(50問を超えた場合は複数ページ)
10541056
total_items = len(items)
@@ -1076,7 +1078,7 @@ def escape_japanese(text):
10761078
page_title = escape_japanese(f"{dataset_name} - 問題 (ページ {page_num})")
10771079

10781080
story.append(Paragraph(page_title, title_style))
1079-
story.append(Spacer(1, 10*mm))
1081+
story.append(Spacer(1, 3*mm))
10801082

10811083
# 表データを準備(2列構成:左側25問、右側25問)
10821084
table_data = []
@@ -1096,17 +1098,31 @@ def escape_japanese(text):
10961098
# 回答→質問
10971099
question_text = (left_item.get('回答') or
10981100
left_item.get('answer') or '')
1101+
answer_text = (left_item.get('質問') or
1102+
left_item.get('question') or '')
10991103
else:
11001104
# 質問→回答(デフォルト)
11011105
question_text = (left_item.get('質問') or
11021106
left_item.get('question') or '')
1107+
answer_text = (left_item.get('回答') or
1108+
left_item.get('answer') or '')
11031109

11041110
if question_text:
11051111
if font_available:
11061112
left_question = f"{left_item.get('番号', current_item_index + i + 1)}. {question_text}"
11071113
else:
11081114
left_question = escape_japanese(f"{left_item.get('番号', current_item_index + i + 1)}. {question_text}")
1109-
left_answer = "________________"
1115+
1116+
# 回答欄の処理
1117+
if include_answers == 'red':
1118+
# 薄い赤字で回答を表示(赤シートで隠しやすいように)
1119+
if font_available:
1120+
left_answer = f"<font color='#FF6666'>{answer_text}</font>"
1121+
else:
1122+
left_answer = f"<font color='#FF6666'>{escape_japanese(answer_text)}</font>"
1123+
left_answer = Paragraph(left_answer, question_style)
1124+
else:
1125+
left_answer = "________________"
11101126
else:
11111127
left_question = ""
11121128
left_answer = ""
@@ -1121,17 +1137,31 @@ def escape_japanese(text):
11211137
# 回答→質問
11221138
question_text = (right_item.get('回答') or
11231139
right_item.get('answer') or '')
1140+
answer_text = (right_item.get('質問') or
1141+
right_item.get('question') or '')
11241142
else:
11251143
# 質問→回答(デフォルト)
11261144
question_text = (right_item.get('質問') or
11271145
right_item.get('question') or '')
1146+
answer_text = (right_item.get('回答') or
1147+
right_item.get('answer') or '')
11281148

11291149
if question_text:
11301150
if font_available:
11311151
right_question = f"{right_item.get('番号', current_item_index + i + 26)}. {question_text}"
11321152
else:
11331153
right_question = escape_japanese(f"{right_item.get('番号', current_item_index + i + 26)}. {question_text}")
1134-
right_answer = "________________"
1154+
1155+
# 回答欄の処理
1156+
if include_answers == 'red':
1157+
# 薄い赤字で回答を表示(赤シートで隠しやすいように)
1158+
if font_available:
1159+
right_answer = f"<font color='#FF6666'>{answer_text}</font>"
1160+
else:
1161+
right_answer = f"<font color='#FF6666'>{escape_japanese(answer_text)}</font>"
1162+
right_answer = Paragraph(right_answer, question_style)
1163+
else:
1164+
right_answer = "________________"
11351165
else:
11361166
right_question = ""
11371167
right_answer = ""
@@ -1149,7 +1179,7 @@ def escape_japanese(text):
11491179
])
11501180

11511181
# 表を作成(4列:問題、解答欄、問題、解答欄)
1152-
table = Table(table_data, colWidths=[55*mm, 35*mm, 55*mm, 35*mm], rowHeights=[8*mm] * len(table_data))
1182+
table = Table(table_data, colWidths=[55*mm, 35*mm, 55*mm, 35*mm], rowHeights=[6*mm] * len(table_data))
11531183

11541184
# 表のスタイル設定
11551185
table.setStyle(TableStyle([
@@ -1159,21 +1189,102 @@ def escape_japanese(text):
11591189
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
11601190
('FONTNAME', (0, 0), (-1, 0), 'Japanese' if font_available else 'Helvetica-Bold'),
11611191
('FONTSIZE', (0, 0), (-1, 0), 10),
1162-
('BOTTOMPADDING', (0, 0), (-1, 0), 8),
1192+
('BOTTOMPADDING', (0, 0), (-1, 0), 4),
11631193

11641194
# データ行のスタイル
11651195
('FONTNAME', (0, 1), (-1, -1), 'Japanese' if font_available else 'Helvetica'),
11661196
('FONTSIZE', (0, 1), (-1, -1), 9),
11671197
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
11681198
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
1169-
('LEFTPADDING', (0, 0), (-1, -1), 3),
1170-
('RIGHTPADDING', (0, 0), (-1, -1), 3),
1171-
('TOPPADDING', (0, 0), (-1, -1), 6),
1172-
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
1199+
('LEFTPADDING', (0, 0), (-1, -1), 2),
1200+
('RIGHTPADDING', (0, 0), (-1, -1), 2),
1201+
('TOPPADDING', (0, 0), (-1, -1), 3),
1202+
('BOTTOMPADDING', (0, 0), (-1, -1), 3),
11731203
]))
11741204

11751205
story.append(table)
11761206

1207+
# 回答を下部に含める場合は、回答セクションを追加
1208+
if include_answers == 'bottom':
1209+
# 回答セクション用のスペース
1210+
story.append(Spacer(1, 5*mm))
1211+
1212+
# 回答セクションのタイトル
1213+
if font_available:
1214+
answer_title_text = "回答"
1215+
else:
1216+
answer_title_text = escape_japanese("回答")
1217+
1218+
answer_title_style = ParagraphStyle(
1219+
'AnswerTitle',
1220+
parent=styles['Heading2'],
1221+
fontName='Japanese' if font_available else 'Helvetica-Bold',
1222+
fontSize=12,
1223+
spaceAfter=3
1224+
)
1225+
1226+
story.append(Paragraph(answer_title_text, answer_title_style))
1227+
1228+
# 回答データを作成(現在のページの問題に対応)
1229+
answer_data = []
1230+
answers_per_row = 5 # 1行に5個の回答を配置
1231+
1232+
for i, item in enumerate(page_items):
1233+
# 回答テキストを取得
1234+
if quiz_type == 'answer_to_question':
1235+
# 回答→質問の場合、質問が答え
1236+
answer_text = (item.get('質問') or item.get('question') or '')
1237+
else:
1238+
# 質問→回答の場合、回答が答え
1239+
answer_text = (item.get('回答') or item.get('answer') or '')
1240+
1241+
question_number = item.get('番号', current_item_index + i + 1)
1242+
1243+
if font_available:
1244+
answer_entry = f"{question_number}. {answer_text}"
1245+
else:
1246+
answer_entry = escape_japanese(f"{question_number}. {answer_text}")
1247+
1248+
answer_data.append(answer_entry)
1249+
1250+
# 回答を表形式で配置(1行に複数個)
1251+
answer_table_data = []
1252+
answer_style = ParagraphStyle(
1253+
'AnswerStyle',
1254+
parent=styles['Normal'],
1255+
fontName='Japanese' if font_available else 'Helvetica',
1256+
fontSize=8,
1257+
spaceAfter=2
1258+
)
1259+
1260+
for i in range(0, len(answer_data), answers_per_row):
1261+
row_data = []
1262+
for j in range(answers_per_row):
1263+
if i + j < len(answer_data):
1264+
# 薄い赤字で回答を表示
1265+
if font_available:
1266+
red_answer = f"<font color='#FF6666'>{answer_data[i + j]}</font>"
1267+
else:
1268+
red_answer = f"<font color='#FF6666'>{answer_data[i + j]}</font>"
1269+
row_data.append(Paragraph(red_answer, answer_style))
1270+
else:
1271+
row_data.append("")
1272+
answer_table_data.append(row_data)
1273+
1274+
if answer_table_data:
1275+
answer_table = Table(answer_table_data, colWidths=[38*mm] * answers_per_row)
1276+
answer_table.setStyle(TableStyle([
1277+
('FONTNAME', (0, 0), (-1, -1), 'Japanese' if font_available else 'Helvetica'),
1278+
('FONTSIZE', (0, 0), (-1, -1), 8),
1279+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
1280+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
1281+
('LEFTPADDING', (0, 0), (-1, -1), 2),
1282+
('RIGHTPADDING', (0, 0), (-1, -1), 2),
1283+
('TOPPADDING', (0, 0), (-1, -1), 2),
1284+
('BOTTOMPADDING', (0, 0), (-1, -1), 2),
1285+
]))
1286+
story.append(answer_table)
1287+
11771288
# 次のページの準備
11781289
current_item_index += current_page_items
11791290

@@ -1188,7 +1299,24 @@ def escape_japanese(text):
11881299
question_text = (item.get('質問') or
11891300
item.get('question') or '問題')
11901301
story.append(Paragraph(f"{i}. {question_text} Answer: ___________", question_style))
1191-
story.append(Spacer(1, 3*mm))
1302+
story.append(Spacer(1, 1*mm))
1303+
1304+
# フォールバック時も回答を含める
1305+
if include_answers == 'bottom':
1306+
story.append(Spacer(1, 5*mm))
1307+
story.append(Paragraph("回答", title_style))
1308+
for i, item in enumerate(items, 1):
1309+
if quiz_type == 'answer_to_question':
1310+
answer_text = (item.get('質問') or item.get('question') or '')
1311+
else:
1312+
answer_text = (item.get('回答') or item.get('answer') or '')
1313+
# 薄い赤字で回答を表示
1314+
red_answer = f"<font color='#FF6666'>{i}. {answer_text}</font>"
1315+
story.append(Paragraph(red_answer, question_style))
1316+
elif include_answers == 'red':
1317+
# 赤字で回答を表示する場合は既に問題文に含まれているのでここでは何もしない
1318+
pass
1319+
11921320
doc.build(story)
11931321

11941322
buffer.seek(0)

0 commit comments

Comments
 (0)