Add /api/setup-schema endpoint für Directus M2M Relationen

Konfiguriert alle fehlenden Directus-Relationen:
- questions.object → objects (FK fix)
- questions.short_answer → words (FK fix)
- questions ↔ objects M2M via questions_objects
- words ↔ objects M2M via words_objects
- questions ↔ words (distractor) M2M via questions_distractor_words
- words.linked_questions backref via questions_words

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 10:43:41 +02:00
parent 0f83210aec
commit 1bc6d8b30f

131
app.py
View File

@@ -1214,6 +1214,137 @@ def delete_word_item(w_id: str):
return jsonify({"error": "Delete failed"}), status return jsonify({"error": "Delete failed"}), status
@app.route("/api/setup-schema", methods=["POST"])
def setup_directus_schema():
"""
Einmalig ausführen: Konfiguriert alle M2M-Relationen in Directus.
Idempotent bereits vorhandene Felder/Relationen werden übersprungen.
"""
token = request.headers.get("Authorization", "")
results = []
def do(label, method, path, body=None):
data, status = _directus(method, path, token, body)
ok = status in (200, 201, 204)
results.append({"label": label, "status": status, "ok": ok,
"err": None if ok else (data.get("errors") or data)})
return ok
# ── Fix M2O: questions.object → objects ──────────────────────────────────
do("relation questions.object→objects", "POST", "/relations", {
"collection": "questions", "field": "object",
"related_collection": "objects",
"schema": {"on_delete": "SET NULL"},
"meta": {"one_deselect_action": "nullify"},
})
# Update display template so the object label shows
do("update field questions.object template", "PATCH", "/fields/questions/object", {
"meta": {"options": {"template": "{{user_notes}}"}}
})
# ── Fix M2O: questions.short_answer → words ───────────────────────────────
do("relation questions.short_answer→words", "POST", "/relations", {
"collection": "questions", "field": "short_answer",
"related_collection": "words",
"schema": {"on_delete": "SET NULL"},
"meta": {"one_deselect_action": "nullify"},
})
# ── M2M: questions ↔ objects via questions_objects ────────────────────────
do("field questions.linked_objects", "POST", "/fields/questions", {
"field": "linked_objects", "type": "alias",
"meta": {"special": ["m2m"], "interface": "list-m2m",
"options": {"template": "{{objects_id.user_notes}}"},
"note": "Verknüpfte Objekte"},
})
do("field objects.linked_questions", "POST", "/fields/objects", {
"field": "linked_questions", "type": "alias",
"meta": {"special": ["m2m"], "interface": "list-m2m",
"options": {"template": "{{questions_id.question_de}}"},
"note": "Verknüpfte Fragen"},
})
do("relation questions_objects.questions_id→questions", "POST", "/relations", {
"collection": "questions_objects", "field": "questions_id",
"related_collection": "questions",
"schema": {"on_delete": "CASCADE"},
"meta": {"junction_field": "objects_id", "one_field": "linked_objects",
"one_deselect_action": "nullify"},
})
do("relation questions_objects.objects_id→objects", "POST", "/relations", {
"collection": "questions_objects", "field": "objects_id",
"related_collection": "objects",
"schema": {"on_delete": "CASCADE"},
"meta": {"junction_field": "questions_id", "one_field": "linked_questions",
"one_deselect_action": "nullify"},
})
# ── M2M: words ↔ objects via words_objects ────────────────────────────────
do("field words.linked_objects", "POST", "/fields/words", {
"field": "linked_objects", "type": "alias",
"meta": {"special": ["m2m"], "interface": "list-m2m",
"options": {"template": "{{objects_id.user_notes}}"},
"note": "Verknüpfte Objekte"},
})
do("field objects.linked_words", "POST", "/fields/objects", {
"field": "linked_words", "type": "alias",
"meta": {"special": ["m2m"], "interface": "list-m2m",
"options": {"template": "{{words_id.title_de}}"},
"note": "Verknüpfte Wörter"},
})
do("relation words_objects.words_id→words", "POST", "/relations", {
"collection": "words_objects", "field": "words_id",
"related_collection": "words",
"schema": {"on_delete": "CASCADE"},
"meta": {"junction_field": "objects_id", "one_field": "linked_objects",
"one_deselect_action": "nullify"},
})
do("relation words_objects.objects_id→objects", "POST", "/relations", {
"collection": "words_objects", "field": "objects_id",
"related_collection": "objects",
"schema": {"on_delete": "CASCADE"},
"meta": {"junction_field": "words_id", "one_field": "linked_words",
"one_deselect_action": "nullify"},
})
# ── M2M: questions ↔ words (distractor) via questions_distractor_words ────
do("field questions.distractor_words", "POST", "/fields/questions", {
"field": "distractor_words", "type": "alias",
"meta": {"special": ["m2m"], "interface": "list-m2m",
"options": {"template": "{{words_id.title_de}}"},
"note": "Ablenker-Wörter (nicht in Frage/Antwort)"},
})
do("relation questions_distractor_words.questions_id→questions", "POST", "/relations", {
"collection": "questions_distractor_words", "field": "questions_id",
"related_collection": "questions",
"schema": {"on_delete": "CASCADE"},
"meta": {"junction_field": "words_id", "one_field": "distractor_words",
"one_deselect_action": "nullify"},
})
do("relation questions_distractor_words.words_id→words", "POST", "/relations", {
"collection": "questions_distractor_words", "field": "words_id",
"related_collection": "words",
"schema": {"on_delete": "CASCADE"},
"meta": {"junction_field": "questions_id", "one_deselect_action": "nullify"},
})
# ── Backref: words → questions via existing questions_words junction ───────
do("field words.linked_questions", "POST", "/fields/words", {
"field": "linked_questions", "type": "alias",
"meta": {"special": ["m2m"], "interface": "list-m2m",
"options": {"template": "{{questions_id.question_de}}"},
"note": "Fragen in denen dieses Wort vorkommt"},
})
# Patch the existing questions_words relation to add the one_field backref on words
do("patch relation questions_words.words_id one_field", "PATCH",
"/relations/questions_words/words_id", {
"meta": {"one_field": "linked_questions", "one_deselect_action": "nullify"}
})
failed = [r for r in results if not r["ok"]]
return jsonify({"ok": len(failed) == 0, "total": len(results),
"failed": len(failed), "results": results})
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)