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)