feat: object label per object + {obj:UUID} sentence placeholders

- Annotate: per-object single label input (M2M via db_objects_db_words), auto-save on blur, remove picture-level word section
- Generate: object chips insert {obj:UUID} at cursor position in question/statement textarea
- Live preview resolves {obj:UUID} → actual object label
- PairsList display also resolves placeholders
- Remove F/A/B word chip system from pair form (replaced by object placeholders)
- Backend: POST /api/directus/db-objects/<id>/words replaces existing word with single label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 13:04:37 +02:00
parent 2595b8d32e
commit 214f8a2019
5 changed files with 252 additions and 456 deletions

58
app.py
View File

@@ -1895,31 +1895,45 @@ def directus_db_object_pairs(obj_id):
return jsonify({"ok": True, "pair_id": pair_id, "statement_id": stmt_id, "question_id": q_id})
@app.route("/api/directus/db-objects/<obj_id>/words", methods=["GET"])
@app.route("/api/directus/db-objects/<obj_id>/words", methods=["GET", "POST"])
def directus_db_object_words(obj_id):
"""Gibt alle db_words zurück, die via db_objects_db_words mit dem Objekt verknüpft sind."""
token = request.headers.get("Authorization", "")
data, s = _directus(
"GET",
f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}"
f"&fields=id,db_words_id.id,db_words_id.titel_de,db_words_id.level,db_words_id.status&limit=500",
token,
)
if s != 200:
return jsonify({"data": []})
items = []
for entry in (data.get("data") or []):
word = entry.get("db_words_id") or {}
if not isinstance(word, dict) or not word.get("id"):
continue
if word.get("status") == "archived":
continue
items.append({
"word_id": word["id"],
"titel_de": word.get("titel_de", ""),
"level": word.get("level") or 50,
})
return jsonify({"data": items})
if request.method == "GET":
data, s = _directus(
"GET",
f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}"
f"&fields=id,db_words_id.id,db_words_id.titel_de,db_words_id.level,db_words_id.status&limit=500",
token,
)
if s != 200:
return jsonify({"data": []})
items = []
for entry in (data.get("data") or []):
word = entry.get("db_words_id") or {}
if not isinstance(word, dict) or not word.get("id"):
continue
if word.get("status") == "archived":
continue
items.append({
"word_id": word["id"],
"titel_de": word.get("titel_de", ""),
"level": word.get("level") or 50,
})
return jsonify({"data": items})
else: # POST — replace with single word
body = request.get_json(force=True, silent=True) or {}
titel_de = (body.get("titel_de") or "").strip()
level = int(body.get("level") or 50)
# Delete all existing junctions for this object
existing, _ = _directus("GET", f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}&fields=id&limit=20", token)
for e in (existing.get("data") or []):
_directus("DELETE", f"/items/db_objects_db_words/{e['id']}", token)
if not titel_de:
return jsonify({"ok": True, "cleared": True})
wid, _ = _find_or_create_db_word(titel_de, level, token)
_directus("POST", "/items/db_objects_db_words", token, {"db_objects_id": obj_id, "db_words_id": wid})
return jsonify({"ok": True, "word_id": wid})
@app.route("/api/directus/db-pairs/<pair_id>", methods=["PATCH", "DELETE"])