From a622ac49df80699c526a75d1dbabdf3805920da0 Mon Sep 17 00:00:00 2001 From: Tim Leikauf Date: Wed, 6 May 2026 22:00:55 +0200 Subject: [PATCH] refactor(words): words_pictures auf natives Directus M2M umgestellt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET: Deep-Query über pictures.linked_words statt manuelle Junction-Abfrage - POST: PATCH /items/pictures/{id} mit linked_words.create statt _ensure_link - _ensure_junction/_ensure_link für words_pictures entfernt - Setup-Logik in _setup_words_pictures() ausgelagert (idempotent) - Batch-Insert aller neuen Links in einem einzigen PATCH-Call Co-Authored-By: Claude Sonnet 4.6 --- app.py | 145 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 60 deletions(-) diff --git a/app.py b/app.py index a3a4979..30cc047 100644 --- a/app.py +++ b/app.py @@ -113,74 +113,96 @@ def directus_object(obj_id): return jsonify(data), status +def _setup_words_pictures(token: str): + """Richtet M2M-Relation words ↔ pictures idempotent ein (läuft einmalig).""" + _directus("PATCH", "/fields/pictures/linked_words", token, { + "type": "alias", + "meta": {"special": ["m2m"], "interface": "list-m2m", + "options": {"template": "{{words_id.title_de}}"}, + "note": "Verknüpfte Safe Words"}, + }) + _directus("PATCH", "/fields/words/linked_pictures", token, { + "type": "alias", + "meta": {"special": ["m2m"], "interface": "list-m2m", + "options": {"template": "{{pictures_id.media}}"}, + "note": "Verknüpfte Bilder"}, + }) + _directus("POST", "/relations", token, { + "collection": "words_pictures", "field": "words_id", + "related_collection": "words", + "schema": {"on_delete": "CASCADE"}, + "meta": {"junction_field": "pictures_id", "one_field": "linked_pictures", + "one_deselect_action": "nullify"}, + }) + _directus("POST", "/relations", token, { + "collection": "words_pictures", "field": "pictures_id", + "related_collection": "pictures", + "schema": {"on_delete": "CASCADE"}, + "meta": {"junction_field": "words_id", "one_field": "linked_words", + "one_deselect_action": "nullify"}, + }) + + @app.route("/api/directus/pictures//words", methods=["GET", "POST"]) def directus_picture_words(pic_id): - """Proxy: Safe-Words eines Bildes laden (GET) oder speichern (POST).""" + """Proxy: Safe-Words eines Bildes laden (GET) oder speichern (POST). + Nutzt natives Directus M2M über pictures.linked_words. + """ token = request.headers.get("Authorization", "") if request.method == "GET": - junc, _ = _directus( + # Natives Directus Deep-Query über M2M-Relation + data, status = _directus( "GET", - f"/items/words_pictures?filter[pictures_id][_eq]={pic_id}&fields=id,words_id&limit=500", + f"/items/pictures/{pic_id}" + f"?fields[]=linked_words.id" + f"&fields[]=linked_words.words_id.id" + f"&fields[]=linked_words.words_id.title_de" + f"&fields[]=linked_words.words_id.level" + f"&fields[]=linked_words.words_id.status", token, ) - w_ids = [e["words_id"] for e in (junc.get("data") or []) if e.get("words_id")] - if not w_ids: + if status != 200: return jsonify({"data": []}) - ids_param = urllib.parse.quote(",".join(w_ids), safe="") - w_data, _ = _directus( - "GET", - f"/items/words?filter[id][_in]={ids_param}&filter[status][_neq]=archived&fields=id,title_de,level,status&limit=500", - token, - ) - junc_by_word = {e["words_id"]: e["id"] for e in (junc.get("data") or [])} - items = [ - { - "id": junc_by_word.get(w["id"], ""), - "word_id": w["id"], - "title_de": w["title_de"], - "level": w.get("level") or 50, - "status": w.get("status", ""), - } - for w in (w_data.get("data") or []) - ] + items = [] + for entry in ((data.get("data") or {}).get("linked_words") or []): + word = entry.get("words_id") or {} + if not isinstance(word, dict) or not word.get("id"): + continue + if word.get("status") == "archived": + continue + items.append({ + "id": entry.get("id", ""), + "word_id": word["id"], + "title_de": word.get("title_de", ""), + "level": word.get("level") or 50, + "status": word.get("status", ""), + }) return jsonify({"data": items}) else: # POST body = request.get_json(force=True, silent=True) or {} - words = body.get("words", []) + words_to_save = body.get("words", []) - # Junction + Relationen idempotent einrichten - _ensure_junction("words_pictures", "words_id", "pictures_id", token) - _directus("PATCH", "/fields/pictures/linked_words", token, { - "type": "alias", - "meta": {"special": ["m2m"], "interface": "list-m2m", - "options": {"template": "{{words_id.title_de}}"}}, - }) - _directus("PATCH", "/fields/words/linked_pictures", token, { - "type": "alias", - "meta": {"special": ["m2m"], "interface": "list-m2m", - "options": {"template": "{{pictures_id.media}}"}}, - }) - _directus("POST", "/relations", token, { - "collection": "words_pictures", "field": "words_id", - "related_collection": "words", - "schema": {"on_delete": "CASCADE"}, - "meta": {"junction_field": "pictures_id", - "one_field": "linked_pictures", - "one_deselect_action": "nullify"}, - }) - _directus("POST", "/relations", token, { - "collection": "words_pictures", "field": "pictures_id", - "related_collection": "pictures", - "schema": {"on_delete": "CASCADE"}, - "meta": {"junction_field": "words_id", - "one_field": "linked_words", - "one_deselect_action": "nullify"}, - }) + # Relationen einmalig sicherstellen + _setup_words_pictures(token) + # Bereits verknüpfte Word-IDs laden (Duplikat-Schutz) + existing_data, _ = _directus( + "GET", + f"/items/pictures/{pic_id}?fields[]=linked_words.words_id", + token, + ) + existing_ids = set() + for e in ((existing_data.get("data") or {}).get("linked_words") or []): + wid = e.get("words_id") + if wid: + existing_ids.add(wid if isinstance(wid, str) else wid.get("id", "")) + + # Wörter anlegen/finden, Level updaten, neue Links sammeln + new_links = [] saved = 0 - for entry in words: + for entry in words_to_save: title_de = (entry.get("title_de") or "").strip() level = int(entry.get("level") or 50) if not title_de: @@ -188,17 +210,20 @@ def directus_picture_words(pic_id): try: wid, is_new = _find_or_create_word(title_de, level, token) if not is_new: - # Wort existiert bereits → Level aktualisieren _directus("PATCH", f"/items/words/{wid}", token, {"level": level}) - _ensure_link( - "words_pictures", - {"words_id": wid, "pictures_id": pic_id}, - {"words_id": wid, "pictures_id": pic_id}, - token, - ) - saved += 1 + if wid not in existing_ids: + new_links.append({"words_id": wid}) + existing_ids.add(wid) + saved += 1 except Exception as e: print(f"[picture_words] error for '{title_de}': {e}") + + # Alle neuen Links in einem einzigen Directus-PATCH + if new_links: + _directus("PATCH", f"/items/pictures/{pic_id}", token, { + "linked_words": {"create": new_links} + }) + return jsonify({"ok": True, "saved": saved})