API: Workspace aus dem Assistenten in einem Rutsch anlegen
POST /api/v1/workspaces/assistent legt Workspace mit Distro, Version 1, Merkmalen und Webapps aus Vorlagen in einer Transaktion an und lehnt Merkmale ab, die auf der gewaehlten Distro nicht umgesetzt sind (Fehlermeldung nennt sie). Key wird aus dem Namen erzeugt und je Eigentuemer eindeutig gemacht. Workspace-Creator neu, Schritt D. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b361579cc6
commit
857461cc44
1 file changed
+164
@@ -214,6 +214,16 @@ class CreateKategorieRequest(BaseModel):
|
||||
art: Literal["app", "richtlinie"] = "app"
|
||||
|
||||
|
||||
class CreateWorkspaceAssistentRequest(BaseModel):
|
||||
"""Workspace-Creator neu, Schritt D: alles, was der Assistent sammelt."""
|
||||
name: str
|
||||
description: str | None = None
|
||||
organization_id: str | None = None
|
||||
backend_id: str
|
||||
merkmal_ids: list[str] = []
|
||||
webapp_template_ids: list[str] = []
|
||||
|
||||
|
||||
class CreateWorkspaceRequest(BaseModel):
|
||||
key: str
|
||||
name: str
|
||||
@@ -2962,6 +2972,143 @@ def create_workspace(
|
||||
|
||||
return {"id": str(workspace_id), "key": key, "name": name}
|
||||
|
||||
def workspace_key_aus_name(cur, name: str, organization_id: str | None) -> str:
|
||||
"""Key (eindeutig je Eigentümer) aus dem Anzeigenamen, Umlaute auf ihr
|
||||
ASCII-Grundzeichen reduziert; bei Kollision mit -2, -3 … ergänzt."""
|
||||
|
||||
ascii_only = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
|
||||
basis = re.sub(r"[^a-z0-9]+", "-", ascii_only.lower()).strip("-")[:48] or "workspace"
|
||||
cur.execute(
|
||||
"SELECT key FROM workspaces WHERE organization_id IS NOT DISTINCT FROM %s",
|
||||
(organization_id,),
|
||||
)
|
||||
vergeben = {row[0] for row in cur.fetchall()}
|
||||
key, zaehler = basis, 2
|
||||
while key in vergeben:
|
||||
key = f"{basis}-{zaehler}"
|
||||
zaehler += 1
|
||||
return key
|
||||
|
||||
|
||||
def create_workspace_mit_inhalt(
|
||||
name: str, description: str | None, organization_id: str | None, backend_id: str,
|
||||
merkmal_ids: list[str], webapp_template_ids: list[str],
|
||||
):
|
||||
"""
|
||||
Legt einen Workspace aus dem Assistenten in einem Rutsch an (eine
|
||||
Transaktion): Workspace mit Distro, veröffentlichte Version 1, Merkmale,
|
||||
Webapps aus Vorlagen. Lehnt ab, wenn ein Merkmal auf der gewählten Distro
|
||||
nicht umgesetzt ist - ein Workspace soll nichts enthalten, was auf seinen
|
||||
Geräten still fehlt (ADR-0038).
|
||||
|
||||
Rückgabe: (workspace_dict, None) oder (None, (fehlercode, nachricht)).
|
||||
"""
|
||||
|
||||
name = name.strip()
|
||||
if not name:
|
||||
return None, ("invalid_request", "Bitte einen Namen angeben.")
|
||||
|
||||
merkmal_ids = list(dict.fromkeys(merkmal_ids))
|
||||
webapp_template_ids = list(dict.fromkeys(webapp_template_ids))
|
||||
workspace_id, version_id = uuid4(), uuid4()
|
||||
icon_kopien = []
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT name FROM backends WHERE id = %s", (backend_id,))
|
||||
if cur.fetchone() is None:
|
||||
return None, ("backend_not_found", "Die gewählte Distro gibt es nicht.")
|
||||
|
||||
if organization_id is not None:
|
||||
cur.execute("SELECT 1 FROM organizations WHERE id = %s", (organization_id,))
|
||||
if cur.fetchone() is None:
|
||||
return None, ("organization_not_found", "Die gewählte Organisation gibt es nicht.")
|
||||
|
||||
if merkmal_ids:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id::text, m.name, bp.id IS NOT NULL
|
||||
FROM merkmale m
|
||||
LEFT JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
WHERE m.id::text = ANY(%s)
|
||||
""",
|
||||
(backend_id, merkmal_ids),
|
||||
)
|
||||
gefunden = {mid: (mname, umgesetzt) for mid, mname, umgesetzt in cur.fetchall()}
|
||||
if len(gefunden) != len(merkmal_ids):
|
||||
return None, ("merkmal_not_found", "Mindestens ein gewähltes Merkmal gibt es nicht.")
|
||||
fehlend = sorted(mname for mname, umgesetzt in gefunden.values() if not umgesetzt)
|
||||
if fehlend:
|
||||
return None, (
|
||||
"distro_konflikt",
|
||||
"Auf der gewählten Distro nicht umgesetzt: " + ", ".join(fehlend),
|
||||
)
|
||||
|
||||
vorlagen = []
|
||||
if webapp_template_ids:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, url, icon_filename, icon_content_type, kategorie_id
|
||||
FROM webapp_templates WHERE id::text = ANY(%s)
|
||||
""",
|
||||
(webapp_template_ids,),
|
||||
)
|
||||
vorlagen = cur.fetchall()
|
||||
if len(vorlagen) != len(webapp_template_ids):
|
||||
return None, ("template_not_found", "Mindestens eine gewählte Webapp-Vorlage gibt es nicht.")
|
||||
|
||||
key = workspace_key_aus_name(cur, name, organization_id)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspaces (id, organization_id, key, name, description, backend_id, sichtbarkeit, ist_vorlage)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, FALSE)
|
||||
""",
|
||||
(
|
||||
workspace_id, organization_id, key, name, description, backend_id,
|
||||
"global" if organization_id is None else "organisation",
|
||||
),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_versionen (id, workspace_id, nummer, status, notiz, veroeffentlicht_am)
|
||||
VALUES (%s, %s, 1, 'veroeffentlicht', 'Mit dem Assistenten erstellt', now())
|
||||
""",
|
||||
(version_id, workspace_id),
|
||||
)
|
||||
for merkmal_id in merkmal_ids:
|
||||
cur.execute(
|
||||
"INSERT INTO workspace_version_merkmale (workspace_version_id, merkmal_id) VALUES (%s, %s)",
|
||||
(version_id, merkmal_id),
|
||||
)
|
||||
for sort_order, (template_id, tname, url, icon_filename, icon_content_type, kategorie_id) in enumerate(vorlagen):
|
||||
webapp_id = uuid4()
|
||||
kopie = f"{webapp_id}{Path(icon_filename).suffix}" if icon_filename else None
|
||||
if kopie:
|
||||
icon_kopien.append((icon_filename, kopie))
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO workspace_webapps
|
||||
(id, workspace_id, workspace_version_id, name, url, icon_filename, icon_content_type,
|
||||
sort_order, template_id, kategorie_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
webapp_id, workspace_id, version_id, tname, url, kopie, icon_content_type,
|
||||
sort_order, template_id, kategorie_id,
|
||||
),
|
||||
)
|
||||
|
||||
# Eigene Icon-Kopie je Webapp wie bei activate_webapp_template(), damit
|
||||
# Änderungen an der Vorlage die Kopie nicht mitreißen.
|
||||
for quelle, ziel in icon_kopien:
|
||||
quelle_pfad = WEBAPP_ICONS_DIR / quelle
|
||||
if quelle_pfad.is_file():
|
||||
WEBAPP_ICONS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(WEBAPP_ICONS_DIR / ziel).write_bytes(quelle_pfad.read_bytes())
|
||||
|
||||
return {"id": str(workspace_id), "key": key, "name": name}, None
|
||||
|
||||
|
||||
def clone_workspace(source_workspace_id: str, organization_id: str):
|
||||
"""
|
||||
"Diesen Standard übernehmen" (GUI-Umbau Phase 3, Nachtrag 11.09.2026):
|
||||
@@ -4856,6 +5003,23 @@ def list_workspaces(authorization: str | None = Header(default=None)):
|
||||
return {"success": True, "workspaces": fetch_workspaces()}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces/assistent")
|
||||
def create_workspace_assistent_endpoint(
|
||||
payload: CreateWorkspaceAssistentRequest, authorization: str | None = Header(default=None)
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
workspace, fehler = create_workspace_mit_inhalt(
|
||||
payload.name, payload.description, payload.organization_id, payload.backend_id,
|
||||
payload.merkmal_ids, payload.webapp_template_ids,
|
||||
)
|
||||
if fehler:
|
||||
return {"success": False, "error": fehler[0], "message": fehler[1]}
|
||||
|
||||
return {"success": True, "workspace": workspace}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces")
|
||||
def create_workspace_endpoint(payload: CreateWorkspaceRequest, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
|
||||
Reference in new issue
Block a user