refactor(words): words_pictures auf natives Directus M2M umgestellt
- 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 <noreply@anthropic.com>
This commit is contained in:
131
app.py
131
app.py
@@ -113,74 +113,96 @@ def directus_object(obj_id):
|
|||||||
return jsonify(data), status
|
return jsonify(data), status
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/directus/pictures/<pic_id>/words", methods=["GET", "POST"])
|
def _setup_words_pictures(token: str):
|
||||||
def directus_picture_words(pic_id):
|
"""Richtet M2M-Relation words ↔ pictures idempotent ein (läuft einmalig)."""
|
||||||
"""Proxy: Safe-Words eines Bildes laden (GET) oder speichern (POST)."""
|
|
||||||
token = request.headers.get("Authorization", "")
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
junc, _ = _directus(
|
|
||||||
"GET",
|
|
||||||
f"/items/words_pictures?filter[pictures_id][_eq]={pic_id}&fields=id,words_id&limit=500",
|
|
||||||
token,
|
|
||||||
)
|
|
||||||
w_ids = [e["words_id"] for e in (junc.get("data") or []) if e.get("words_id")]
|
|
||||||
if not w_ids:
|
|
||||||
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 [])
|
|
||||||
]
|
|
||||||
return jsonify({"data": items})
|
|
||||||
|
|
||||||
else: # POST
|
|
||||||
body = request.get_json(force=True, silent=True) or {}
|
|
||||||
words = body.get("words", [])
|
|
||||||
|
|
||||||
# Junction + Relationen idempotent einrichten
|
|
||||||
_ensure_junction("words_pictures", "words_id", "pictures_id", token)
|
|
||||||
_directus("PATCH", "/fields/pictures/linked_words", token, {
|
_directus("PATCH", "/fields/pictures/linked_words", token, {
|
||||||
"type": "alias",
|
"type": "alias",
|
||||||
"meta": {"special": ["m2m"], "interface": "list-m2m",
|
"meta": {"special": ["m2m"], "interface": "list-m2m",
|
||||||
"options": {"template": "{{words_id.title_de}}"}},
|
"options": {"template": "{{words_id.title_de}}"},
|
||||||
|
"note": "Verknüpfte Safe Words"},
|
||||||
})
|
})
|
||||||
_directus("PATCH", "/fields/words/linked_pictures", token, {
|
_directus("PATCH", "/fields/words/linked_pictures", token, {
|
||||||
"type": "alias",
|
"type": "alias",
|
||||||
"meta": {"special": ["m2m"], "interface": "list-m2m",
|
"meta": {"special": ["m2m"], "interface": "list-m2m",
|
||||||
"options": {"template": "{{pictures_id.media}}"}},
|
"options": {"template": "{{pictures_id.media}}"},
|
||||||
|
"note": "Verknüpfte Bilder"},
|
||||||
})
|
})
|
||||||
_directus("POST", "/relations", token, {
|
_directus("POST", "/relations", token, {
|
||||||
"collection": "words_pictures", "field": "words_id",
|
"collection": "words_pictures", "field": "words_id",
|
||||||
"related_collection": "words",
|
"related_collection": "words",
|
||||||
"schema": {"on_delete": "CASCADE"},
|
"schema": {"on_delete": "CASCADE"},
|
||||||
"meta": {"junction_field": "pictures_id",
|
"meta": {"junction_field": "pictures_id", "one_field": "linked_pictures",
|
||||||
"one_field": "linked_pictures",
|
|
||||||
"one_deselect_action": "nullify"},
|
"one_deselect_action": "nullify"},
|
||||||
})
|
})
|
||||||
_directus("POST", "/relations", token, {
|
_directus("POST", "/relations", token, {
|
||||||
"collection": "words_pictures", "field": "pictures_id",
|
"collection": "words_pictures", "field": "pictures_id",
|
||||||
"related_collection": "pictures",
|
"related_collection": "pictures",
|
||||||
"schema": {"on_delete": "CASCADE"},
|
"schema": {"on_delete": "CASCADE"},
|
||||||
"meta": {"junction_field": "words_id",
|
"meta": {"junction_field": "words_id", "one_field": "linked_words",
|
||||||
"one_field": "linked_words",
|
|
||||||
"one_deselect_action": "nullify"},
|
"one_deselect_action": "nullify"},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/directus/pictures/<pic_id>/words", methods=["GET", "POST"])
|
||||||
|
def directus_picture_words(pic_id):
|
||||||
|
"""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":
|
||||||
|
# Natives Directus Deep-Query über M2M-Relation
|
||||||
|
data, status = _directus(
|
||||||
|
"GET",
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if status != 200:
|
||||||
|
return jsonify({"data": []})
|
||||||
|
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_to_save = body.get("words", [])
|
||||||
|
|
||||||
|
# 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
|
saved = 0
|
||||||
for entry in words:
|
for entry in words_to_save:
|
||||||
title_de = (entry.get("title_de") or "").strip()
|
title_de = (entry.get("title_de") or "").strip()
|
||||||
level = int(entry.get("level") or 50)
|
level = int(entry.get("level") or 50)
|
||||||
if not title_de:
|
if not title_de:
|
||||||
@@ -188,17 +210,20 @@ def directus_picture_words(pic_id):
|
|||||||
try:
|
try:
|
||||||
wid, is_new = _find_or_create_word(title_de, level, token)
|
wid, is_new = _find_or_create_word(title_de, level, token)
|
||||||
if not is_new:
|
if not is_new:
|
||||||
# Wort existiert bereits → Level aktualisieren
|
|
||||||
_directus("PATCH", f"/items/words/{wid}", token, {"level": level})
|
_directus("PATCH", f"/items/words/{wid}", token, {"level": level})
|
||||||
_ensure_link(
|
if wid not in existing_ids:
|
||||||
"words_pictures",
|
new_links.append({"words_id": wid})
|
||||||
{"words_id": wid, "pictures_id": pic_id},
|
existing_ids.add(wid)
|
||||||
{"words_id": wid, "pictures_id": pic_id},
|
|
||||||
token,
|
|
||||||
)
|
|
||||||
saved += 1
|
saved += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[picture_words] error for '{title_de}': {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})
|
return jsonify({"ok": True, "saved": saved})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user