From 8f01c0396e0a3816c416d7ac7113c77334243de0 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 26 Apr 2026 20:38:26 +0200 Subject: [PATCH] =?UTF-8?q?Orphan-Junction-Cleanup=20+=20Refresh-Button=20?= =?UTF-8?q?f=C3=BCr=20Fragen/W=C3=B6rter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: DELETE question/word räumt alle Junction-Zeilen mit auf - Backend: /purge-orphans bereinigt verwaiste Junctions per Objekt - Frontend: reloadQW ruft purgeOrphans vor dem Neu-Laden auf - Frontend: ↺-Button in Wörter- und Fragen-Sidebar Co-Authored-By: Claude Sonnet 4.6 --- app.py | 46 +++++++++++++++++++++++++++++-- frontend/src/api.ts | 10 +++++++ frontend/src/pages/GenerateIt.tsx | 12 ++++++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index b6708d1..bcc4a4b 100644 --- a/app.py +++ b/app.py @@ -1242,10 +1242,22 @@ def get_object_words_list(obj_id: str): return jsonify({"data": items}) +def _delete_junction_rows(collection: str, field: str, value: str, token: str): + """Löscht alle Junction-Zeilen für einen gegebenen Fremdschlüssel.""" + data, s = _directus("GET", + f"/items/{collection}?filter[{field}][_eq]={value}&fields=id&limit=5000", token) + ids = [e["id"] for e in (data.get("data") or []) if e.get("id")] + if ids: + _directus("DELETE", f"/items/{collection}", token, ids) + + @app.route("/api/question/", methods=["DELETE"]) def delete_question_item(q_id: str): - """Löscht eine Frage aus Directus.""" + """Löscht eine Frage + alle zugehörigen Junction-Zeilen aus Directus.""" token = request.headers.get("Authorization", "") + _delete_junction_rows("questions_objects", "questions_id", q_id, token) + _delete_junction_rows("questions_distractor_words", "questions_id", q_id, token) + _delete_junction_rows("questions_words", "questions_id", q_id, token) _, status = _directus("DELETE", f"/items/questions/{q_id}", token) if status in (200, 204): return jsonify({"ok": True}) @@ -1254,14 +1266,44 @@ def delete_question_item(q_id: str): @app.route("/api/word/", methods=["DELETE"]) def delete_word_item(w_id: str): - """Löscht ein Wort aus Directus.""" + """Löscht ein Wort + alle zugehörigen Junction-Zeilen aus Directus.""" token = request.headers.get("Authorization", "") + _delete_junction_rows("words_objects", "words_id", w_id, token) + _delete_junction_rows("questions_words", "words_id", w_id, token) + _delete_junction_rows("questions_distractor_words", "words_id", w_id, token) _, status = _directus("DELETE", f"/items/words/{w_id}", token) if status in (200, 204): return jsonify({"ok": True}) return jsonify({"error": "Delete failed"}), status +@app.route("/api/object//purge-orphans", methods=["POST"]) +def purge_orphan_junctions(obj_id: str): + """ + Bereinigt verwaiste Junction-Einträge für ein Objekt: + Entfernt Zeilen aus questions_objects/words_objects deren Frage/Wort nicht mehr existiert. + """ + token = request.headers.get("Authorization", "") + removed = 0 + + for junc_col, fk_field, item_col in [ + ("questions_objects", "questions_id", "questions"), + ("words_objects", "words_id", "words"), + ]: + junc_data, _ = _directus("GET", + f"/items/{junc_col}?filter[objects_id][_eq]={obj_id}&fields=id,{fk_field}&limit=5000", token) + for row in (junc_data.get("data") or []): + fk_val = row.get(fk_field) + if not fk_val: + continue + item_data, s = _directus("GET", f"/items/{item_col}/{fk_val}?fields=id", token) + if s != 200 or not item_data.get("data"): + _directus("DELETE", f"/items/{junc_col}/{row['id']}", token) + removed += 1 + + return jsonify({"ok": True, "orphans_removed": removed}) + + @app.route("/api/fix-distractor-field", methods=["POST"]) def fix_distractor_field(): """Setzt special=m2m auf questions.distractor_words (einmalig).""" diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c3c8c40..6c43754 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -281,3 +281,13 @@ export async function deleteWord(wId: string, token: string): Promise { }) if (!res.ok) throw new Error('Fehler beim Löschen des Worts') } + +export async function purgeOrphans(objId: string, token: string): Promise<{ orphans_removed: number }> { + const res = await fetch(`/api/object/${encodeURIComponent(objId)}/purge-orphans`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + }) + const data = await res.json() + if (!res.ok) throw new Error('Fehler beim Bereinigen') + return data +} diff --git a/frontend/src/pages/GenerateIt.tsx b/frontend/src/pages/GenerateIt.tsx index 4a09e5b..f5d91fd 100644 --- a/frontend/src/pages/GenerateIt.tsx +++ b/frontend/src/pages/GenerateIt.tsx @@ -14,6 +14,7 @@ import { getObjectWords, deleteQuestion, deleteWord, + purgeOrphans, type GenerateStats, type ObjectQuestion, type ObjectWord, @@ -180,6 +181,7 @@ export default function GenerateIt() { const reloadQW = (objId: string) => { if (!token) return + purgeOrphans(objId, token).catch(console.error) getObjectQuestions(objId, token).then(setQuestions).catch(console.error) getObjectWords(objId, token).then(setObjWords).catch(console.error) } @@ -439,9 +441,12 @@ export default function GenerateIt() { {/* Words sidebar */}