refactor: migrate to new db_* Directus collections
- DrawIt: load db_pictures (status=draft), create db_objects/db_words with blurhash placeholder, finish sets status=objects_created - GenerateIt: load db_pictures (status=objects_created), right panel replaced with manual QA pairs (db_pairs + db_question + db_statement) - Backend: new routes for db_pictures, db_objects, db_words, db_pairs - Types/API: full db_* type definitions and API helpers - Directus: user_notes field in db_objects, M2M db_words<->db_pictures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
187
app.py
187
app.py
@@ -261,6 +261,7 @@ def _serve_spa():
|
||||
@app.route("/")
|
||||
@app.route("/draw")
|
||||
@app.route("/generate")
|
||||
@app.route("/annotate")
|
||||
def serve_spa():
|
||||
return _serve_spa()
|
||||
|
||||
@@ -1690,6 +1691,192 @@ def setup_words_pictures():
|
||||
"failed": len(failed), "results": results})
|
||||
|
||||
|
||||
# ── db_* Collection Routes ────────────────────────────────────────────────────
|
||||
|
||||
def _find_or_create_db_word(titel_de: str, level: int, token: str) -> tuple:
|
||||
"""Return (word_id, is_new). Creates db_word with status=draft if missing."""
|
||||
enc = urllib.parse.quote(titel_de, safe="")
|
||||
data, status = _directus("GET", f"/items/db_words?filter[titel_de][_eq]={enc}&fields=id&limit=1", token)
|
||||
if status == 200 and data.get("data"):
|
||||
return data["data"][0]["id"], False
|
||||
body = {"status": "draft", "titel_de": titel_de, "level": level}
|
||||
data, status = _directus("POST", "/items/db_words", token, body)
|
||||
if status in (200, 201):
|
||||
return data["data"]["id"], True
|
||||
raise RuntimeError(f"db_word creation failed ({status}): {data}")
|
||||
|
||||
|
||||
@app.route("/api/directus/db-pictures", methods=["GET"])
|
||||
def directus_db_pictures():
|
||||
token = request.headers.get("Authorization", "")
|
||||
pic_status = request.args.get("status", "draft")
|
||||
data, status = _directus("GET", f"/items/db_pictures?filter[status][_eq]={pic_status}&fields=id,picture,blurhash,status&sort=date_created", token)
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
@app.route("/api/directus/db-pictures/<pic_id>", methods=["PATCH"])
|
||||
def directus_db_picture_patch(pic_id):
|
||||
token = request.headers.get("Authorization", "")
|
||||
data, status = _directus("PATCH", f"/items/db_pictures/{pic_id}", token, body=request.get_json())
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
@app.route("/api/directus/db-objects", methods=["GET", "POST"])
|
||||
def directus_db_objects():
|
||||
token = request.headers.get("Authorization", "")
|
||||
if request.method == "GET":
|
||||
picture_id = request.args.get("picture_id", "")
|
||||
fields = "id,selections,user_notes,status,picture"
|
||||
path = f"/items/db_objects?filter[picture][_eq]={picture_id}&fields={fields}&sort=date_created"
|
||||
data, status = _directus("GET", path, token)
|
||||
return jsonify(data), status
|
||||
else:
|
||||
data, status = _directus("POST", "/items/db_objects", token, body=request.get_json())
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
@app.route("/api/directus/db-objects/<obj_id>", methods=["PATCH", "DELETE"])
|
||||
def directus_db_object(obj_id):
|
||||
token = request.headers.get("Authorization", "")
|
||||
if request.method == "PATCH":
|
||||
data, status = _directus("PATCH", f"/items/db_objects/{obj_id}", token, body=request.get_json())
|
||||
else:
|
||||
data, status = _directus("DELETE", f"/items/db_objects/{obj_id}", token)
|
||||
return jsonify(data), status
|
||||
|
||||
|
||||
@app.route("/api/directus/db-pictures/<pic_id>/words", methods=["GET", "POST"])
|
||||
def directus_db_picture_words(pic_id):
|
||||
token = request.headers.get("Authorization", "")
|
||||
if request.method == "GET":
|
||||
data, s = _directus(
|
||||
"GET",
|
||||
f"/items/db_words_db_pictures?filter[db_pictures_id][_eq]={pic_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({
|
||||
"junction_id": entry.get("id"),
|
||||
"word_id": word["id"],
|
||||
"titel_de": word.get("titel_de", ""),
|
||||
"level": word.get("level") or 50,
|
||||
"status": word.get("status", ""),
|
||||
})
|
||||
return jsonify({"data": items})
|
||||
else:
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
words_to_save = body.get("words", [])
|
||||
existing_data, _ = _directus(
|
||||
"GET",
|
||||
f"/items/db_words_db_pictures?filter[db_pictures_id][_eq]={pic_id}&fields=db_words_id&limit=500",
|
||||
token,
|
||||
)
|
||||
existing_ids = set()
|
||||
for e in (existing_data.get("data") or []):
|
||||
wid = e.get("db_words_id")
|
||||
if wid:
|
||||
existing_ids.add(wid if isinstance(wid, str) else wid.get("id", ""))
|
||||
saved = 0
|
||||
for entry in words_to_save:
|
||||
titel_de = (entry.get("titel_de") or "").strip()
|
||||
level = int(entry.get("level") or 50)
|
||||
if not titel_de:
|
||||
continue
|
||||
try:
|
||||
wid, is_new = _find_or_create_db_word(titel_de, level, token)
|
||||
if not is_new:
|
||||
_directus("PATCH", f"/items/db_words/{wid}", token, {"level": level})
|
||||
if wid not in existing_ids:
|
||||
_directus("POST", "/items/db_words_db_pictures", token, {"db_words_id": wid, "db_pictures_id": pic_id})
|
||||
existing_ids.add(wid)
|
||||
saved += 1
|
||||
except Exception as e:
|
||||
print(f"[db_picture_words] error for '{titel_de}': {e}")
|
||||
return jsonify({"ok": True, "saved": saved})
|
||||
|
||||
|
||||
@app.route("/api/directus/db-objects/<obj_id>/pairs", methods=["GET", "POST"])
|
||||
def directus_db_object_pairs(obj_id):
|
||||
token = request.headers.get("Authorization", "")
|
||||
if request.method == "GET":
|
||||
junc_data, _ = _directus(
|
||||
"GET",
|
||||
f"/items/db_objects_db_pairs?filter[db_objects_id][_eq]={obj_id}&fields=id,db_pairs_id&limit=200",
|
||||
token,
|
||||
)
|
||||
pair_ids = [e["db_pairs_id"] for e in (junc_data.get("data") or []) if e.get("db_pairs_id")]
|
||||
if not pair_ids:
|
||||
return jsonify({"data": []})
|
||||
ids_param = urllib.parse.quote(",".join(pair_ids), safe="")
|
||||
pairs_data, _ = _directus("GET", f"/items/db_pairs?filter[id][_in]={ids_param}&fields=id,status,level&sort=date_created&limit=200", token)
|
||||
pairs = pairs_data.get("data") or []
|
||||
result = []
|
||||
for pair in pairs:
|
||||
pid = pair["id"]
|
||||
q_junc, _ = _directus("GET", f"/items/db_pairs_db_question?filter[db_pairs_id][_eq]={pid}&fields=db_question_id&limit=10", token)
|
||||
q_ids = [e["db_question_id"] for e in (q_junc.get("data") or []) if e.get("db_question_id")]
|
||||
questions = []
|
||||
for qid in q_ids:
|
||||
q_d, _ = _directus("GET", f"/items/db_question/{qid}?fields=id,question_de,level,status", token)
|
||||
if q_d.get("data"):
|
||||
questions.append(q_d["data"])
|
||||
s_junc, _ = _directus("GET", f"/items/db_pairs_db_statement?filter[db_pairs_id][_eq]={pid}&fields=db_statement_id&limit=10", token)
|
||||
s_ids = [e["db_statement_id"] for e in (s_junc.get("data") or []) if e.get("db_statement_id")]
|
||||
statements = []
|
||||
for sid in s_ids:
|
||||
s_d, _ = _directus("GET", f"/items/db_statement/{sid}?fields=id,statement_de,level,status", token)
|
||||
if s_d.get("data"):
|
||||
statements.append(s_d["data"])
|
||||
result.append({**pair, "questions": questions, "statements": statements})
|
||||
return jsonify({"data": result})
|
||||
else:
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
question_de = (body.get("question_de") or "").strip()
|
||||
statement_de = (body.get("statement_de") or "").strip()
|
||||
level = int(body.get("level") or 1)
|
||||
words = body.get("words", [])
|
||||
if not statement_de:
|
||||
return jsonify({"error": "statement_de is required"}), 400
|
||||
pair_resp, s = _directus("POST", "/items/db_pairs", token, {"status": "draft", "level": level})
|
||||
if s not in (200, 201):
|
||||
return jsonify({"error": "Failed to create pair"}), 500
|
||||
pair_id = pair_resp["data"]["id"]
|
||||
_directus("POST", "/items/db_objects_db_pairs", token, {"db_objects_id": obj_id, "db_pairs_id": pair_id})
|
||||
stmt_resp, s = _directus("POST", "/items/db_statement", token, {"status": "draft", "statement_de": statement_de, "level": level})
|
||||
if s not in (200, 201):
|
||||
return jsonify({"error": "Failed to create statement"}), 500
|
||||
stmt_id = stmt_resp["data"]["id"]
|
||||
_directus("POST", "/items/db_pairs_db_statement", token, {"db_pairs_id": pair_id, "db_statement_id": stmt_id})
|
||||
q_id = None
|
||||
if question_de:
|
||||
q_resp, s = _directus("POST", "/items/db_question", token, {"status": "draft", "question_de": question_de, "level": level})
|
||||
if s in (200, 201):
|
||||
q_id = q_resp["data"]["id"]
|
||||
_directus("POST", "/items/db_pairs_db_question", token, {"db_pairs_id": pair_id, "db_question_id": q_id})
|
||||
for we in words:
|
||||
titel_de = (we.get("titel_de") or "").strip()
|
||||
w_level = int(we.get("level") or level)
|
||||
if not titel_de:
|
||||
continue
|
||||
try:
|
||||
wid, _ = _find_or_create_db_word(titel_de, w_level, token)
|
||||
_directus("POST", "/items/db_statement_db_words", token, {"db_statement_id": stmt_id, "db_words_id": wid})
|
||||
if q_id:
|
||||
_directus("POST", "/items/db_question_db_words", token, {"db_question_id": q_id, "db_words_id": wid})
|
||||
except Exception as e:
|
||||
print(f"[db_object_pairs] word error '{titel_de}': {e}")
|
||||
return jsonify({"ok": True, "pair_id": pair_id, "statement_id": stmt_id, "question_id": q_id})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user