Objekte direkt in Directus speichern + neuer Annotationsworkflow

- DirectusObject Typ + CanvasObject Interface in types.ts
- DrawCanvas nutzt CanvasObject (generisch, nicht mehr ObjectMeta-gebunden)
- Flask: /api/directus/objects (GET/POST), /api/directus/objects/<id> (PATCH/DELETE)
- Flask: /api/directus/setup-m2m (einmalig: m2m für categories/questions)
- api.ts: getDirectusObjects, createDirectusObject, updateDirectusObject, deleteDirectusObject
- DrawIt: Objekte werden in Directus gespeichert (mit picture, bbox/polygon, user_notes, parent)
- DrawIt: Linke Sidebar zeigt Objektliste mit Notizen-Editor und Löschen-Button
- DrawIt: Rechte Sidebar: Modus, user_notes Textarea, Parent-Dropdown, Auswahlen
- Directus: user_notes Feld (textarea), action/resolution/confidence/media versteckt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 20:26:46 +02:00
parent 01812ce954
commit 343d6a2389
5 changed files with 395 additions and 213 deletions

142
app.py
View File

@@ -38,44 +38,126 @@ def read_prompt(filepath: Path, fallback: str) -> str:
return fallback.strip()
def _directus(method, path, token, body=None):
"""Hilfsfunktion: Directus-API-Aufruf via urllib."""
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = token
req = urllib.request.Request(
f"{DIRECTUS_URL}{path}",
data=json.dumps(body).encode() if body is not None else None,
headers=headers,
method=method,
)
try:
with urllib.request.urlopen(req) as resp:
raw = resp.read().decode("utf-8")
return json.loads(raw) if raw else {}, resp.status
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8")
return json.loads(raw) if raw else {}, e.code
@app.route("/api/directus/auth/login", methods=["POST"])
def directus_auth_login():
"""Proxy: Directus-Login ohne CORS-Probleme."""
try:
body = json.dumps(request.get_json()).encode("utf-8")
req = urllib.request.Request(
f"{DIRECTUS_URL}/auth/login",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode("utf-8"))
return jsonify(data)
except urllib.error.HTTPError as e:
data = json.loads(e.read().decode("utf-8"))
return jsonify(data), e.code
except Exception as e:
return jsonify({"errors": [{"message": str(e)}]}), 500
data, status = _directus("POST", "/auth/login", token=None, body=request.get_json())
return jsonify(data), status
@app.route("/api/directus/pictures", methods=["GET"])
def directus_pictures():
"""Proxy: Directus-Bilder (status=new) ohne CORS-Probleme."""
"""Proxy: Directus-Bilder (status=new)."""
token = request.headers.get("Authorization", "")
try:
req = urllib.request.Request(
f"{DIRECTUS_URL}/items/pictures?filter[status][_eq]=new&fields=id,media,status&sort=date_created",
headers={"Authorization": token},
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode("utf-8"))
return jsonify(data)
except urllib.error.HTTPError as e:
data = json.loads(e.read().decode("utf-8"))
return jsonify(data), e.code
except Exception as e:
return jsonify({"errors": [{"message": str(e)}]}), 500
data, status = _directus("GET", "/items/pictures?filter[status][_eq]=new&fields=id,media,status&sort=date_created", token)
return jsonify(data), status
@app.route("/api/directus/objects", methods=["GET", "POST"])
def directus_objects():
"""Proxy: Objekte laden (GET) oder anlegen (POST)."""
token = request.headers.get("Authorization", "")
if request.method == "GET":
picture_id = request.args.get("picture_id", "")
fields = "id,bbox,polygon,user_notes,parent,status,picture"
path = f"/items/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/objects", token, body=request.get_json())
return jsonify(data), status
@app.route("/api/directus/objects/<obj_id>", methods=["PATCH", "DELETE"])
def directus_object(obj_id):
"""Proxy: Objekt aktualisieren (PATCH) oder löschen (DELETE)."""
token = request.headers.get("Authorization", "")
if request.method == "PATCH":
data, status = _directus("PATCH", f"/items/objects/{obj_id}", token, body=request.get_json())
else:
data, status = _directus("DELETE", f"/items/objects/{obj_id}", token)
return jsonify(data), status
@app.route("/api/directus/setup-m2m", methods=["POST"])
def directus_setup_m2m():
"""Einmalig: m2m-Relationen für categories und questions auf objects anlegen."""
token = request.headers.get("Authorization", "")
results = []
for rel_name, related_table, related_fk in [
("categories", "categories", "categories_id"),
("questions", "questions", "questions_id"),
]:
junction = f"objects_{rel_name}"
# 1. Altes m2o-Feld entfernen
d, s = _directus("DELETE", f"/fields/objects/{rel_name}", token)
results.append({"step": f"delete_m2o_{rel_name}", "status": s})
# 2. Junction-Collection anlegen
d, s = _directus("POST", "/collections", token, {
"collection": junction,
"meta": {"hidden": True, "icon": "import_export"},
"schema": {},
})
results.append({"step": f"create_junction_{junction}", "status": s})
# 3. Felder der Junction
for field_def in [
{"field": "id", "type": "integer", "schema": {"has_auto_increment": True, "is_primary_key": True, "is_nullable": False}, "meta": {"hidden": True}},
{"field": "objects_id","type": "uuid", "schema": {"foreign_key_table": "objects", "foreign_key_column": "id", "is_nullable": False}, "meta": {"hidden": True}},
{"field": related_fk, "type": "uuid", "schema": {"foreign_key_table": related_table, "foreign_key_column": "id", "is_nullable": False}, "meta": {"hidden": True}},
]:
d, s = _directus("POST", f"/fields/{junction}", token, field_def)
results.append({"step": f"field_{junction}_{field_def['field']}", "status": s})
# 4. Relation junction.objects_id → objects (mit back-reference)
d, s = _directus("POST", "/relations", token, {
"collection": junction, "field": "objects_id",
"related_collection": "objects",
"meta": {"one_field": rel_name, "junction_field": related_fk, "sort_field": None},
"schema": {"on_delete": "CASCADE"},
})
results.append({"step": f"relation_{junction}_objects", "status": s})
# 5. Relation junction.related_fk → related_table
d, s = _directus("POST", "/relations", token, {
"collection": junction, "field": related_fk,
"related_collection": related_table,
"schema": {"on_delete": "CASCADE"},
})
results.append({"step": f"relation_{junction}_{rel_name}", "status": s})
# 6. Alias-Feld auf objects (m2m)
d, s = _directus("POST", "/fields/objects", token, {
"field": rel_name, "type": "alias",
"meta": {"interface": "list-m2m", "special": ["m2m"], "hidden": False, "width": "full"},
"schema": None,
})
results.append({"step": f"alias_{rel_name}", "status": s})
return jsonify({"results": results})
@app.route("/api/images", methods=["GET"])