Workspace-Verwaltung: Entwurf/Veroeffentlichen, Wirkung, Vorlage, Kopie, Loeschen
Inhalte aendern sich nur noch im Entwurf (Endpunkte lehnen sonst mit kein_entwurf ab, Webapps muessen zum Entwurf gehoeren, neue Merkmale zur Distro passen). Veroeffentlichen prueft die Distro erneut. Neue Endpunkte: Entwurf anlegen/verwerfen, veroeffentlichen, Versionen, Nutzung (Profile, OEs, Geraete automatisch vs. gepinnt), Wirkung (Diff plus Nutzung), Name/Zweck, Vorlage mit Sichtbarkeit, Kopie-Pruefung und Kopie auf andere Distro (fehlende Merkmale fallen weg und werden gemeldet), Loeschen nur wenn ungenutzt (raeumt Icons auf), Kundengruppen. geraete_basis_herkunft() liefert die Herkunft der Grundausstattung. Workspace-Creator neu, Schritt E. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
857461cc44
commit
421e3edc45
1 file changed
+670
-4
@@ -214,6 +214,28 @@ class CreateKategorieRequest(BaseModel):
|
||||
art: Literal["app", "richtlinie"] = "app"
|
||||
|
||||
|
||||
class WorkspaceAktualisierenRequest(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class WorkspaceVeroeffentlichenRequest(BaseModel):
|
||||
notiz: str | None = None
|
||||
|
||||
|
||||
class WorkspaceVorlageRequest(BaseModel):
|
||||
ist_vorlage: bool
|
||||
sichtbarkeit: Literal["global", "kundengruppe", "organisation"] = "global"
|
||||
kundengruppe_ids: list[str] = []
|
||||
organization_ids: list[str] = []
|
||||
|
||||
|
||||
class WorkspaceKopieRequest(BaseModel):
|
||||
name: str
|
||||
backend_id: str
|
||||
organization_id: str | None = None
|
||||
|
||||
|
||||
class CreateWorkspaceAssistentRequest(BaseModel):
|
||||
"""Workspace-Creator neu, Schritt D: alles, was der Assistent sammelt."""
|
||||
name: str
|
||||
@@ -1279,9 +1301,18 @@ def arbeitsversion_id(cur, workspace_id):
|
||||
|
||||
|
||||
def geraete_basis(cur, device_id: str):
|
||||
"""Wie geraete_basis_herkunft(), ohne die Herkunft."""
|
||||
|
||||
ergebnis = geraete_basis_herkunft(cur, device_id)
|
||||
return ergebnis[:2] if ergebnis else None
|
||||
|
||||
|
||||
def geraete_basis_herkunft(cur, device_id: str):
|
||||
"""
|
||||
Grundausstattung eines Geräts (Workspace-Creator neu, Schritt C):
|
||||
(workspace_version_id, backend_id) oder None ohne Zuweisung.
|
||||
(workspace_version_id, backend_id, herkunft) oder None ohne Zuweisung.
|
||||
herkunft: {"art": "oe", "oe_id", "automatisch"} oder {"art": "installation"}
|
||||
- für die Wirkungsvorschau (wer steigt bei einer neuen Version mit um).
|
||||
|
||||
Die Distro (backend_id) kommt immer aus der Installation und ändert sich
|
||||
nie. Der Workspace kommt von der nächstgelegenen OE mit Workspace (eigene
|
||||
@@ -1324,12 +1355,16 @@ def geraete_basis(cur, device_id: str):
|
||||
while aktuell is not None and aktuell in oes:
|
||||
parent_id, workspace_id, version_id, automatisch = oes[aktuell]
|
||||
if workspace_id is not None:
|
||||
if automatisch or version_id is None:
|
||||
automatisch = bool(automatisch or version_id is None)
|
||||
if automatisch:
|
||||
version_id = veroeffentlichte_version_id(cur, workspace_id)
|
||||
return version_id, backend_id
|
||||
return version_id, backend_id, {"art": "oe", "oe_id": aktuell, "automatisch": automatisch}
|
||||
aktuell = parent_id
|
||||
|
||||
return veroeffentlichte_version_id(cur, installations_workspace_id), backend_id
|
||||
return (
|
||||
veroeffentlichte_version_id(cur, installations_workspace_id), backend_id,
|
||||
{"art": "installation"},
|
||||
)
|
||||
|
||||
def fetch_auftragskatalog_state(device_id: str):
|
||||
"""
|
||||
@@ -3109,6 +3144,445 @@ def create_workspace_mit_inhalt(
|
||||
return {"id": str(workspace_id), "key": key, "name": name}, None
|
||||
|
||||
|
||||
# --- Workspace-Verwaltung (Workspace-Creator neu, Schritt E, 07.10.2026) ---
|
||||
#
|
||||
# Inhalte (Merkmale, Webapps) ändern sich nur noch in einem Entwurf. Die
|
||||
# veröffentlichte Version ist eingefroren; Veröffentlichen macht den Entwurf
|
||||
# zur neuen Version, auf die alle Geräte mit "automatisch aktualisieren"
|
||||
# (bzw. ohne OE-Workspace) umsteigen. Gepinnte OEs bleiben auf ihrer Version.
|
||||
|
||||
def entwurf_id(cur, workspace_id):
|
||||
cur.execute(
|
||||
"SELECT id FROM workspace_versionen WHERE workspace_id = %s AND status = 'entwurf'",
|
||||
(workspace_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def _version_kopieren(cur, quelle_version_id, ziel_version_id, ziel_workspace_id, merkmal_filter=None):
|
||||
"""Merkmale und Webapps einer Version in eine andere kopieren (Webapps
|
||||
mit neuer ID, Icon-Datei geteilt). merkmal_filter: optionale Menge
|
||||
erlaubter Merkmal-IDs."""
|
||||
|
||||
cur.execute(
|
||||
"SELECT merkmal_id FROM workspace_version_merkmale WHERE workspace_version_id = %s",
|
||||
(quelle_version_id,),
|
||||
)
|
||||
for (merkmal_id,) in cur.fetchall():
|
||||
if merkmal_filter is None or merkmal_id in merkmal_filter:
|
||||
cur.execute(
|
||||
"INSERT INTO workspace_version_merkmale (workspace_version_id, merkmal_id) VALUES (%s, %s)",
|
||||
(ziel_version_id, merkmal_id),
|
||||
)
|
||||
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, kiosk_modus)
|
||||
SELECT gen_random_uuid(), %s, %s, name, url, icon_filename, icon_content_type, sort_order,
|
||||
template_id, kategorie_id, kiosk_modus
|
||||
FROM workspace_webapps WHERE workspace_version_id = %s
|
||||
""",
|
||||
(ziel_workspace_id, ziel_version_id, quelle_version_id),
|
||||
)
|
||||
|
||||
|
||||
def workspace_entwurf_anlegen(workspace_id: str):
|
||||
"""Neuer Entwurf als Kopie der neuesten veröffentlichten Version; gibt es
|
||||
schon einen, wird er zurückgegeben. None, wenn der Workspace fehlt."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT 1 FROM workspaces WHERE id = %s FOR UPDATE", (workspace_id,))
|
||||
if cur.fetchone() is None:
|
||||
return None
|
||||
vorhanden = entwurf_id(cur, workspace_id)
|
||||
if vorhanden:
|
||||
return str(vorhanden)
|
||||
cur.execute("SELECT COALESCE(max(nummer), 0) + 1 FROM workspace_versionen WHERE workspace_id = %s", (workspace_id,))
|
||||
(nummer,) = cur.fetchone()
|
||||
neu = uuid4()
|
||||
cur.execute(
|
||||
"INSERT INTO workspace_versionen (id, workspace_id, nummer, status) VALUES (%s, %s, %s, 'entwurf')",
|
||||
(neu, workspace_id, nummer),
|
||||
)
|
||||
quelle = veroeffentlichte_version_id(cur, workspace_id)
|
||||
if quelle:
|
||||
_version_kopieren(cur, quelle, neu, workspace_id)
|
||||
return str(neu)
|
||||
|
||||
|
||||
def _icons_aufraeumen(cur, icon_dateien):
|
||||
"""Webapp-Icons löschen, die keine Zeile mehr verwendet (nach dem Löschen
|
||||
von Webapp-Zeilen)."""
|
||||
|
||||
for datei in {d for d in icon_dateien if d}:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM workspace_webapps WHERE icon_filename = %s
|
||||
UNION ALL SELECT 1 FROM gruppen_webapps WHERE icon_filename = %s
|
||||
UNION ALL SELECT 1 FROM webapp_templates WHERE icon_filename = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(datei, datei, datei),
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
pfad = WEBAPP_ICONS_DIR / datei
|
||||
if pfad.is_file():
|
||||
pfad.unlink()
|
||||
|
||||
|
||||
def workspace_entwurf_verwerfen(workspace_id: str) -> bool:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
vid = entwurf_id(cur, workspace_id)
|
||||
if vid is None:
|
||||
return False
|
||||
cur.execute("SELECT icon_filename FROM workspace_webapps WHERE workspace_version_id = %s", (vid,))
|
||||
icons = [r[0] for r in cur.fetchall()]
|
||||
cur.execute("DELETE FROM workspace_versionen WHERE id = %s", (vid,))
|
||||
_icons_aufraeumen(cur, icons)
|
||||
return True
|
||||
|
||||
|
||||
def workspace_veroeffentlichen(workspace_id: str, notiz: str | None):
|
||||
"""Rückgabe: (nummer, None) oder (None, (fehlercode, nachricht))."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
vid = entwurf_id(cur, workspace_id)
|
||||
if vid is None:
|
||||
return None, ("kein_entwurf", "Es gibt keinen Entwurf zum Veröffentlichen.")
|
||||
# Gleiche Distro-Regel wie beim Anlegen: nichts veröffentlichen,
|
||||
# was auf der Distro des Workspace still fehlen würde.
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.name FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
JOIN workspaces w ON w.id = %s
|
||||
LEFT JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = w.backend_id
|
||||
WHERE vm.workspace_version_id = %s AND w.backend_id IS NOT NULL AND bp.id IS NULL
|
||||
ORDER BY m.name
|
||||
""",
|
||||
(workspace_id, vid),
|
||||
)
|
||||
fehlend = [r[0] for r in cur.fetchall()]
|
||||
if fehlend:
|
||||
return None, ("distro_konflikt", "Auf der Distro des Workspace nicht umgesetzt: " + ", ".join(fehlend))
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE workspace_versionen SET status = 'veroeffentlicht', veroeffentlicht_am = now(), notiz = %s
|
||||
WHERE id = %s RETURNING nummer
|
||||
""",
|
||||
(notiz or None, vid),
|
||||
)
|
||||
(nummer,) = cur.fetchone()
|
||||
cur.execute("UPDATE workspaces SET updated_at = now() WHERE id = %s", (workspace_id,))
|
||||
return nummer, None
|
||||
|
||||
|
||||
def workspace_versionen(workspace_id: str) -> list[dict]:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT v.id, v.nummer, v.status, v.notiz, v.erstellt_am, v.veroeffentlicht_am,
|
||||
(SELECT count(*) FROM workspace_version_merkmale vm WHERE vm.workspace_version_id = v.id),
|
||||
(SELECT count(*) FROM workspace_webapps ww WHERE ww.workspace_version_id = v.id)
|
||||
FROM workspace_versionen v WHERE v.workspace_id = %s ORDER BY v.nummer DESC
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(vid), "nummer": nummer, "status": status, "notiz": notiz,
|
||||
"erstellt_am": erstellt.isoformat() if erstellt else None,
|
||||
"veroeffentlicht_am": veroeff.isoformat() if veroeff else None,
|
||||
"anzahl_merkmale": anz_m, "anzahl_webapps": anz_w,
|
||||
}
|
||||
for vid, nummer, status, notiz, erstellt, veroeff, anz_m, anz_w in cur.fetchall()
|
||||
]
|
||||
|
||||
|
||||
def workspace_nutzung(workspace_id: str) -> dict:
|
||||
"""
|
||||
Wer nutzt diesen Workspace: Installationsprofile (künftige
|
||||
Installationen), OEs (mit Version/Automatik) und Geräte (aktive, nicht
|
||||
archivierte) - letztere aufgeteilt in "steigen bei neuer Version
|
||||
automatisch um" und "auf Version N gepinnt".
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bv.id, bv.label, o.name FROM bereitstellungsvorlagen bv
|
||||
JOIN organizations o ON o.id = bv.organization_id
|
||||
WHERE bv.workspace_id = %s ORDER BY o.name, bv.label
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
profile = [{"id": str(i), "label": l, "organisation": o} for i, l, o in cur.fetchall()]
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT oe.id, oe.name, o.name, oe.automatisch_aktualisieren, v.nummer
|
||||
FROM organisationseinheiten oe
|
||||
JOIN organizations o ON o.id = oe.organization_id
|
||||
LEFT JOIN workspace_versionen v ON v.id = oe.workspace_version_id
|
||||
WHERE oe.workspace_id = %s ORDER BY o.name, oe.name
|
||||
""",
|
||||
(workspace_id,),
|
||||
)
|
||||
oes = [
|
||||
{"id": str(i), "name": n, "organisation": o, "automatisch": a or nr is None, "version": nr}
|
||||
for i, n, o, a, nr in cur.fetchall()
|
||||
]
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT d.id, o.name FROM devices d
|
||||
JOIN organizations o ON o.id = d.organization_id
|
||||
JOIN assignments a ON a.device_id = d.id
|
||||
WHERE d.archived_at IS NULL AND d.deprovisioned_at IS NULL
|
||||
"""
|
||||
)
|
||||
geraete = cur.fetchall()
|
||||
cur.execute("SELECT id, nummer FROM workspace_versionen WHERE workspace_id = %s", (workspace_id,))
|
||||
eigene_versionen = {vid: nr for vid, nr in cur.fetchall()}
|
||||
|
||||
automatisch, gepinnt, organisationen = 0, {}, set()
|
||||
for device_id, org_name in geraete:
|
||||
basis = geraete_basis_herkunft(cur, str(device_id))
|
||||
if basis is None or basis[0] not in eigene_versionen:
|
||||
continue
|
||||
organisationen.add(org_name)
|
||||
if basis[2]["art"] == "installation" or basis[2]["automatisch"]:
|
||||
automatisch += 1
|
||||
else:
|
||||
nr = eigene_versionen[basis[0]]
|
||||
gepinnt[nr] = gepinnt.get(nr, 0) + 1
|
||||
|
||||
return {
|
||||
"profile": profile,
|
||||
"oes": oes,
|
||||
"geraete_automatisch": automatisch,
|
||||
"geraete_gepinnt": [{"version": nr, "anzahl": n} for nr, n in sorted(gepinnt.items())],
|
||||
"organisationen": sorted(organisationen | {p["organisation"] for p in profile} | {o["organisation"] for o in oes}),
|
||||
"in_benutzung": bool(profile or oes or automatisch or gepinnt),
|
||||
}
|
||||
|
||||
|
||||
def workspace_wirkung(workspace_id: str) -> dict | None:
|
||||
"""Wirkungsvorschau für das Veröffentlichen des Entwurfs: Unterschiede
|
||||
zur neuesten veröffentlichten Version plus Nutzung."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
neu = entwurf_id(cur, workspace_id)
|
||||
if neu is None:
|
||||
return None
|
||||
alt = veroeffentlichte_version_id(cur, workspace_id)
|
||||
|
||||
def merkmale(vid):
|
||||
if vid is None:
|
||||
return {}
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.name, k.art FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
LEFT JOIN kategorien k ON k.id = m.kategorie_id
|
||||
WHERE vm.workspace_version_id = %s
|
||||
""",
|
||||
(vid,),
|
||||
)
|
||||
return {mid: (name, art or "app") for mid, name, art in cur.fetchall()}
|
||||
|
||||
def webapps(vid):
|
||||
if vid is None:
|
||||
return set()
|
||||
cur.execute("SELECT name, url FROM workspace_webapps WHERE workspace_version_id = %s", (vid,))
|
||||
return set(cur.fetchall())
|
||||
|
||||
m_alt, m_neu = merkmale(alt), merkmale(neu)
|
||||
w_alt, w_neu = webapps(alt), webapps(neu)
|
||||
|
||||
def namen(ids, quelle, art):
|
||||
return sorted(quelle[i][0] for i in ids if quelle[i][1] == art)
|
||||
|
||||
hinzu, weg = set(m_neu) - set(m_alt), set(m_alt) - set(m_neu)
|
||||
return {
|
||||
"apps_neu": namen(hinzu, m_neu, "app"),
|
||||
"apps_weg": namen(weg, m_alt, "app"),
|
||||
"richtlinien_neu": namen(hinzu, m_neu, "richtlinie"),
|
||||
"richtlinien_weg": namen(weg, m_alt, "richtlinie"),
|
||||
"webapps_neu": sorted(n for n, _u in w_neu - w_alt),
|
||||
"webapps_weg": sorted(n for n, _u in w_alt - w_neu),
|
||||
"unveraendert": not (hinzu or weg or w_neu != w_alt),
|
||||
"nutzung": workspace_nutzung(workspace_id),
|
||||
}
|
||||
|
||||
|
||||
def workspace_aktualisieren(workspace_id: str, name: str, description: str | None) -> bool:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE workspaces SET name = %s, description = %s, updated_at = now() WHERE id = %s RETURNING id",
|
||||
(name.strip(), description or None, workspace_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def workspace_vorlage_setzen(
|
||||
workspace_id: str, ist_vorlage: bool, sichtbarkeit: str,
|
||||
kundengruppe_ids: list[str], organization_ids: list[str],
|
||||
):
|
||||
"""Vorlagen-Kennzeichen und für wen sie sichtbar ist. Rückgabe wie
|
||||
workspace_veroeffentlichen()."""
|
||||
|
||||
if sichtbarkeit not in ("global", "kundengruppe", "organisation"):
|
||||
return None, ("invalid_request", "Unbekannte Sichtbarkeit.")
|
||||
if ist_vorlage and sichtbarkeit == "kundengruppe" and not kundengruppe_ids:
|
||||
return None, ("invalid_request", "Bitte mindestens eine Kundengruppe wählen.")
|
||||
if ist_vorlage and sichtbarkeit == "organisation" and not organization_ids:
|
||||
return None, ("invalid_request", "Bitte mindestens eine Organisation wählen.")
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE workspaces SET ist_vorlage = %s, sichtbarkeit = %s, updated_at = now() WHERE id = %s RETURNING id",
|
||||
(ist_vorlage, sichtbarkeit, workspace_id),
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
return None, ("not_found", "Workspace wurde nicht gefunden.")
|
||||
cur.execute("DELETE FROM workspace_freigabe_kundengruppen WHERE workspace_id = %s", (workspace_id,))
|
||||
cur.execute("DELETE FROM workspace_freigabe_organisationen WHERE workspace_id = %s", (workspace_id,))
|
||||
if sichtbarkeit == "kundengruppe":
|
||||
for kg in dict.fromkeys(kundengruppe_ids):
|
||||
cur.execute(
|
||||
"INSERT INTO workspace_freigabe_kundengruppen (workspace_id, kundengruppe_id) VALUES (%s, %s)",
|
||||
(workspace_id, kg),
|
||||
)
|
||||
if sichtbarkeit == "organisation":
|
||||
for org in dict.fromkeys(organization_ids):
|
||||
cur.execute(
|
||||
"INSERT INTO workspace_freigabe_organisationen (workspace_id, organization_id) VALUES (%s, %s)",
|
||||
(workspace_id, org),
|
||||
)
|
||||
return True, None
|
||||
|
||||
|
||||
def workspace_freigaben(workspace_id: str) -> dict:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT kundengruppe_id FROM workspace_freigabe_kundengruppen WHERE workspace_id = %s", (workspace_id,))
|
||||
kgs = [str(r[0]) for r in cur.fetchall()]
|
||||
cur.execute("SELECT organization_id FROM workspace_freigabe_organisationen WHERE workspace_id = %s", (workspace_id,))
|
||||
orgs = [str(r[0]) for r in cur.fetchall()]
|
||||
return {"kundengruppe_ids": kgs, "organization_ids": orgs}
|
||||
|
||||
|
||||
def fetch_kundengruppen() -> list[dict]:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id, key, name FROM kundengruppen ORDER BY sort_order, name")
|
||||
return [{"id": str(i), "key": k, "name": n} for i, k, n in cur.fetchall()]
|
||||
|
||||
|
||||
def workspace_kopie_pruefen(workspace_id: str, backend_id: str) -> list[str] | None:
|
||||
"""Merkmale der neuesten veröffentlichten Version, die auf der Ziel-Distro
|
||||
nicht umgesetzt sind und bei einer Kopie wegfallen würden."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
vid = veroeffentlichte_version_id(cur, workspace_id)
|
||||
if vid is None:
|
||||
return None
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.name FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
LEFT JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = %s
|
||||
WHERE vm.workspace_version_id = %s AND bp.id IS NULL ORDER BY m.name
|
||||
""",
|
||||
(backend_id, vid),
|
||||
)
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
|
||||
def workspace_kopieren(workspace_id: str, name: str, backend_id: str, organization_id: str | None):
|
||||
"""Kopie der neuesten veröffentlichten Version als neuer Workspace
|
||||
(Version 1) - auch auf eine andere Distro; Merkmale, die dort fehlen,
|
||||
fallen weg und werden zurückgemeldet."""
|
||||
|
||||
name = name.strip()
|
||||
if not name:
|
||||
return None, ("invalid_request", "Bitte einen Namen angeben.")
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
quelle = veroeffentlichte_version_id(cur, workspace_id)
|
||||
if quelle is None:
|
||||
return None, ("not_found", "Workspace oder veröffentlichte Version nicht gefunden.")
|
||||
cur.execute("SELECT description FROM workspaces WHERE id = %s", (workspace_id,))
|
||||
(description,) = cur.fetchone()
|
||||
cur.execute("SELECT 1 FROM backends WHERE id = %s", (backend_id,))
|
||||
if cur.fetchone() is None:
|
||||
return None, ("backend_not_found", "Die gewählte Distro gibt es nicht.")
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT vm.merkmal_id, m.name, bp.id IS NOT NULL FROM workspace_version_merkmale vm
|
||||
JOIN merkmale m ON m.id = vm.merkmal_id
|
||||
LEFT JOIN blueprints bp ON bp.merkmal_id = vm.merkmal_id AND bp.backend_id = %s
|
||||
WHERE vm.workspace_version_id = %s
|
||||
""",
|
||||
(backend_id, quelle),
|
||||
)
|
||||
zeilen = cur.fetchall()
|
||||
erlaubt = {mid for mid, _n, ok in zeilen if ok}
|
||||
weggefallen = sorted(n for _mid, n, ok in zeilen if not ok)
|
||||
|
||||
neu_ws, neu_v = uuid4(), uuid4()
|
||||
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)
|
||||
""",
|
||||
(neu_ws, 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', %s, now())
|
||||
""",
|
||||
(neu_v, neu_ws, "Kopie von Workspace " + str(workspace_id)),
|
||||
)
|
||||
_version_kopieren(cur, quelle, neu_v, neu_ws, merkmal_filter=erlaubt)
|
||||
|
||||
return {"id": str(neu_ws), "key": key, "name": name, "weggefallen": weggefallen}, None
|
||||
|
||||
|
||||
def workspace_loeschen(workspace_id: str):
|
||||
"""Löscht einen Workspace samt Versionen - nur wenn ihn nichts mehr nutzt
|
||||
(kein Profil, keine OE, kein Gerät)."""
|
||||
|
||||
nutzung = workspace_nutzung(workspace_id)
|
||||
if nutzung["in_benutzung"]:
|
||||
return None, ("in_benutzung", "Der Workspace wird noch verwendet und kann nicht gelöscht werden.")
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT icon_filename FROM workspace_webapps WHERE workspace_id = %s", (workspace_id,))
|
||||
icons = [r[0] for r in cur.fetchall()]
|
||||
cur.execute("DELETE FROM workspaces WHERE id = %s RETURNING id", (workspace_id,))
|
||||
if cur.fetchone() is None:
|
||||
return None, ("not_found", "Workspace wurde nicht gefunden.")
|
||||
_icons_aufraeumen(cur, icons)
|
||||
return True, None
|
||||
|
||||
|
||||
def clone_workspace(source_workspace_id: str, organization_id: str):
|
||||
"""
|
||||
"Diesen Standard übernehmen" (GUI-Umbau Phase 3, Nachtrag 11.09.2026):
|
||||
@@ -5048,6 +5522,139 @@ def clone_workspace_endpoint(
|
||||
return {"success": True, "workspace": neuer_workspace}
|
||||
|
||||
|
||||
@app.get("/api/v1/kundengruppen")
|
||||
def list_kundengruppen(authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, "kundengruppen": fetch_kundengruppen()}
|
||||
|
||||
|
||||
@app.put("/api/v1/workspaces/{workspace_id}")
|
||||
def update_workspace_endpoint(
|
||||
workspace_id: str, payload: WorkspaceAktualisierenRequest, authorization: str | None = Header(default=None)
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not payload.name.strip():
|
||||
return {"success": False, "error": "invalid_request", "message": "Bitte einen Namen angeben."}
|
||||
if not workspace_aktualisieren(workspace_id, payload.name, payload.description):
|
||||
return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.delete("/api/v1/workspaces/{workspace_id}")
|
||||
def delete_workspace_endpoint(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
_ok, fehler = workspace_loeschen(workspace_id)
|
||||
if fehler:
|
||||
return {"success": False, "error": fehler[0], "message": fehler[1]}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/workspaces/{workspace_id}/versionen")
|
||||
def list_workspace_versionen(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, "versionen": workspace_versionen(workspace_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces/{workspace_id}/entwurf")
|
||||
def create_workspace_entwurf(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
vid = workspace_entwurf_anlegen(workspace_id)
|
||||
if vid is None:
|
||||
return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."}
|
||||
return {"success": True, "version_id": vid}
|
||||
|
||||
|
||||
@app.delete("/api/v1/workspaces/{workspace_id}/entwurf")
|
||||
def delete_workspace_entwurf(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not workspace_entwurf_verwerfen(workspace_id):
|
||||
return {"success": False, "error": "kein_entwurf", "message": "Es gibt keinen Entwurf."}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces/{workspace_id}/veroeffentlichen")
|
||||
def publish_workspace_entwurf(
|
||||
workspace_id: str, payload: WorkspaceVeroeffentlichenRequest, authorization: str | None = Header(default=None)
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
nummer, fehler = workspace_veroeffentlichen(workspace_id, payload.notiz)
|
||||
if fehler:
|
||||
return {"success": False, "error": fehler[0], "message": fehler[1]}
|
||||
return {"success": True, "nummer": nummer}
|
||||
|
||||
|
||||
@app.get("/api/v1/workspaces/{workspace_id}/nutzung")
|
||||
def get_workspace_nutzung(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, "nutzung": workspace_nutzung(workspace_id)}
|
||||
|
||||
|
||||
@app.get("/api/v1/workspaces/{workspace_id}/wirkung")
|
||||
def get_workspace_wirkung(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
wirkung = workspace_wirkung(workspace_id)
|
||||
if wirkung is None:
|
||||
return {"success": False, "error": "kein_entwurf", "message": "Es gibt keinen Entwurf."}
|
||||
return {"success": True, "wirkung": wirkung}
|
||||
|
||||
|
||||
@app.put("/api/v1/workspaces/{workspace_id}/vorlage")
|
||||
def set_workspace_vorlage(
|
||||
workspace_id: str, payload: WorkspaceVorlageRequest, authorization: str | None = Header(default=None)
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
_ok, fehler = workspace_vorlage_setzen(
|
||||
workspace_id, payload.ist_vorlage, payload.sichtbarkeit, payload.kundengruppe_ids, payload.organization_ids
|
||||
)
|
||||
if fehler:
|
||||
return {"success": False, "error": fehler[0], "message": fehler[1]}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/workspaces/{workspace_id}/kopie-pruefung")
|
||||
def check_workspace_kopie(workspace_id: str, backend_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
fehlend = workspace_kopie_pruefen(workspace_id, backend_id)
|
||||
if fehlend is None:
|
||||
return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."}
|
||||
return {"success": True, "weggefallen": fehlend}
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces/{workspace_id}/kopie")
|
||||
def copy_workspace_endpoint(
|
||||
workspace_id: str, payload: WorkspaceKopieRequest, authorization: str | None = Header(default=None)
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
kopie, fehler = workspace_kopieren(workspace_id, payload.name, payload.backend_id, payload.organization_id)
|
||||
if fehler:
|
||||
return {"success": False, "error": fehler[0], "message": fehler[1]}
|
||||
return {"success": True, "workspace": kopie}
|
||||
|
||||
|
||||
@app.get("/api/v1/workspaces/{workspace_id}")
|
||||
def get_workspace(workspace_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
@@ -5058,14 +5665,58 @@ def get_workspace(workspace_id: str, authorization: str | None = Header(default=
|
||||
if workspace is None:
|
||||
return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."}
|
||||
|
||||
workspace.update(workspace_freigaben(workspace_id))
|
||||
return {"success": True, "workspace": workspace}
|
||||
|
||||
|
||||
def _nur_im_entwurf(workspace_id: str, webapp_id: str | None = None, merkmal_id: str | None = None):
|
||||
"""
|
||||
Inhaltliche Änderungen gehen seit Schritt E (07.10.2026) nur in einen
|
||||
Entwurf - die veröffentlichte Version ist eingefroren. Prüft zusätzlich,
|
||||
dass eine Webapp zu diesem Entwurf gehört und ein neues Merkmal auf der
|
||||
Distro des Workspace umgesetzt ist. Gibt eine Fehlerantwort oder None.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
vid = entwurf_id(cur, workspace_id)
|
||||
if vid is None:
|
||||
return {"success": False, "error": "kein_entwurf",
|
||||
"message": "Änderungen gehen nur in einem Entwurf - bitte zuerst einen Entwurf anlegen."}
|
||||
if webapp_id is not None:
|
||||
cur.execute(
|
||||
"SELECT 1 FROM workspace_webapps WHERE id = %s AND workspace_version_id = %s",
|
||||
(webapp_id, vid),
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
return {"success": False, "error": "not_found", "message": "Webapp gehört nicht zum Entwurf."}
|
||||
if merkmal_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.name, w.backend_id IS NULL OR bp.id IS NOT NULL
|
||||
FROM merkmale m JOIN workspaces w ON w.id = %s
|
||||
LEFT JOIN blueprints bp ON bp.merkmal_id = m.id AND bp.backend_id = w.backend_id
|
||||
WHERE m.id = %s
|
||||
""",
|
||||
(workspace_id, merkmal_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return {"success": False, "error": "not_found", "message": "Merkmal wurde nicht gefunden."}
|
||||
if not row[1]:
|
||||
return {"success": False, "error": "distro_konflikt",
|
||||
"message": f"{row[0]} ist auf der Distro dieses Workspace nicht umgesetzt."}
|
||||
return None
|
||||
|
||||
|
||||
@app.post("/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
def add_workspace_merkmal(workspace_id: str, merkmal_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if fehler := _nur_im_entwurf(workspace_id, merkmal_id=merkmal_id):
|
||||
return fehler
|
||||
|
||||
add_merkmal_to_workspace(workspace_id, merkmal_id)
|
||||
|
||||
return {"success": True}
|
||||
@@ -5076,6 +5727,9 @@ def remove_workspace_merkmal(workspace_id: str, merkmal_id: str, authorization:
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if fehler := _nur_im_entwurf(workspace_id):
|
||||
return fehler
|
||||
|
||||
remove_merkmal_from_workspace(workspace_id, merkmal_id)
|
||||
|
||||
return {"success": True}
|
||||
@@ -5094,6 +5748,9 @@ def create_workspace_webapp(workspace_id: str, payload: CreateWebappRequest, aut
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if fehler := _nur_im_entwurf(workspace_id):
|
||||
return fehler
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"webapp": create_webapp(
|
||||
@@ -5108,6 +5765,9 @@ def update_workspace_webapp(workspace_id: str, webapp_id: str, payload: UpdateWe
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if fehler := _nur_im_entwurf(workspace_id, webapp_id=webapp_id):
|
||||
return fehler
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"webapp": update_webapp(
|
||||
@@ -5122,6 +5782,9 @@ def delete_workspace_webapp(workspace_id: str, webapp_id: str, authorization: st
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if fehler := _nur_im_entwurf(workspace_id, webapp_id=webapp_id):
|
||||
return fehler
|
||||
|
||||
delete_webapp(webapp_id)
|
||||
|
||||
return {"success": True}
|
||||
@@ -5174,6 +5837,9 @@ def activate_webapp_template_endpoint(workspace_id: str, template_id: str, autho
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if fehler := _nur_im_entwurf(workspace_id):
|
||||
return fehler
|
||||
|
||||
webapp = activate_webapp_template(workspace_id, template_id)
|
||||
|
||||
if webapp is None:
|
||||
|
||||
Reference in new issue
Block a user