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 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 question’s correct answer
|
# 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
|
# take only the most‐recent 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():
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
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