Feature: Support both Free-Text and Multiple-Choice question types
This commit is contained in:
+68
-18
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
import datetime
|
import datetime
|
||||||
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form
|
from fastapi import FastAPI, Request, Depends, HTTPException, status, Form
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||||
@@ -28,6 +29,27 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
||||||
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
||||||
|
|
||||||
|
def format_question_for_client(q: Question, number: int, total: int) -> dict:
|
||||||
|
options_list = None
|
||||||
|
if q.question_type == "multiple_choice" and q.options:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(q.options)
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
options_list = list(parsed)
|
||||||
|
random.shuffle(options_list)
|
||||||
|
except Exception:
|
||||||
|
options_list = [opt.strip() for opt in q.options.split(",") if opt.strip()]
|
||||||
|
random.shuffle(options_list)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"question_number": number,
|
||||||
|
"total_questions": total,
|
||||||
|
"category": q.category,
|
||||||
|
"question": q.question,
|
||||||
|
"question_type": q.question_type or "text",
|
||||||
|
"options": options_list
|
||||||
|
}
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def startup_event():
|
def startup_event():
|
||||||
seed_db()
|
seed_db()
|
||||||
@@ -65,16 +87,14 @@ async def start_game(db: Session = Depends(get_db)):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
first_q = questions[0]
|
first_q = questions[0]
|
||||||
return {
|
res = format_question_for_client(first_q, 1, len(question_ids))
|
||||||
|
res.update({
|
||||||
"session_id": session_id,
|
"session_id": session_id,
|
||||||
"question_number": 1,
|
|
||||||
"total_questions": len(question_ids),
|
|
||||||
"category": first_q.category,
|
|
||||||
"question": first_q.question,
|
|
||||||
"score": 0,
|
"score": 0,
|
||||||
"attempts_used": 0,
|
"attempts_used": 0,
|
||||||
"max_attempts": 3
|
"max_attempts": 3
|
||||||
}
|
})
|
||||||
|
return res
|
||||||
|
|
||||||
@app.post("/api/game/check")
|
@app.post("/api/game/check")
|
||||||
async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)):
|
async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db)):
|
||||||
@@ -118,12 +138,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
if not is_finished:
|
if not is_finished:
|
||||||
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
||||||
if next_q:
|
if next_q:
|
||||||
next_q_data = {
|
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
|
||||||
"question_number": session.current_question_idx + 1,
|
|
||||||
"total_questions": len(question_ids),
|
|
||||||
"category": next_q.category,
|
|
||||||
"question": next_q.question
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"correct": True,
|
"correct": True,
|
||||||
@@ -155,12 +170,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
if not is_finished:
|
if not is_finished:
|
||||||
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
||||||
if next_q:
|
if next_q:
|
||||||
next_q_data = {
|
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
|
||||||
"question_number": session.current_question_idx + 1,
|
|
||||||
"total_questions": len(question_ids),
|
|
||||||
"category": next_q.category,
|
|
||||||
"question": next_q.question
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
@@ -218,11 +228,23 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
|||||||
if not payload.frage.strip() or not payload.antwort.strip():
|
if not payload.frage.strip() or not payload.antwort.strip():
|
||||||
raise HTTPException(status_code=400, detail="Frage und Antwort dürfen nicht leer sein.")
|
raise HTTPException(status_code=400, detail="Frage und Antwort dürfen nicht leer sein.")
|
||||||
|
|
||||||
|
options_json = None
|
||||||
|
q_type = payload.question_type or "text"
|
||||||
|
if q_type == "multiple_choice":
|
||||||
|
opts = [payload.antwort.strip()]
|
||||||
|
if payload.falsche_antworten:
|
||||||
|
for opt in payload.falsche_antworten:
|
||||||
|
if opt and opt.strip():
|
||||||
|
opts.append(opt.strip())
|
||||||
|
options_json = json.dumps(opts, ensure_ascii=False)
|
||||||
|
|
||||||
new_suggestion = Question(
|
new_suggestion = Question(
|
||||||
category="community",
|
category="community",
|
||||||
question=payload.frage.strip(),
|
question=payload.frage.strip(),
|
||||||
canonical_answer=payload.antwort.strip(),
|
canonical_answer=payload.antwort.strip(),
|
||||||
hint=payload.tipp.strip() if payload.tipp else None,
|
hint=payload.tipp.strip() if payload.tipp else None,
|
||||||
|
question_type=q_type,
|
||||||
|
options=options_json,
|
||||||
status="needs_review", # User Question Need Test / Review
|
status="needs_review", # User Question Need Test / Review
|
||||||
submitted_by=payload.name.strip() if payload.name else "Gast",
|
submitted_by=payload.name.strip() if payload.name else "Gast",
|
||||||
sort_order=999
|
sort_order=999
|
||||||
@@ -304,17 +326,31 @@ async def admin_add_question(
|
|||||||
canonical_answer: str = Form(...),
|
canonical_answer: str = Form(...),
|
||||||
hint: str = Form(None),
|
hint: str = Form(None),
|
||||||
category: str = Form("harry_potter"),
|
category: str = Form("harry_potter"),
|
||||||
|
question_type: str = Form("text"),
|
||||||
|
wrong_option_1: str = Form(None),
|
||||||
|
wrong_option_2: str = Form(None),
|
||||||
|
wrong_option_3: str = Form(None),
|
||||||
sort_order: int = Form(0),
|
sort_order: int = Form(0),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
if not request.session.get("admin_user"):
|
if not request.session.get("admin_user"):
|
||||||
return RedirectResponse(url="/admin/login", status_code=303)
|
return RedirectResponse(url="/admin/login", status_code=303)
|
||||||
|
|
||||||
|
options_json = None
|
||||||
|
if question_type == "multiple_choice":
|
||||||
|
opts = [canonical_answer.strip()]
|
||||||
|
for w in [wrong_option_1, wrong_option_2, wrong_option_3]:
|
||||||
|
if w and w.strip():
|
||||||
|
opts.append(w.strip())
|
||||||
|
options_json = json.dumps(opts, ensure_ascii=False)
|
||||||
|
|
||||||
new_q = Question(
|
new_q = Question(
|
||||||
category=category.strip(),
|
category=category.strip(),
|
||||||
question=question.strip(),
|
question=question.strip(),
|
||||||
canonical_answer=canonical_answer.strip(),
|
canonical_answer=canonical_answer.strip(),
|
||||||
hint=hint.strip() if hint else None,
|
hint=hint.strip() if hint else None,
|
||||||
|
question_type=question_type.strip(),
|
||||||
|
options=options_json,
|
||||||
status="active",
|
status="active",
|
||||||
sort_order=sort_order,
|
sort_order=sort_order,
|
||||||
submitted_by=f"Admin ({request.session['admin_user']})"
|
submitted_by=f"Admin ({request.session['admin_user']})"
|
||||||
@@ -365,6 +401,8 @@ async def admin_edit_question(
|
|||||||
question: str = Form(...),
|
question: str = Form(...),
|
||||||
canonical_answer: str = Form(...),
|
canonical_answer: str = Form(...),
|
||||||
hint: str = Form(None),
|
hint: str = Form(None),
|
||||||
|
question_type: str = Form("text"),
|
||||||
|
options_raw: str = Form(None),
|
||||||
status_val: str = Form("active"),
|
status_val: str = Form("active"),
|
||||||
sort_order: int = Form(0),
|
sort_order: int = Form(0),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
@@ -379,6 +417,18 @@ async def admin_edit_question(
|
|||||||
q.question = question.strip()
|
q.question = question.strip()
|
||||||
q.canonical_answer = canonical_answer.strip()
|
q.canonical_answer = canonical_answer.strip()
|
||||||
q.hint = hint.strip() if hint else None
|
q.hint = hint.strip() if hint else None
|
||||||
|
q.question_type = question_type.strip()
|
||||||
|
|
||||||
|
if options_raw and options_raw.strip():
|
||||||
|
val = options_raw.strip()
|
||||||
|
if val.startswith("[") and val.endswith("]"):
|
||||||
|
q.options = val
|
||||||
|
else:
|
||||||
|
items = [x.strip() for x in val.split(",") if x.strip()]
|
||||||
|
q.options = json.dumps(items, ensure_ascii=False)
|
||||||
|
else:
|
||||||
|
q.options = None
|
||||||
|
|
||||||
q.status = status_val.strip()
|
q.status = status_val.strip()
|
||||||
q.sort_order = sort_order
|
q.sort_order = sort_order
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class Question(Base):
|
|||||||
question = Column(Text, nullable=False)
|
question = Column(Text, nullable=False)
|
||||||
canonical_answer = Column(String(255), nullable=False)
|
canonical_answer = Column(String(255), nullable=False)
|
||||||
accepted_answers = Column(Text, nullable=True) # JSON or comma-delimited
|
accepted_answers = Column(Text, nullable=True) # JSON or comma-delimited
|
||||||
|
question_type = Column(String(20), default="text") # 'text' or 'multiple_choice'
|
||||||
|
options = Column(Text, nullable=True) # JSON list of options for multiple choice
|
||||||
hint = Column(Text, nullable=True)
|
hint = Column(Text, nullable=True)
|
||||||
status = Column(String(30), default="active", index=True) # active, needs_review, rejected, archived
|
status = Column(String(30), default="active", index=True) # active, needs_review, rejected, archived
|
||||||
submitted_by = Column(String(100), nullable=True)
|
submitted_by = Column(String(100), nullable=True)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class SuggestionCreateRequest(BaseModel):
|
|||||||
antwort: str
|
antwort: str
|
||||||
tipp: Optional[str] = None
|
tipp: Optional[str] = None
|
||||||
name: Optional[str] = "Gast"
|
name: Optional[str] = "Gast"
|
||||||
|
question_type: Optional[str] = "text"
|
||||||
|
falsche_antworten: Optional[List[str]] = None
|
||||||
|
|
||||||
class QuestionAdminCreate(BaseModel):
|
class QuestionAdminCreate(BaseModel):
|
||||||
category: str = "harry_potter"
|
category: str = "harry_potter"
|
||||||
|
|||||||
@@ -428,6 +428,88 @@ body {
|
|||||||
box-shadow: 0 0 12px rgba(201, 168, 106, 0.4);
|
box-shadow: 0 0 12px rgba(201, 168, 106, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Multiple Choice Styles */
|
||||||
|
.choice-group {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 2px solid var(--border-gold);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(201, 168, 106, 0.18);
|
||||||
|
border-color: var(--gold);
|
||||||
|
box-shadow: 0 0 15px rgba(201, 168, 106, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-letter {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(201, 168, 106, 0.2);
|
||||||
|
border: 1px solid var(--gold);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--gold-bright);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-selected {
|
||||||
|
border-color: var(--gold-bright);
|
||||||
|
background: rgba(201, 168, 106, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-correct {
|
||||||
|
background: rgba(46, 204, 113, 0.25) !important;
|
||||||
|
border-color: #2ecc71 !important;
|
||||||
|
color: #55efc4 !important;
|
||||||
|
box-shadow: 0 0 15px rgba(46, 204, 113, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-wrong {
|
||||||
|
background: rgba(231, 76, 60, 0.25) !important;
|
||||||
|
border-color: #e74c3c !important;
|
||||||
|
color: #ff7675 !important;
|
||||||
|
box-shadow: 0 0 15px rgba(231, 76, 60, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.hint-box {
|
.hint-box {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|||||||
+14
-2
@@ -7,8 +7,9 @@ function showTab(tabId) {
|
|||||||
targetTab.classList.add("active");
|
targetTab.classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark active button
|
if (event && event.currentTarget) {
|
||||||
event.currentTarget.classList.add("active");
|
event.currentTarget.classList.add("active");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEdit(id) {
|
function toggleEdit(id) {
|
||||||
@@ -18,6 +19,17 @@ function toggleEdit(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAdminNewType(type) {
|
||||||
|
const mcGroup = document.getElementById("adminMcOptions");
|
||||||
|
if (mcGroup) {
|
||||||
|
if (type === "multiple_choice") {
|
||||||
|
mcGroup.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
mcGroup.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function approveQuestion(id) {
|
async function approveQuestion(id) {
|
||||||
if (!confirm(`Möchtest du Frage #${id} wirklich freischalten und für alle Spieler aktivieren?`)) {
|
if (!confirm(`Möchtest du Frage #${id} wirklich freischalten und für alle Spieler aktivieren?`)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+64
-14
@@ -103,16 +103,42 @@ function renderQuestion(qData) {
|
|||||||
document.getElementById("attemptBadge").innerText = `Versuch ${attemptsUsed + 1} von ${maxAttempts}`;
|
document.getElementById("attemptBadge").innerText = `Versuch ${attemptsUsed + 1} von ${maxAttempts}`;
|
||||||
document.getElementById("frageText").innerText = qData.question;
|
document.getElementById("frageText").innerText = qData.question;
|
||||||
|
|
||||||
|
const textGroup = document.getElementById("textAnswerGroup");
|
||||||
|
const choiceGroup = document.getElementById("choiceAnswerGroup");
|
||||||
|
const choiceContainer = document.getElementById("choiceOptionsContainer");
|
||||||
const input = document.getElementById("answerInput");
|
const input = document.getElementById("answerInput");
|
||||||
input.value = "";
|
|
||||||
input.disabled = false;
|
|
||||||
input.focus();
|
|
||||||
|
|
||||||
document.getElementById("checkBtn").disabled = false;
|
|
||||||
document.getElementById("hintBtn").disabled = false;
|
|
||||||
document.getElementById("hintBox").classList.add("hidden");
|
document.getElementById("hintBox").classList.add("hidden");
|
||||||
document.getElementById("feedbackBox").innerHTML = "";
|
document.getElementById("feedbackBox").innerHTML = "";
|
||||||
|
|
||||||
|
if (qData.question_type === "multiple_choice" && qData.options && qData.options.length > 0) {
|
||||||
|
textGroup.classList.add("hidden");
|
||||||
|
choiceGroup.classList.remove("hidden");
|
||||||
|
choiceContainer.innerHTML = "";
|
||||||
|
|
||||||
|
const letters = ["A", "B", "C", "D", "E", "F"];
|
||||||
|
qData.options.forEach((opt, idx) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "choice-btn";
|
||||||
|
btn.type = "button";
|
||||||
|
btn.innerHTML = `<span class="choice-letter">${letters[idx] || "•"}</span> <span class="choice-text">${opt}</span>`;
|
||||||
|
btn.onclick = () => submitAnswer(opt, btn);
|
||||||
|
choiceContainer.appendChild(btn);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
choiceGroup.classList.add("hidden");
|
||||||
|
textGroup.classList.remove("hidden");
|
||||||
|
input.value = "";
|
||||||
|
input.disabled = false;
|
||||||
|
document.getElementById("checkBtn").disabled = false;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hintBtn = document.getElementById("hintBtn");
|
||||||
|
if (hintBtn) hintBtn.disabled = false;
|
||||||
|
const hintBtnMC = document.getElementById("hintBtnMC");
|
||||||
|
if (hintBtnMC) hintBtnMC.disabled = false;
|
||||||
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,17 +147,28 @@ function updateProgress() {
|
|||||||
document.getElementById("progressBar").style.width = percent + "%";
|
document.getElementById("progressBar").style.width = percent + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkAnswer() {
|
function checkAnswer() {
|
||||||
const input = document.getElementById("answerInput");
|
const input = document.getElementById("answerInput");
|
||||||
const answer = input.value.trim();
|
const answer = input.value.trim();
|
||||||
|
|
||||||
if (!answer) {
|
if (!answer) {
|
||||||
input.focus();
|
input.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
submitAnswer(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAnswer(answer, clickedBtn = null) {
|
||||||
const checkBtn = document.getElementById("checkBtn");
|
const checkBtn = document.getElementById("checkBtn");
|
||||||
checkBtn.disabled = true;
|
const input = document.getElementById("answerInput");
|
||||||
|
if (checkBtn) checkBtn.disabled = true;
|
||||||
|
if (input) input.disabled = true;
|
||||||
|
|
||||||
|
// Disable all choice buttons
|
||||||
|
const choiceBtns = document.querySelectorAll(".choice-btn");
|
||||||
|
choiceBtns.forEach(b => b.disabled = true);
|
||||||
|
if (clickedBtn) {
|
||||||
|
clickedBtn.classList.add("choice-selected");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/game/check", {
|
const res = await fetch("/api/game/check", {
|
||||||
@@ -157,9 +194,12 @@ async function checkAnswer() {
|
|||||||
if (result.correct) {
|
if (result.correct) {
|
||||||
// Success!
|
// Success!
|
||||||
playSound('magic');
|
playSound('magic');
|
||||||
|
if (clickedBtn) {
|
||||||
|
clickedBtn.classList.remove("choice-selected");
|
||||||
|
clickedBtn.classList.add("choice-correct");
|
||||||
|
}
|
||||||
frageBox.classList.add("sparkle-effect");
|
frageBox.classList.add("sparkle-effect");
|
||||||
feedbackBox.innerHTML = `<span class="feedback-correct">✨ Richtig! 1 Hauspunkt für dein Haus! ✨</span>`;
|
feedbackBox.innerHTML = `<span class="feedback-correct">✨ Richtig! 1 Hauspunkt für dein Haus! ✨</span>`;
|
||||||
input.disabled = true;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
frageBox.classList.remove("sparkle-effect");
|
frageBox.classList.remove("sparkle-effect");
|
||||||
@@ -175,6 +215,10 @@ async function checkAnswer() {
|
|||||||
} else {
|
} else {
|
||||||
// Incorrect
|
// Incorrect
|
||||||
playSound('wrong');
|
playSound('wrong');
|
||||||
|
if (clickedBtn) {
|
||||||
|
clickedBtn.classList.remove("choice-selected");
|
||||||
|
clickedBtn.classList.add("choice-wrong");
|
||||||
|
}
|
||||||
frageBox.classList.add("shake-effect");
|
frageBox.classList.add("shake-effect");
|
||||||
setTimeout(() => frageBox.classList.remove("shake-effect"), 500);
|
setTimeout(() => frageBox.classList.remove("shake-effect"), 500);
|
||||||
|
|
||||||
@@ -182,12 +226,11 @@ async function checkAnswer() {
|
|||||||
document.getElementById("attemptBadge").innerText = `Versuch ${Math.min(attemptsUsed + 1, maxAttempts)} von ${maxAttempts}`;
|
document.getElementById("attemptBadge").innerText = `Versuch ${Math.min(attemptsUsed + 1, maxAttempts)} von ${maxAttempts}`;
|
||||||
|
|
||||||
if (result.attempts_left === 0) {
|
if (result.attempts_left === 0) {
|
||||||
// Exhausted all 3 attempts
|
// Exhausted all attempts
|
||||||
feedbackBox.innerHTML = `
|
feedbackBox.innerHTML = `
|
||||||
<div class="feedback-wrong">Leider nicht richtig (3 Fehlversuche).</div>
|
<div class="feedback-wrong">Leider nicht richtig (3 Fehlversuche).</div>
|
||||||
<div class="feedback-revealed">Richtige Lösung: <strong>${result.revealed_answer}</strong></div>
|
<div class="feedback-revealed">Richtige Lösung: <strong>${result.revealed_answer}</strong></div>
|
||||||
`;
|
`;
|
||||||
input.disabled = true;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (result.is_finished) {
|
if (result.is_finished) {
|
||||||
@@ -202,14 +245,21 @@ async function checkAnswer() {
|
|||||||
} else {
|
} else {
|
||||||
// Still attempts remaining
|
// Still attempts remaining
|
||||||
feedbackBox.innerHTML = `<span class="feedback-wrong">Leider falsch! Noch ${result.attempts_left} Versuch(e).</span>`;
|
feedbackBox.innerHTML = `<span class="feedback-wrong">Leider falsch! Noch ${result.attempts_left} Versuch(e).</span>`;
|
||||||
checkBtn.disabled = false;
|
if (checkBtn) checkBtn.disabled = false;
|
||||||
input.select();
|
if (input) {
|
||||||
|
input.disabled = false;
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
// Re-enable choice buttons
|
||||||
|
choiceBtns.forEach(b => b.disabled = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Netzwerkfehler: " + err.message);
|
alert("Netzwerkfehler: " + err.message);
|
||||||
checkBtn.disabled = false;
|
if (checkBtn) checkBtn.disabled = false;
|
||||||
|
if (input) input.disabled = false;
|
||||||
|
choiceBtns.forEach(b => b.disabled = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
function toggleVorschlagType(type) {
|
||||||
|
const mcGroup = document.getElementById("mcOptionsGroup");
|
||||||
|
const antwortLabel = document.getElementById("antwortLabel");
|
||||||
|
const antwortHelp = document.getElementById("antwortHelp");
|
||||||
|
|
||||||
|
if (type === "multiple_choice") {
|
||||||
|
mcGroup.classList.remove("hidden");
|
||||||
|
antwortLabel.innerText = "Richtige Antwort (die korrekte Option):";
|
||||||
|
antwortHelp.innerText = "Diese Antwort wird zusammen mit den falschen Optionen zufällig für den Spieler angeordnet.";
|
||||||
|
} else {
|
||||||
|
mcGroup.classList.add("hidden");
|
||||||
|
antwortLabel.innerText = "Richtige Antwort:";
|
||||||
|
antwortHelp.innerText = "Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitSuggestion(event) {
|
async function submitSuggestion(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -5,6 +21,7 @@ async function submitSuggestion(event) {
|
|||||||
const frage = document.getElementById("frage").value.trim();
|
const frage = document.getElementById("frage").value.trim();
|
||||||
const antwort = document.getElementById("antwort").value.trim();
|
const antwort = document.getElementById("antwort").value.trim();
|
||||||
const tipp = document.getElementById("tipp").value.trim();
|
const tipp = document.getElementById("tipp").value.trim();
|
||||||
|
const qType = document.getElementById("q_type").value;
|
||||||
const submitBtn = document.getElementById("submitBtn");
|
const submitBtn = document.getElementById("submitBtn");
|
||||||
const resultBox = document.getElementById("resultMessage");
|
const resultBox = document.getElementById("resultMessage");
|
||||||
|
|
||||||
@@ -13,19 +30,36 @@ async function submitSuggestion(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let falscheAntworten = [];
|
||||||
|
if (qType === "multiple_choice") {
|
||||||
|
const w1 = document.getElementById("wrong1").value.trim();
|
||||||
|
const w2 = document.getElementById("wrong2").value.trim();
|
||||||
|
const w3 = document.getElementById("wrong3").value.trim();
|
||||||
|
falscheAntworten = [w1, w2, w3].filter(x => x.length > 0);
|
||||||
|
|
||||||
|
if (falscheAntworten.length === 0) {
|
||||||
|
alert("Bitte gib bei Multiple-Choice mindestens eine falsche Antwortmöglichkeit an.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.innerText = "✨ Vorschlag wird übertragen...";
|
submitBtn.innerText = "✨ Vorschlag wird übertragen...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const payload = {
|
||||||
|
name: name || "Gast",
|
||||||
|
frage: frage,
|
||||||
|
antwort: antwort,
|
||||||
|
tipp: tipp || null,
|
||||||
|
question_type: qType,
|
||||||
|
falsche_antworten: falscheAntworten
|
||||||
|
};
|
||||||
|
|
||||||
const res = await fetch("/api/suggestions", {
|
const res = await fetch("/api/suggestions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload)
|
||||||
name: name || "Gast",
|
|
||||||
frage: frage,
|
|
||||||
antwort: antwort,
|
|
||||||
tipp: tipp || null
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -39,6 +73,7 @@ async function submitSuggestion(event) {
|
|||||||
`;
|
`;
|
||||||
resultBox.classList.remove("hidden");
|
resultBox.classList.remove("hidden");
|
||||||
document.getElementById("suggestionForm").reset();
|
document.getElementById("suggestionForm").reset();
|
||||||
|
toggleVorschlagType("text");
|
||||||
} else {
|
} else {
|
||||||
resultBox.innerHTML = `
|
resultBox.innerHTML = `
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
|
|||||||
@@ -52,15 +52,25 @@
|
|||||||
{% for q in pending_questions %}
|
{% for q in pending_questions %}
|
||||||
<div class="card question-admin-card" id="q-card-{{ q.id }}">
|
<div class="card question-admin-card" id="q-card-{{ q.id }}">
|
||||||
<div class="q-header">
|
<div class="q-header">
|
||||||
<span class="badge badge-warning">⚠️ Status: Prüfung ausstehend</span>
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<span class="badge badge-warning">⚠️ Prüfung ausstehend</span>
|
||||||
|
{% if q.question_type == 'multiple_choice' %}
|
||||||
|
<span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-muted">🔤 Freitext</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<span class="q-submitter">Von: <strong>{{ q.submitted_by or 'Gast' }}</strong> am {{ q.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
<span class="q-submitter">Von: <strong>{{ q.submitted_by or 'Gast' }}</strong> am {{ q.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="q-body">
|
<div class="q-body">
|
||||||
<h3>{{ q.question }}</h3>
|
<h3>{{ q.question }}</h3>
|
||||||
<p><strong>Erwartete Antwort:</strong> <span class="highlight-answer">{{ q.canonical_answer }}</span></p>
|
<p><strong>Erwartete Antwort:</strong> <span class="highlight-answer">{{ q.canonical_answer }}</span></p>
|
||||||
|
{% if q.options %}
|
||||||
|
<p style="margin-top: 5px;"><strong>Auswahl-Optionen:</strong> <code>{{ q.options }}</code></p>
|
||||||
|
{% endif %}
|
||||||
{% if q.hint %}
|
{% if q.hint %}
|
||||||
<p><strong>Tipp:</strong> <em>{{ q.hint }}</em></p>
|
<p style="margin-top: 5px;"><strong>Tipp:</strong> <em>{{ q.hint }}</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Interaktives Testfeld für den Admin -->
|
<!-- Interaktives Testfeld für den Admin -->
|
||||||
@@ -93,10 +103,21 @@
|
|||||||
<label>Frage:</label>
|
<label>Frage:</label>
|
||||||
<input type="text" name="question" class="input-text" value="{{ q.question }}" required>
|
<input type="text" name="question" class="input-text" value="{{ q.question }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Fragentyp:</label>
|
||||||
|
<select name="question_type" class="input-text">
|
||||||
|
<option value="text" {% if q.question_type != 'multiple_choice' %}selected{% endif %}>🔤 Freitext</option>
|
||||||
|
<option value="multiple_choice" {% if q.question_type == 'multiple_choice' %}selected{% endif %}>🔘 Multiple Choice</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Richtige Antwort:</label>
|
<label>Richtige Antwort:</label>
|
||||||
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
|
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Optionen (JSON-Liste oder Komma-getrennt für Multiple Choice):</label>
|
||||||
|
<input type="text" name="options_raw" class="input-text" value="{{ q.options or '' }}">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Tipp:</label>
|
<label>Tipp:</label>
|
||||||
<input type="text" name="hint" class="input-text" value="{{ q.hint or '' }}">
|
<input type="text" name="hint" class="input-text" value="{{ q.hint or '' }}">
|
||||||
@@ -120,15 +141,25 @@
|
|||||||
{% for q in active_questions %}
|
{% for q in active_questions %}
|
||||||
<div class="card question-admin-card" id="q-card-{{ q.id }}">
|
<div class="card question-admin-card" id="q-card-{{ q.id }}">
|
||||||
<div class="q-header">
|
<div class="q-header">
|
||||||
<span class="badge badge-success">Reihenfolge #{{ q.sort_order }}</span>
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<span class="badge badge-success">Reihenfolge #{{ q.sort_order }}</span>
|
||||||
|
{% if q.question_type == 'multiple_choice' %}
|
||||||
|
<span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-muted">🔤 Freitext</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<span class="q-category">Kategorie: {{ q.category }}</span>
|
<span class="q-category">Kategorie: {{ q.category }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="q-body">
|
<div class="q-body">
|
||||||
<h3>{{ loop.index }}. {{ q.question }}</h3>
|
<h3>{{ loop.index }}. {{ q.question }}</h3>
|
||||||
<p><strong>Antwort:</strong> <span class="highlight-answer">{{ q.canonical_answer }}</span></p>
|
<p><strong>Antwort:</strong> <span class="highlight-answer">{{ q.canonical_answer }}</span></p>
|
||||||
|
{% if q.options %}
|
||||||
|
<p style="margin-top: 5px;"><strong>Optionen:</strong> <code>{{ q.options }}</code></p>
|
||||||
|
{% endif %}
|
||||||
{% if q.hint %}
|
{% if q.hint %}
|
||||||
<p><strong>Tipp:</strong> <em>{{ q.hint }}</em></p>
|
<p style="margin-top: 5px;"><strong>Tipp:</strong> <em>{{ q.hint }}</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -144,10 +175,21 @@
|
|||||||
<label>Frage:</label>
|
<label>Frage:</label>
|
||||||
<input type="text" name="question" class="input-text" value="{{ q.question }}" required>
|
<input type="text" name="question" class="input-text" value="{{ q.question }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Fragentyp:</label>
|
||||||
|
<select name="question_type" class="input-text">
|
||||||
|
<option value="text" {% if q.question_type != 'multiple_choice' %}selected{% endif %}>🔤 Freitext</option>
|
||||||
|
<option value="multiple_choice" {% if q.question_type == 'multiple_choice' %}selected{% endif %}>🔘 Multiple Choice</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Richtige Antwort:</label>
|
<label>Richtige Antwort:</label>
|
||||||
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
|
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Optionen (JSON-Liste oder Komma-getrennt für Multiple Choice):</label>
|
||||||
|
<input type="text" name="options_raw" class="input-text" value="{{ q.options or '' }}">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Tipp:</label>
|
<label>Tipp:</label>
|
||||||
<input type="text" name="hint" class="input-text" value="{{ q.hint or '' }}">
|
<input type="text" name="hint" class="input-text" value="{{ q.hint or '' }}">
|
||||||
@@ -179,6 +221,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_type">Fragentyp:</label>
|
||||||
|
<select name="question_type" id="new_type" class="input-text" onchange="toggleAdminNewType(this.value)">
|
||||||
|
<option value="text" selected>🔤 Freitext-Eingabe (Tippen)</option>
|
||||||
|
<option value="multiple_choice">🔘 Multiple Choice (Auswahl)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new_question">Frage:</label>
|
<label for="new_question">Frage:</label>
|
||||||
<textarea name="question" id="new_question" class="textarea" rows="2" required placeholder="z. B. Welcher Zauber entwaffnet Gegner?"></textarea>
|
<textarea name="question" id="new_question" class="textarea" rows="2" required placeholder="z. B. Welcher Zauber entwaffnet Gegner?"></textarea>
|
||||||
@@ -189,6 +239,14 @@
|
|||||||
<input type="text" name="canonical_answer" id="new_answer" class="input-text" required placeholder="z. B. Expelliarmus">
|
<input type="text" name="canonical_answer" id="new_answer" class="input-text" required placeholder="z. B. Expelliarmus">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiple Choice Optionen für Admin -->
|
||||||
|
<div id="adminMcOptions" class="form-group hidden" style="background: rgba(255,255,255,0.03); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-gold);">
|
||||||
|
<label style="color: var(--gold);">Falsche Optionen für Multiple Choice (Optionen 2 bis 4):</label>
|
||||||
|
<input type="text" name="wrong_option_1" class="input-text" placeholder="Falsche Option 1 (z.B. Stupor)" style="margin-bottom: 8px;">
|
||||||
|
<input type="text" name="wrong_option_2" class="input-text" placeholder="Falsche Option 2 (z.B. Lumos)" style="margin-bottom: 8px;">
|
||||||
|
<input type="text" name="wrong_option_3" class="input-text" placeholder="Falsche Option 3 (z.B. Alohomora)">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new_hint">Tipp (optional):</label>
|
<label for="new_hint">Tipp (optional):</label>
|
||||||
<input type="text" name="hint" id="new_hint" class="input-text" placeholder="z. B. Harrys Lieblingszauber">
|
<input type="text" name="hint" id="new_hint" class="input-text" placeholder="z. B. Harrys Lieblingszauber">
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
|
|
||||||
<h2 id="frageText" class="question-text">Lade verzauberte Schriftrollen...</h2>
|
<h2 id="frageText" class="question-text">Lade verzauberte Schriftrollen...</h2>
|
||||||
|
|
||||||
<div class="answer-form">
|
<!-- Freitext-Eingabe (wenn question_type == 'text') -->
|
||||||
|
<div id="textAnswerGroup" class="answer-form">
|
||||||
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort hier eingeben..." autocomplete="off">
|
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort hier eingeben..." autocomplete="off">
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
@@ -66,6 +67,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
|
||||||
|
<div id="choiceAnswerGroup" class="choice-group hidden">
|
||||||
|
<div id="choiceOptionsContainer" class="choice-grid"></div>
|
||||||
|
<div class="btn-group" style="margin-top: 15px;">
|
||||||
|
<button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()">
|
||||||
|
💡 Zaubertipp
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="hintBox" class="hint-box hidden">
|
<div id="hintBox" class="hint-box hidden">
|
||||||
<strong>📜 Notiz aus der verbotenen Abteilung:</strong>
|
<strong>📜 Notiz aus der verbotenen Abteilung:</strong>
|
||||||
<p id="hintText"></p>
|
<p id="hintText"></p>
|
||||||
|
|||||||
@@ -24,9 +24,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="antwort">Richtige Antwort: <span class="required">*</span></label>
|
<label for="q_type">Art der Frage:</label>
|
||||||
|
<select id="q_type" name="q_type" class="input-text" onchange="toggleVorschlagType(this.value)">
|
||||||
|
<option value="text" selected>🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)</option>
|
||||||
|
<option value="multiple_choice">🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="antwort"><span id="antwortLabel">Richtige Antwort:</span> <span class="required">*</span></label>
|
||||||
<input type="text" id="antwort" name="antwort" class="input-text" placeholder="z. B. Albus Dumbledore" required>
|
<input type="text" id="antwort" name="antwort" class="input-text" placeholder="z. B. Albus Dumbledore" required>
|
||||||
<small class="field-help">Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.</small>
|
<small class="field-help" id="antwortHelp">Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zusätzliche Optionen für Multiple Choice -->
|
||||||
|
<div id="mcOptionsGroup" class="form-group hidden" style="background: rgba(255,255,255,0.03); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-gold);">
|
||||||
|
<label style="color: var(--gold);">Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):</label>
|
||||||
|
<input type="text" id="wrong1" class="input-text" placeholder="Falsche Option 1 (z. B. Severus Snape)" style="margin-bottom: 8px;">
|
||||||
|
<input type="text" id="wrong2" class="input-text" placeholder="Falsche Option 2 (z. B. Minerva McGonagall)" style="margin-bottom: 8px;">
|
||||||
|
<input type="text" id="wrong3" class="input-text" placeholder="Falsche Option 3 (z. B. Rubeus Hagrid)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from app.database import engine
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
with engine.connect() as conn:
|
||||||
|
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS question_type VARCHAR(20) DEFAULT 'text'"))
|
||||||
|
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS options TEXT NULL"))
|
||||||
|
conn.commit()
|
||||||
|
print("Migration successful: added question_type and options columns.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import http.cookiejar
|
||||||
|
import json
|
||||||
|
|
||||||
|
BASE_URL = "http://127.0.0.1:8001"
|
||||||
|
|
||||||
|
def test_mc():
|
||||||
|
print("--- 1. Submit Multiple Choice Suggestion ---")
|
||||||
|
sugg_payload = json.dumps({
|
||||||
|
"frage": "Welches Tier ist das Wappentier von Ravenclaw?",
|
||||||
|
"antwort": "Adler",
|
||||||
|
"tipp": "Viele Muggel denken fälschlicherweise an einen Raben.",
|
||||||
|
"name": "Luna Lovegood",
|
||||||
|
"question_type": "multiple_choice",
|
||||||
|
"falsche_antworten": ["Rabe", "Dachs", "Schlange"]
|
||||||
|
}).encode("utf-8")
|
||||||
|
|
||||||
|
req = urllib.request.Request(f"{BASE_URL}/api/suggestions", data=sugg_payload, headers={"Content-Type": "application/json"})
|
||||||
|
res = urllib.request.urlopen(req)
|
||||||
|
res_data = json.loads(res.read().decode())
|
||||||
|
print("Suggestion result:", res_data)
|
||||||
|
assert res_data["success"] is True
|
||||||
|
|
||||||
|
print("\n--- 2. Admin Login & Approve Question ---")
|
||||||
|
cj = http.cookiejar.CookieJar()
|
||||||
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||||
|
|
||||||
|
login_data = urllib.parse.urlencode({"username": "admin", "password": "Alohomora2026!"}).encode()
|
||||||
|
opener.open(f"{BASE_URL}/admin/login", data=login_data)
|
||||||
|
|
||||||
|
admin_page = opener.open(f"{BASE_URL}/admin").read().decode()
|
||||||
|
assert "Wappentier von Ravenclaw" in admin_page
|
||||||
|
assert "Multiple-Choice" in admin_page
|
||||||
|
print("Admin dashboard shows new Multiple-Choice question.")
|
||||||
|
|
||||||
|
# Find the question ID from DB
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models import Question
|
||||||
|
db = SessionLocal()
|
||||||
|
q = db.query(Question).filter(Question.question.like("%Wappentier von Ravenclaw%")).first()
|
||||||
|
qid = q.id
|
||||||
|
print(f"Found Question ID #{qid}, status={q.status}, type={q.question_type}, options={q.options}")
|
||||||
|
assert q.question_type == "multiple_choice"
|
||||||
|
parsed_opts = json.loads(q.options)
|
||||||
|
assert "Adler" in parsed_opts
|
||||||
|
assert "Rabe" in parsed_opts
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
# Approve question
|
||||||
|
approve_res = opener.open(f"{BASE_URL}/admin/questions/{qid}/approve", data=b"")
|
||||||
|
app_data = json.loads(approve_res.read().decode())
|
||||||
|
print("Approve result:", app_data)
|
||||||
|
assert app_data["success"] is True
|
||||||
|
|
||||||
|
print("\n--- 3. Verify Question Served to Client with Shuffled Options ---")
|
||||||
|
from app.main import format_question_for_client
|
||||||
|
db = SessionLocal()
|
||||||
|
q_updated = db.query(Question).filter(Question.id == qid).first()
|
||||||
|
client_q = format_question_for_client(q_updated, 16, 16)
|
||||||
|
print("Client Question formatted:", client_q)
|
||||||
|
assert client_q["question_type"] == "multiple_choice"
|
||||||
|
assert len(client_q["options"]) == 4
|
||||||
|
assert "Adler" in client_q["options"]
|
||||||
|
assert "canonical_answer" not in client_q
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
print("\n✅ Multiple Choice & Free Text integration tests successfully passed!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_mc()
|
||||||
Reference in New Issue
Block a user