add: upload individual questions

This commit is contained in:
lelo 2025-07-06 15:23:13 +02:00
parent 6fc98c48d9
commit 18ba33d8a6
5 changed files with 78 additions and 26 deletions

76
app.py
View File

@ -9,7 +9,7 @@ from flask import Flask, render_template, request, session, redirect, url_for, a
from flask_socketio import SocketIO, emit, join_room from flask_socketio import SocketIO, emit, join_room
from dotenv import load_dotenv from dotenv import load_dotenv
# global store of all games, keyed by secret # global store of all games, keyed by secret. Each entry will hold its own questions list.
games = {} games = {}
# Load environment vars # Load environment vars
@ -19,18 +19,6 @@ app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev')
# manage_session ensures Flask session is available in SocketIO handlers # manage_session ensures Flask session is available in SocketIO handlers
socketio = SocketIO(app, manage_session=True) socketio = SocketIO(app, manage_session=True)
# Expect columns: question, answer1..answer4, correct (column name)
df = pd.read_excel('quizzes/questions.xlsx')
questions = []
for _, row in df.iterrows():
correct_col = row['correct'] # e.g. 'answer3'
correct_text = row[correct_col]
questions.append({
'question': row['question'],
'options': [row[f'answer{i}'] for i in range(1,5)], # only four options
'correct': correct_text
})
# In-memory game state # In-memory game state
game = { game = {
'players': {}, 'players': {},
@ -40,7 +28,7 @@ game = {
'started': False 'started': False
} }
@app.route('/') @app.route('/', methods=['GET'])
def host(): def host():
session['role'] = 'host' session['role'] = 'host'
# — manage per-game secret in session — # — manage per-game secret in session —
@ -48,14 +36,19 @@ def host():
if not secret: if not secret:
secret = uuid.uuid4().hex[:8] secret = uuid.uuid4().hex[:8]
session['secret'] = secret session['secret'] = secret
# ensure game exists # ensure game exists (with questions placeholder)
if secret not in games: if secret not in games:
games[secret] = { games[secret] = {
'questions': None,
'players': {}, 'order': [], 'players': {}, 'order': [],
'current_q': 0, 'answered': [], 'current_q': 0, 'answered': [],
'started': False, 'finished': False, 'started': False, 'finished': False,
'revealed': False 'revealed': False
} }
game = games[secret]
# if no question file has been uploaded yet, show upload form
if not game.get('questions'):
return render_template('upload.html')
# build join URL + QR # build join URL + QR
join_url = url_for('player', secret=secret, _external=True) join_url = url_for('player', secret=secret, _external=True)
@ -71,6 +64,41 @@ def host():
secret=secret secret=secret
) )
@app.route('/upload', methods=['POST'])
def upload():
session['role'] = 'host'
secret = session.get('secret')
if not secret or secret not in games:
return redirect(url_for('host'))
game = games[secret]
# retrieve uploaded file
file = request.files.get('file')
if not file:
return render_template('upload.html', error='Keine Datei ausgewählt')
# try to read Excel
try:
df = pd.read_excel(file)
except Exception:
return render_template('upload.html', error='Ungültige Excel-Datei')
# mandatory columns
required = ['question','answer1','answer2','answer3','answer4','correct']
if not all(col in df.columns for col in required):
return render_template('upload.html', error=f'Fehler in Datei: Fehlende Spalten. Erforderlich: {required}')
# validate that 'correct' references one of the answer columns
if not df['correct'].isin(['answer1','answer2','answer3','answer4']).all():
return render_template('upload.html', error='Fehler in Datei: Antwortspalte "correct" muss einen der Werte "answer1", "answer2", "answer3" oder "answer4" enthalten.')
# build per-game question list
questions = []
for _, row in df.iterrows():
col = row['correct']
questions.append({
'question': row['question'],
'options': [row[f'answer{i}'] for i in range(1,5)],
'correct': row[col]
})
game['questions'] = questions
# once upload and validation succeeds, redirect to host view (which will show the QR)
return redirect(url_for('host'))
@app.route('/new_game', methods=['POST']) @app.route('/new_game', methods=['POST'])
def new_game(): def new_game():
@ -185,8 +213,8 @@ def on_connect():
players = [game['players'][pid]['name'] for pid in game['order']] players = [game['players'][pid]['name'] for pid in game['order']]
emit('update_roster', {'players': players}) emit('update_roster', {'players': players})
if game.get('started') and game['current_q'] < len(questions): if game.get('started') and game['current_q'] < len(game['questions']):
q = questions[game['current_q']] q = game['questions'][game['current_q']]
emit('new_question', {'question': q['question']}, room='host') emit('new_question', {'question': q['question']}, room='host')
return return
@ -223,7 +251,7 @@ def on_start_game():
game = games[secret] game = games[secret]
# if already through, reset # if already through, reset
if game.get('started') or game['current_q'] >= len(questions): if game.get('started') or game['current_q'] >= len(game['questions']):
game.update({ game.update({
'current_q': 0, 'current_q': 0,
'answered': [], 'answered': [],
@ -274,7 +302,7 @@ def on_next_question():
game = games[secret] game = games[secret]
game['current_q'] += 1 game['current_q'] += 1
if game['current_q'] < len(questions): if game['current_q'] < len(game['questions']):
_send_question(secret) _send_question(secret)
else: else:
# mark finished & emit final boards # mark finished & emit final boards
@ -314,7 +342,7 @@ def on_show_answer():
game = games[secret] game = games[secret]
game['revealed'] = True game['revealed'] = True
# find the correct answer text for current question # find the correct answer text for current question
correct = questions[game['current_q']]['correct'] correct = game['questions'][game['current_q']]['correct']
# emit to host so they can display it # emit to host so they can display it
emit('answer_revealed', {'correct': correct}, room='host') emit('answer_revealed', {'correct': correct}, room='host')
# emit to all players so they can highlight it # emit to all players so they can highlight it
@ -343,7 +371,7 @@ def _send_sync_state(pid, game):
# question? # question?
else: else:
idx = game['current_q'] idx = game['current_q']
q = questions[idx] q = game['questions'][idx]
answered = any(pid == p for p,_ in game['answered']) answered = any(pid == p for p,_ in game['answered'])
emit('sync_state', { emit('sync_state', {
'state': 'question', 'state': 'question',
@ -356,7 +384,7 @@ def _send_sync_state(pid, game):
def _send_question(secret): def _send_question(secret):
game = games[secret] game = games[secret]
game['revealed'] = False game['revealed'] = False
q = questions[game['current_q']] q = game['questions'][game['current_q']]
# clear previous answers # clear previous answers
game['answered'].clear() game['answered'].clear()
# broadcast # broadcast
@ -378,7 +406,7 @@ def _score_and_emit(secret):
per_q = [] per_q = []
# grab the current questions correct answer # grab the current questions correct answer
correct = questions[game['current_q']]['correct'] correct = game['questions'][game['current_q']]['correct']
# take only the mostrecent submission # take only the mostrecent submission
pid, ans = game['answered'][-1] pid, ans = game['answered'][-1]
@ -408,7 +436,7 @@ def _score_and_emit(secret):
socketio.emit('overall_leader', {'board': board}, room='player') socketio.emit('overall_leader', {'board': board}, room='player')
# if that was the last question, finish up # if that was the last question, finish up
if game['current_q'] >= len(questions): if game['current_q'] >= len(game['questions']):
game['finished'] = True game['finished'] = True
socketio.emit('game_over', {'board': board}, room='host') socketio.emit('game_over', {'board': board}, room='host')
for psid, pdata in game['players'].items(): for psid, pdata in game['players'].items():

View File

@ -1,7 +1,7 @@
<!-- host.html --> <!-- host.html -->
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<h1 class="mb-4">Bibelquiz</h1> <h1 class="mb-4">Public Quiz</h1>
<div class="container" id="host-app"> <div class="container" id="host-app">
<!-- QR & Join Roster --> <!-- QR & Join Roster -->
<div id="qrContainer" class="mb-4 text-center"> <div id="qrContainer" class="mb-4 text-center">

View File

@ -22,7 +22,7 @@
<form id="joinForm" action="{{ url_for('do_join') }}" method="post"> <form id="joinForm" action="{{ url_for('do_join') }}" method="post">
<div class="input-group mb-3"> <div class="input-group mb-3">
<input name="name" id="nameInput" type="text" <input name="name" id="nameInput" type="text"
class="form-control" placeholder="Name eingeben" /> class="form-control" placeholder="Deinen Name eingeben..." />
<button type="submit" class="btn btn-success">Beitreten</button> <button type="submit" class="btn btn-success">Beitreten</button>
</div> </div>
</form> </form>

24
templates/upload.html Normal file
View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
<h1 class="mb-4">Fragebogen hochladen</h1>
<!-- Download-Link zur Beispiel-Datei -->
<div class="mb-3">
<a href="{{ url_for('static', filename='questions_example.xlsx') }}" class="btn btn-outline-secondary">
Beispiel-Excel-Datei herunterladen
</a>
</div>
<form action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="file" class="form-label">Excel-Datei auswählen</label>
<input class="form-control" type="file" id="file" name="file" accept=".xlsx,.xls">
</div>
<button type="submit" class="btn btn-primary">Hochladen</button>
</form>
{% if error %}
<div class="alert alert-danger mt-3">{{ error }}</div>
{% endif %}
{% endblock %}