Files
kundenplattform/routers/admin_workspaces.py
T
adminandClaude Opus 5.5 3613331e8c Einstieg Schritt 5: Workspace-Assistent mit Distribution zuerst und Standort/Zweck
Assistent: 1 Distribution (Kurzcharakteristik, für Kunden Hinweis ohne
eingerichtete Installation, Debian vorerst nur Admin), 2 Name und
Standort/Zweck (bestehend, neu oder später; Namensvorschlag), 3-5 nur
Einträge der gewählten Distro (Wechsel wählt ab, mit Hinweis), 6 Prüfen.
Speichern weist im Kundenportal gleich zu, Fehler der Zuweisung stehen auf
der Workspace-Seite. Distro-Leiste und Konfliktkarten entfallen; behebt
nebenbei den Absturz der Zusammenfassung im Kundenportal (Eigentümerfeld
gibt es nur im Admin). Versionsliste und Version festhalten unter
Erweitert; Link "neuen Workspace für … erstellen" bei jedem Standort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:10:56 +01:00

566 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import RedirectResponse
from anode_client import anode_request
from auth import require_admin
from templating import templates
# Workspace-Creator neu (Konzept 07.10.2026, siehe
# platform-docs/roadmap/workspace-creator-neu.md): eigener Hauptbereich
# "Workspaces" für root mit den Reitern Katalog, Workspaces und Vorlagen.
# Schritt A: Katalog (nur lesend), ersetzt die frühere "Flottenmatrix".
router = APIRouter(prefix="/admin/workspaces")
def _liste(result: dict, schluessel: str) -> list[dict]:
return result.get(schluessel, []) if result.get("success") else []
def _eintrag(
name: str, description: str | None, icon_url: str | None, distros: list[dict], eintrag_id: str | None = None,
pflege: dict | None = None,
) -> dict:
# "pflege": was die Katalogseite zum Bearbeiten braucht (Art, Key,
# Kategorie, ...); der Assistent und die Workspace-Seite ignorieren es.
return {
"id": eintrag_id, "name": name, "description": description, "icon_url": icon_url, "distros": distros,
"distro_keys": [d["key"] for d in distros if d["verfuegbar"]],
"pflege": pflege or {},
}
def _nach_kategorie(kategorien: list[dict], eintraege_mit_kategorie: list[tuple[str | None, dict]]) -> list[dict]:
"""Gruppiert (kategorie_id, eintrag)-Paare in der Reihenfolge der
Kategorien (sort_order); leere Kategorien entfallen, Einträge ohne
Kategorie landen am Ende unter "Ohne Kategorie"."""
nach_id: dict[str | None, list[dict]] = {}
for kategorie_id, eintrag in eintraege_mit_kategorie:
nach_id.setdefault(kategorie_id, []).append(eintrag)
gruppen = [
{"name": k["name"], "description": k.get("description"), "eintraege": sorted(nach_id[k["id"]], key=lambda e: e["name"].lower())}
for k in kategorien
if k["id"] in nach_id
]
bekannte_ids = {k["id"] for k in kategorien}
ohne = [e for kid, eintraege in nach_id.items() if kid not in bekannte_ids for e in eintraege]
if ohne:
gruppen.append({"name": "Ohne Kategorie", "description": None, "eintraege": sorted(ohne, key=lambda e: e["name"].lower())})
return gruppen
def katalog_aufbauen(
kategorien: list[dict], merkmale: list[dict], webapp_templates: list[dict],
backends: list[dict], blueprints: list[dict], icon_basis: str = "/admin/workspaces",
) -> list[dict]:
"""
Drei Bereiche (Apps, Webapps, Konfigurationsrichtlinien), darin
Kategorien, darin Einträge mit Distro-Verfügbarkeit. Ob ein Merkmal auf
einer Distro geht, ergibt sich wie bei der früheren Flottenmatrix aus
den Blueprints (Merkmal × Distro → Ansible-Rolle). Webapps laufen im
Browser und sind damit auf jeder Distro verfügbar. icon_basis ist der
Pfad der Icon-Proxys (Admin oder Kundenportal, seit 23.10.2026).
"""
# Rolle je (Merkmal, Distro) - die Katalogseite zeigt sie im Tooltip
# des Distro-Kennzeichens (früher eigene Seite "Distro-Abdeckung").
rollen_je_merkmal: dict[str, dict[str, str]] = {}
for bp in blueprints:
rollen_je_merkmal.setdefault(bp["merkmal"], {})[bp["backend"]] = bp.get("ansible_role")
def distro_liste(verfuegbar, rollen: dict[str, str] | None = None) -> list[dict]:
rollen = rollen or {}
return [
{"key": b["key"], "name": b["name"], "version": b.get("version"), "verfuegbar": b["key"] in verfuegbar,
"rolle": rollen.get(b["key"])}
for b in backends
]
art_je_kategorie = {k["id"]: k.get("art", "app") for k in kategorien}
app_kategorien = [k for k in kategorien if k.get("art", "app") == "app"]
richtlinien_kategorien = [k for k in kategorien if k.get("art") == "richtlinie"]
apps, richtlinien = [], []
for m in merkmale:
rollen = rollen_je_merkmal.get(m["key"], {})
eintrag = _eintrag(
m["name"], m.get("description"),
f"{icon_basis}{m['icon_path']}" if m.get("icon_path") else None,
distro_liste(set(rollen), rollen),
m["id"],
{"art": "merkmal", "key": m["key"], "kategorie_id": m.get("kategorie_id"),
"im_auftragskatalog": bool(m.get("im_auftragskatalog"))},
)
ziel = richtlinien if art_je_kategorie.get(m.get("kategorie_id")) == "richtlinie" else apps
ziel.append((m.get("kategorie_id"), eintrag))
alle_distros = {b["key"] for b in backends}
webapps = [
(
t.get("kategorie_id"),
_eintrag(
t["name"], t.get("description"),
f"{icon_basis}/webapp-icons/{t['icon_path'].split('/')[-1]}" if t.get("icon_path") else None,
distro_liste(alle_distros),
t["id"],
{"art": "webapp", "url": t.get("url"), "kategorie_id": t.get("kategorie_id")},
),
)
for t in webapp_templates
]
bereiche = [
("apps", "Apps", "📦", app_kategorien, apps),
("webapps", "Webapps", "🌐", app_kategorien, webapps),
("richtlinien", "Konfigurationsrichtlinien", "🛡️", richtlinien_kategorien, richtlinien),
]
return [
{
"key": key, "name": name, "symbol": symbol, "anzahl": len(eintraege),
"kategorien": _nach_kategorie(kats, eintraege),
}
for key, name, symbol, kats, eintraege in bereiche
]
@router.get("")
def index(user: dict = Depends(require_admin)):
return RedirectResponse("/admin/workspaces/katalog", status_code=303)
@router.get("/vorlagen")
def vorlagen(user: dict = Depends(require_admin)):
return RedirectResponse("/admin/workspaces/katalog", status_code=303)
# Bereich "Datenbank" (/admin/technik) ist in "Workspaces" aufgegangen
# (Schritt B, 07.10.2026) - alte Lesezeichen und Links leiten weiter.
# Bewusst 307/308 statt 303 für POST, damit ein noch offenes altes Formular
# nicht stillschweigend zu einem GET wird.
ALTE_TECHNIK_PFADE = [
("webapp-templates", "vorlagen/webapp-vorlagen"),
("webapp-icons", "webapp-icons"),
("merkmal-icons", "merkmal-icons"),
("merkmale", "vorlagen/merkmale"),
("kategorien", "vorlagen/kategorien"),
("backends", "vorlagen/distros"),
("workspaces", "liste"),
]
def neuer_pfad_fuer_technik(rest: str) -> str:
rest = rest.strip("/")
for alt, neu in ALTE_TECHNIK_PFADE:
if rest == alt or rest.startswith(alt + "/"):
return "/admin/workspaces/" + neu + rest[len(alt):]
return "/admin/workspaces"
technik_weiterleitung = APIRouter(prefix="/admin/technik")
@technik_weiterleitung.api_route("", methods=["GET", "POST"])
@technik_weiterleitung.api_route("/{rest:path}", methods=["GET", "POST"])
def alte_technik_adresse(request: Request, rest: str = ""):
status = 303 if request.method == "GET" else 307
return RedirectResponse(neuer_pfad_fuer_technik(rest), status_code=status)
def katalog_laden(mit_kategorien: bool = False, icon_basis: str = "/admin/workspaces") -> tuple:
"""(bereiche, backends) - gemeinsame Datengrundlage für Katalog und
Assistent; mit_kategorien hängt die Kategorienliste an (für die
Bearbeiten-Formulare der Katalogseite)."""
kategorien = _liste(anode_request("GET", "/api/v1/kategorien"), "kategorien")
merkmale = _liste(anode_request("GET", "/api/v1/merkmale"), "merkmale")
webapp_templates = _liste(anode_request("GET", "/api/v1/webapp-templates"), "webapp_templates")
backends = _liste(anode_request("GET", "/api/v1/backends"), "backends")
blueprints = _liste(anode_request("GET", "/api/v1/blueprints"), "blueprints")
bereiche = katalog_aufbauen(kategorien, merkmale, webapp_templates, backends, blueprints, icon_basis)
if mit_kategorien:
return bereiche, backends, kategorien
return bereiche, backends
# GUI-Runde 22.10.2026: "Übersicht", "Merkmale bearbeiten" und
# "Distro-Abdeckung" sind in dieser einen Seite aufgegangen, am 23.10.2026
# auch "Kategorien" und "Webapp-Vorlagen" (aufklappbar unter den Einträgen).
@router.get("/katalog")
def katalog(request: Request, user: dict = Depends(require_admin)):
bereiche, backends, kategorien = katalog_laden(mit_kategorien=True)
# Für den Abschnitt Webapp-Vorlagen (GUI-Runde 23.10.2026).
webapp_templates = _liste(anode_request("GET", "/api/v1/webapp-templates"), "webapp_templates")
return templates.TemplateResponse(
request, "admin/workspaces_katalog.html",
{
"user": user, "bereiche": bereiche, "backends": backends, "kategorien": kategorien,
"app_kategorien": [k for k in kategorien if k.get("art", "app") == "app"],
"webapp_templates": webapp_templates,
},
)
# --- Assistent "Workspace erstellen" (Schritt D, 07.10.2026) ---
# Einstieg vereinfachen, Schritt 5 (25.10.2026): Kurzcharakteristik je
# Distribution als Entscheidungshilfe (B15). "nur_admin": für Kunden noch
# nicht wählbar (Debian-Image ohne Desktop, siehe Memory desktop-je-distro).
DISTRO_INFO = {
"mint-image": {
"text": "Einfacher Umstieg von Windows: Startmenü und Taskleiste wie gewohnt. Tuxflotte-Standard für Schulen.",
},
"ubuntu-image": {
"text": "Wenn ein Anschluss an ein Active Directory geplant ist. Bedienung etwas anders als Windows; persönliche Konten (Personalisierung) gibt es hier noch nicht.",
},
"debian-image": {
"text": "Für ältere, schwache Geräte: schlank und sehr stabil.",
"nur_admin": "Noch in Vorbereitung: derzeit ohne grafische Oberfläche.",
},
}
def assistent_seite(
request: Request, user, werte: dict, fehler: str | None = None, *,
ist_admin: bool = True, assistent_ziel: str = "/admin/workspaces/neu", icon_basis: str = "/admin/workspaces",
standorte: list[dict] | None = None, eingerichtete_backend_ids: set[str] | None = None,
):
"""Assistent für Admin und (seit 23.10.2026) Kundenportal - der Kunde
wählt keinen Eigentümer, der Workspace gehört immer seiner Organisation.
Seit Schritt 5 (25.10.2026) wählt der Kunde zuerst die Distribution und
am Ende einen Standort/Zweck (standorte); eingerichtete_backend_ids
sagt, für welche Distros seine Organisation schon installieren kann."""
bereiche, backends = katalog_laden(icon_basis=icon_basis)
organisationen = _liste(anode_request("GET", "/api/v1/organizations"), "organizations") if ist_admin else []
backends = [
{
**b,
"info": DISTRO_INFO.get(b["key"], {}).get("text"),
"gesperrt": None if ist_admin else DISTRO_INFO.get(b["key"], {}).get("nur_admin"),
"eingerichtet": None if eingerichtete_backend_ids is None else b["id"] in eingerichtete_backend_ids,
}
for b in backends
]
return templates.TemplateResponse(
request,
"admin/workspace_assistent.html" if ist_admin else "workspace_assistent.html",
{
"user": user,
"ist_admin": ist_admin,
"assistent_ziel": assistent_ziel,
"bereiche": bereiche,
"backends": backends,
"organisationen": organisationen,
"standorte": standorte,
"werte": werte,
"fehler": fehler,
},
status_code=400 if fehler else 200,
)
@router.get("/neu")
def assistent(request: Request, user: dict = Depends(require_admin)):
return assistent_seite(request, user, {"merkmal_ids": [], "webapp_template_ids": []})
@router.post("/neu")
async def assistent_speichern(request: Request, user: dict = Depends(require_admin)):
form = await request.form()
werte = {
"name": (form.get("name") or "").strip(),
"description": (form.get("description") or "").strip(),
"organization_id": form.get("organization_id") or "",
"backend_id": form.get("backend_id") or "",
"merkmal_ids": form.getlist("merkmal_ids"),
"webapp_template_ids": form.getlist("webapp_template_ids"),
}
if not werte["name"]:
return assistent_seite(request, user, werte, "Bitte einen Namen angeben.")
if not werte["backend_id"]:
return assistent_seite(request, user, werte, "Bitte eine Distro wählen.")
result = anode_request(
"POST", "/api/v1/workspaces/assistent",
json={
"name": werte["name"],
"description": werte["description"] or None,
"organization_id": werte["organization_id"] or None,
"backend_id": werte["backend_id"],
"merkmal_ids": werte["merkmal_ids"],
"webapp_template_ids": werte["webapp_template_ids"],
},
)
if not result.get("success"):
return assistent_seite(request, user, werte, result.get("message", "Workspace konnte nicht angelegt werden."))
return RedirectResponse(f"/admin/workspaces/liste/{result['workspace']['id']}", status_code=303)
# --- Workspace-Detail und -Verwaltung (Schritt E, 07.10.2026) ---
#
# Inhalte ändern sich nur im Entwurf (Server erzwingt das). Die Seite zeigt
# die veröffentlichte Version nur lesend, im Entwurf einen Editor wie im
# Assistenten plus Wirkungsvorschau und Veröffentlichen.
def _zurueck(workspace_id: str, **meldung) -> RedirectResponse:
ziel = f"/admin/workspaces/liste/{workspace_id}"
if meldung:
ziel += "?" + urlencode(meldung)
return RedirectResponse(ziel, status_code=303)
def _ergebnis(workspace_id: str, result: dict, erfolg: str | None = None) -> RedirectResponse:
if not result.get("success"):
return _zurueck(workspace_id, fehler=result.get("message", "Das hat nicht geklappt."))
return _zurueck(workspace_id, hinweis=erfolg) if erfolg else _zurueck(workspace_id)
def detail_kontext(
request: Request, workspace_id: str, workspace: dict, *, ist_admin: bool, darf_bearbeiten: bool, icon_basis: str,
) -> dict:
"""Alles, was templates/_workspace_detail.html braucht - für Admin und
(seit 23.10.2026) Kundenportal. Ohne Bearbeitungsrecht (fremde Vorlage)
keine Nutzung und keine Wirkung: die nennen andere Organisationen."""
ist_entwurf = darf_bearbeiten and bool(workspace.get("version") and workspace["version"]["status"] == "entwurf")
bereiche, backends = katalog_laden(icon_basis=icon_basis)
ausgewaehlt = {m["id"] for m in workspace["merkmale"]}
distro_key = next((b["key"] for b in backends if workspace["backend"] and b["id"] == workspace["backend"]["id"]), None)
webapps = _liste(anode_request("GET", f"/api/v1/workspaces/{workspace_id}/webapps"), "webapps")
vorlagen_verfuegbar = (
_liste(anode_request("GET", f"/api/v1/workspaces/{workspace_id}/webapp-templates/available"), "webapp_templates")
if ist_entwurf else []
)
wirkung = None
if ist_entwurf:
w = anode_request("GET", f"/api/v1/workspaces/{workspace_id}/wirkung")
wirkung = w.get("wirkung") if w.get("success") else None
nutzung = {}
if darf_bearbeiten:
nutzung = (wirkung or {}).get("nutzung") or anode_request(
"GET", f"/api/v1/workspaces/{workspace_id}/nutzung"
).get("nutzung", {})
# Kopie-Prüfung (zweistufig: erst prüfen, dann anlegen)
kopie = None
if request.query_params.get("kopie_backend"):
k = anode_request(
"GET", f"/api/v1/workspaces/{workspace_id}/kopie-pruefung",
params={"backend_id": request.query_params["kopie_backend"]},
)
kopie = {
"name": request.query_params.get("kopie_name", ""),
"backend_id": request.query_params["kopie_backend"],
"organization_id": request.query_params.get("kopie_org", "") if ist_admin else "",
"weggefallen": k.get("weggefallen", []) if k.get("success") else [],
}
return {
"workspace": workspace,
"workspace_id": workspace_id,
"ist_entwurf": ist_entwurf,
"bereiche": [b for b in bereiche if b["key"] != "webapps"],
"ausgewaehlt": ausgewaehlt,
"distro_key": distro_key,
"webapps": webapps,
"vorlagen_verfuegbar": vorlagen_verfuegbar,
"wirkung": wirkung,
"nutzung": nutzung,
"versionen": _liste(anode_request("GET", f"/api/v1/workspaces/{workspace_id}/versionen"), "versionen"),
"backends": backends,
"kundengruppen": _liste(anode_request("GET", "/api/v1/kundengruppen"), "kundengruppen") if ist_admin else [],
"organisationen": _liste(anode_request("GET", "/api/v1/organizations"), "organizations") if ist_admin else [],
"kopie": kopie,
"fehler": request.query_params.get("fehler"),
"hinweis": request.query_params.get("hinweis"),
"ist_admin": ist_admin,
"darf_bearbeiten": darf_bearbeiten,
"icon_basis": icon_basis,
}
@router.get("/liste/{workspace_id}")
def workspace_detail(request: Request, workspace_id: str, user: dict = Depends(require_admin)):
basis = {"user": user, "ws_basis": "/admin/workspaces/liste", "ws_liste_url": "/admin/workspaces/liste"}
result = anode_request("GET", f"/api/v1/workspaces/{workspace_id}")
workspace = result.get("workspace") if result.get("success") else None
if workspace is None:
return templates.TemplateResponse(
request, "admin/workspace_detail.html", {**basis, "workspace": None}, status_code=404
)
return templates.TemplateResponse(
request, "admin/workspace_detail.html",
{**basis, **detail_kontext(request, workspace_id, workspace, ist_admin=True, darf_bearbeiten=True, icon_basis="/admin/workspaces")},
)
@router.post("/liste/{workspace_id}/entwurf")
def entwurf_anlegen(workspace_id: str, user: dict = Depends(require_admin)):
return _ergebnis(workspace_id, anode_request("POST", f"/api/v1/workspaces/{workspace_id}/entwurf"))
@router.post("/liste/{workspace_id}/entwurf/verwerfen")
def entwurf_verwerfen(workspace_id: str, user: dict = Depends(require_admin)):
return _ergebnis(
workspace_id, anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/entwurf"), "Entwurf verworfen."
)
@router.post("/liste/{workspace_id}/veroeffentlichen")
def veroeffentlichen(workspace_id: str, notiz: str = Form(default=""), user: dict = Depends(require_admin)):
result = anode_request(
"POST", f"/api/v1/workspaces/{workspace_id}/veroeffentlichen", json={"notiz": notiz.strip() or None}
)
return _ergebnis(workspace_id, result, f"Version {result.get('nummer')} veröffentlicht.")
@router.post("/liste/{workspace_id}/merkmale")
async def workspace_merkmale_speichern(request: Request, workspace_id: str, user: dict = Depends(require_admin)):
"""Diff-basiertes Sammel-Speichern in den Entwurf (die Einzel-Endpunkte
auf anode lehnen ab, wenn es keinen Entwurf gibt oder ein Merkmal auf
der Distro fehlt - die erste Ablehnung wird angezeigt)."""
form = await request.form()
fehler = merkmale_diff_speichern(workspace_id, set(form.getlist("merkmal_ids")))
if fehler:
return _ergebnis(workspace_id, fehler)
return _zurueck(workspace_id, hinweis="Entwurf gespeichert.")
def merkmale_diff_speichern(workspace_id: str, ausgewaehlt: set[str]) -> dict | None:
"""Gewählte Merkmale in den Entwurf übernehmen; liefert die erste
Ablehnung von anode oder None."""
result = anode_request("GET", f"/api/v1/workspaces/{workspace_id}")
workspace = result.get("workspace") if result.get("success") else None
aktuell = {m["id"] for m in workspace["merkmale"]} if workspace else set()
for merkmal_id in ausgewaehlt - aktuell:
r = anode_request("POST", f"/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
if not r.get("success"):
return r
for merkmal_id in aktuell - ausgewaehlt:
r = anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
if not r.get("success"):
return r
return None
@router.post("/liste/{workspace_id}/webapps")
def webapp_hinzufuegen(
workspace_id: str, name: str = Form(...), url: str = Form(...), auf_desktop: bool = Form(default=False),
user: dict = Depends(require_admin),
):
return _ergebnis(
workspace_id,
anode_request(
"POST", f"/api/v1/workspaces/{workspace_id}/webapps",
json={"name": name, "url": url, "auf_desktop": auf_desktop},
),
)
@router.post("/liste/{workspace_id}/webapps/{webapp_id}/desktop")
def webapp_desktop(
workspace_id: str, webapp_id: str, auf_desktop: bool = Form(default=False), user: dict = Depends(require_admin),
):
return _ergebnis(
workspace_id,
anode_request(
"PUT", f"/api/v1/workspaces/{workspace_id}/webapps/{webapp_id}/desktop",
json={"auf_desktop": auf_desktop},
),
)
@router.post("/liste/{workspace_id}/webapps/vorlage")
def webapp_aus_vorlage(workspace_id: str, template_id: str = Form(...), user: dict = Depends(require_admin)):
return _ergebnis(
workspace_id,
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/webapp-templates/{template_id}/activate"),
)
@router.post("/liste/{workspace_id}/webapps/{webapp_id}/entfernen")
def webapp_entfernen(workspace_id: str, webapp_id: str, user: dict = Depends(require_admin)):
return _ergebnis(workspace_id, anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/webapps/{webapp_id}"))
@router.post("/liste/{workspace_id}/standardbrowser")
def standardbrowser_setzen(workspace_id: str, browser: str = Form(default=""), user: dict = Depends(require_admin)):
return _ergebnis(
workspace_id,
anode_request("PUT", f"/api/v1/workspaces/{workspace_id}/standardbrowser", json={"browser": browser or None}),
"Standardbrowser gespeichert.",
)
@router.post("/liste/{workspace_id}/einstellungen")
def einstellungen(
workspace_id: str, name: str = Form(...), description: str = Form(default=""), user: dict = Depends(require_admin)
):
return _ergebnis(
workspace_id,
anode_request("PUT", f"/api/v1/workspaces/{workspace_id}", json={"name": name, "description": description or None}),
"Gespeichert.",
)
@router.post("/liste/{workspace_id}/vorlage")
async def vorlage(request: Request, workspace_id: str, user: dict = Depends(require_admin)):
form = await request.form()
result = anode_request(
"PUT", f"/api/v1/workspaces/{workspace_id}/vorlage",
json={
"ist_vorlage": form.get("ist_vorlage") == "ja",
"sichtbarkeit": form.get("sichtbarkeit") or "global",
"kundengruppe_ids": form.getlist("kundengruppe_ids"),
"organization_ids": form.getlist("organization_ids"),
},
)
return _ergebnis(workspace_id, result, "Vorlagen-Einstellung gespeichert.")
@router.post("/liste/{workspace_id}/kopie")
def kopie_anlegen(
workspace_id: str, name: str = Form(...), backend_id: str = Form(...), organization_id: str = Form(default=""),
user: dict = Depends(require_admin),
):
result = anode_request(
"POST", f"/api/v1/workspaces/{workspace_id}/kopie",
json={"name": name, "backend_id": backend_id, "organization_id": organization_id or None},
)
if not result.get("success"):
return _ergebnis(workspace_id, result)
neu = result["workspace"]
hinweis = "Kopie angelegt."
if neu.get("weggefallen"):
hinweis += " Auf der neuen Distro weggefallen: " + ", ".join(neu["weggefallen"])
return _zurueck(neu["id"], hinweis=hinweis)
@router.post("/liste/{workspace_id}/loeschen")
def loeschen(workspace_id: str, bestaetigt: str = Form(default=""), user: dict = Depends(require_admin)):
if bestaetigt != "ja":
return _zurueck(workspace_id, fehler="Bitte das Löschen bestätigen.")
result = anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}")
if not result.get("success"):
return _ergebnis(workspace_id, result)
return RedirectResponse("/admin/workspaces/liste?" + urlencode({"hinweis": "Workspace gelöscht."}), status_code=303)