From 1bc6d8b30f2fee600135c9e240ced5275bfcf0c6 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 26 Apr 2026 10:43:41 +0200 Subject: [PATCH] =?UTF-8?q?Add=20/api/setup-schema=20endpoint=20f=C3=BCr?= =?UTF-8?q?=20Directus=20M2M=20Relationen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app.py | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/app.py b/app.py index 453b0b9..daba590 100644 --- a/app.py +++ b/app.py @@ -1214,6 +1214,137 @@ def delete_word_item(w_id: str): 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__": app.run(host="0.0.0.0", port=8000, debug=True)