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:
142
app.py
142
app.py
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user