add: upload individual questions
This commit is contained in:
parent
6fc98c48d9
commit
18ba33d8a6
76
app.py
76
app.py
@ -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 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 = {}
|
||||
|
||||
# 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
|
||||
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
|
||||
game = {
|
||||
'players': {},
|
||||
@ -40,7 +28,7 @@ game = {
|
||||
'started': False
|
||||
}
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/', methods=['GET'])
|
||||
def host():
|
||||
session['role'] = 'host'
|
||||
# — manage per-game secret in session —
|
||||
@ -48,14 +36,19 @@ def host():
|
||||
if not secret:
|
||||
secret = uuid.uuid4().hex[:8]
|
||||
session['secret'] = secret
|
||||
# ensure game exists
|
||||
# ensure game exists (with questions placeholder)
|
||||
if secret not in games:
|
||||
games[secret] = {
|
||||
'questions': None,
|
||||
'players': {}, 'order': [],
|
||||
'current_q': 0, 'answered': [],
|
||||
'started': False, 'finished': 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
|
||||
join_url = url_for('player', secret=secret, _external=True)
|
||||
@ -71,6 +64,41 @@ def host():
|
||||
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'])
|
||||
def new_game():
|
||||
@ -185,8 +213,8 @@ def on_connect():
|
||||
players = [game['players'][pid]['name'] for pid in game['order']]
|
||||
emit('update_roster', {'players': players})
|
||||
|
||||
if game.get('started') and game['current_q'] < len(questions):
|
||||
q = questions[game['current_q']]
|
||||
if game.get('started') and game['current_q'] < len(game['questions']):
|
||||
q = game['questions'][game['current_q']]
|
||||
emit('new_question', {'question': q['question']}, room='host')
|
||||
return
|
||||
|
||||
@ -223,7 +251,7 @@ def on_start_game():
|
||||
game = games[secret]
|
||||
|
||||
# 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({
|
||||
'current_q': 0,
|
||||
'answered': [],
|
||||
@ -274,7 +302,7 @@ def on_next_question():
|
||||
game = games[secret]
|
||||
game['current_q'] += 1
|
||||
|
||||
if game['current_q'] < len(questions):
|
||||
if game['current_q'] < len(game['questions']):
|
||||
_send_question(secret)
|
||||
else:
|
||||
# mark finished & emit final boards
|
||||
@ -314,7 +342,7 @@ def on_show_answer():
|
||||
game = games[secret]
|
||||
game['revealed'] = True
|
||||
# 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('answer_revealed', {'correct': correct}, room='host')
|
||||
# emit to all players so they can highlight it
|
||||
@ -343,7 +371,7 @@ def _send_sync_state(pid, game):
|
||||
# question?
|
||||
else:
|
||||
idx = game['current_q']
|
||||
q = questions[idx]
|
||||
q = game['questions'][idx]
|
||||
answered = any(pid == p for p,_ in game['answered'])
|
||||
emit('sync_state', {
|
||||
'state': 'question',
|
||||
@ -356,7 +384,7 @@ def _send_sync_state(pid, game):
|
||||
def _send_question(secret):
|
||||
game = games[secret]
|
||||
game['revealed'] = False
|
||||
q = questions[game['current_q']]
|
||||
q = game['questions'][game['current_q']]
|
||||
# clear previous answers
|
||||
game['answered'].clear()
|
||||
# broadcast
|
||||
@ -378,7 +406,7 @@ def _score_and_emit(secret):
|
||||
per_q = []
|
||||
|
||||
# grab the current question’s correct answer
|
||||
correct = questions[game['current_q']]['correct']
|
||||
correct = game['questions'][game['current_q']]['correct']
|
||||
# take only the most‐recent submission
|
||||
pid, ans = game['answered'][-1]
|
||||
|
||||
@ -408,7 +436,7 @@ def _score_and_emit(secret):
|
||||
socketio.emit('overall_leader', {'board': board}, room='player')
|
||||
|
||||
# if that was the last question, finish up
|
||||
if game['current_q'] >= len(questions):
|
||||
if game['current_q'] >= len(game['questions']):
|
||||
game['finished'] = True
|
||||
socketio.emit('game_over', {'board': board}, room='host')
|
||||
for psid, pdata in game['players'].items():
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<!-- host.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h1 class="mb-4">Bibelquiz</h1>
|
||||
<h1 class="mb-4">Public Quiz</h1>
|
||||
<div class="container" id="host-app">
|
||||
<!-- QR & Join Roster -->
|
||||
<div id="qrContainer" class="mb-4 text-center">
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<form id="joinForm" action="{{ url_for('do_join') }}" method="post">
|
||||
<div class="input-group mb-3">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
24
templates/upload.html
Normal file
24
templates/upload.html
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user