short_answer_de/en/se Felder + Distractor-Wörter in Fragen-Sidebar

- Directus: questions.short_answer_de/en/se Text-Felder angelegt
- Backend: short_answer_de beim Erstellen speichern
- Backend: get_object_questions_list gibt short_answer_de + distractor_words zurück
- Frontend: Sidebar zeigt Kurzantwort (blau) + Ablenker-Chips pro Frage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 20:15:14 +02:00
parent f4a4b40914
commit 47af0d705c
3 changed files with 68 additions and 11 deletions

40
app.py
View File

@@ -937,7 +937,7 @@ def _find_or_create_word(title_de: str, level: int, token: str):
def _find_or_create_question(question_de: str, answer_de: str, level: int,
short_answer_id, obj_id: str, token: str):
short_answer_id, short_answer_de: str, obj_id: str, token: str):
"""Return (question_id, is_new). Creates question with status=draft if missing."""
enc = urllib.parse.quote(question_de, safe="")
data, status = _directus(
@@ -957,6 +957,8 @@ def _find_or_create_question(question_de: str, answer_de: str, level: int,
}
if short_answer_id:
body["short_answer"] = short_answer_id
if short_answer_de:
body["short_answer_de"] = short_answer_de
data, status = _directus("POST", "/items/questions", token, body)
if status in (200, 201):
@@ -1098,7 +1100,7 @@ def generate_questions(obj_id: str):
# Frage anlegen / verknüpfen
try:
q_id, q_is_new = _find_or_create_question(q_de, a_de, level, short_answer_id, obj_id, token)
q_id, q_is_new = _find_or_create_question(q_de, a_de, level, short_answer_id, short_text, obj_id, token)
except Exception as e:
print(f"[generate_questions] question error level {level}: {e}")
continue
@@ -1180,17 +1182,47 @@ def publish_questions(obj_id: str):
@app.route("/api/object/<obj_id>/questions", methods=["GET"])
def get_object_questions_list(obj_id: str):
"""Gibt alle verknüpften Fragen eines Objekts zurück (2-Schritt-Query)."""
"""Gibt alle verknüpften Fragen eines Objekts zurück (mit short_answer_de + distractor_words)."""
token = request.headers.get("Authorization", "")
# Schritt 1: Frage-IDs aus Junction
junc, _ = _directus("GET",
f"/items/questions_objects?filter[objects_id][_eq]={obj_id}&fields=questions_id&limit=200", token)
q_ids = [e["questions_id"] for e in (junc.get("data") or []) if e.get("questions_id")]
if not q_ids:
return jsonify({"data": []})
# Schritt 2: Fragen laden (inkl. short_answer_de)
ids_param = urllib.parse.quote(",".join(q_ids), safe="")
q_data, _ = _directus("GET",
f"/items/questions?filter[id][_in]={ids_param}&fields=id,question_de,answer_de,level,status&limit=200", token)
f"/items/questions?filter[id][_in]={ids_param}&fields=id,question_de,answer_de,short_answer_de,level,status&limit=200", token)
items = sorted(q_data.get("data") or [], key=lambda x: x.get("level") or 0)
# Schritt 3: Distractor-Wörter pro Frage (Bulk)
dw_junc, _ = _directus("GET",
f"/items/questions_distractor_words?filter[questions_id][_in]={ids_param}&fields=questions_id,words_id&limit=5000", token)
dw_entries = dw_junc.get("data") or []
# Wort-IDs sammeln und Titel laden
all_word_ids = list({e["words_id"] for e in dw_entries if e.get("words_id")})
word_title_map: dict[str, str] = {}
if all_word_ids:
wids_param = urllib.parse.quote(",".join(all_word_ids), safe="")
w_data, _ = _directus("GET",
f"/items/words?filter[id][_in]={wids_param}&fields=id,title_de&limit=5000", token)
word_title_map = {w["id"]: w["title_de"] for w in (w_data.get("data") or [])}
# Distractor-Wörter je Frage gruppieren
dw_by_question: dict[str, list[str]] = {}
for e in dw_entries:
qid = e.get("questions_id")
wid = e.get("words_id")
if qid and wid and wid in word_title_map:
dw_by_question.setdefault(qid, []).append(word_title_map[wid])
for item in items:
item["distractor_words"] = dw_by_question.get(item["id"], [])
return jsonify({"data": items})

View File

@@ -235,6 +235,8 @@ export interface ObjectQuestion {
id: string
question_de: string
answer_de: string
short_answer_de: string | null
distractor_words: string[]
level: number
status: string
}

View File

@@ -462,7 +462,7 @@ export default function GenerateIt() {
</aside>
{/* Questions sidebar */}
<aside className="sidebar sidebar--right" style={{ width: 280, minWidth: 220 }}>
<aside className="sidebar sidebar--right" style={{ width: 300, minWidth: 240 }}>
<div className="sidebar-panel" style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<h3 className="sidebar-heading">
Fragen
@@ -473,20 +473,43 @@ export default function GenerateIt() {
) : (
<div style={{ overflowY: 'auto', flex: 1 }}>
{questions.map(q => (
<div key={q.id} style={{ padding: '6px 10px', borderBottom: '1px solid var(--border)', fontSize: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 4 }}>
<span style={{ fontWeight: 600, color: 'var(--muted)', fontSize: 10, flexShrink: 0 }}>
<div key={q.id} style={{ padding: '7px 10px', borderBottom: '1px solid var(--border)', fontSize: 12 }}>
{/* Header: Level + Status + Delete */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3 }}>
<span style={{ fontWeight: 700, color: 'var(--muted)', fontSize: 10 }}>
L{q.level}
{q.status === 'published' && <span style={{ marginLeft: 4, color: 'var(--success)' }}>↑</span>}
</span>
<button
onClick={() => handleDeleteQuestion(q.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, lineHeight: 1, color: 'var(--muted)', fontSize: 11, flexShrink: 0 }}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: 'var(--muted)', fontSize: 11 }}
title="Frage löschen"
></button>
</div>
<div style={{ marginTop: 2, color: 'var(--fg)' }}>{q.question_de}</div>
<div style={{ marginTop: 2, color: 'var(--muted)', fontStyle: 'italic' }}>{q.answer_de}</div>
{/* Frage */}
<div style={{ color: 'var(--fg)', marginBottom: 2 }}>{q.question_de}</div>
{/* Antwort */}
<div style={{ color: 'var(--muted)', fontStyle: 'italic', marginBottom: q.short_answer_de || q.distractor_words?.length ? 4 : 0 }}>{q.answer_de}</div>
{/* Short Answer */}
{q.short_answer_de && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 3 }}>
<span style={{ fontSize: 10, color: 'var(--muted)', flexShrink: 0 }}>Kurz:</span>
<span style={{ background: 'var(--primary-muted, #e8f0fe)', color: 'var(--primary)', borderRadius: 4, padding: '1px 6px', fontSize: 11, fontWeight: 600 }}>
{q.short_answer_de}
</span>
</div>
)}
{/* Distractor Words */}
{q.distractor_words?.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
<span style={{ fontSize: 10, color: 'var(--muted)', alignSelf: 'center', flexShrink: 0 }}>Ablenker:</span>
{q.distractor_words.map(dw => (
<span key={dw} style={{ background: 'var(--bg-muted, #f5f5f5)', border: '1px solid var(--border)', borderRadius: 4, padding: '1px 5px', fontSize: 10 }}>
{dw}
</span>
))}
</div>
)}
</div>
))}
</div>