diff --git a/app.py b/app.py index 08ccb2d..d59679a 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ from routers import ( admin_kunden, admin_technik, geraete, + gruppen, installationsmedium, organisation, struktur, @@ -21,6 +22,7 @@ app.include_router(auth.router) app.include_router(geraete.router) app.include_router(organisation.router) app.include_router(struktur.router) +app.include_router(gruppen.router) app.include_router(installationsmedium.router) app.include_router(admin_technik.router) app.include_router(admin_geraete.router) diff --git a/routers/gruppen.py b/routers/gruppen.py new file mode 100644 index 0000000..d12f374 --- /dev/null +++ b/routers/gruppen.py @@ -0,0 +1,180 @@ +from fastapi import APIRouter, Depends, Form, HTTPException, Request +from fastapi.responses import RedirectResponse + +from anode_client import anode_request +from auth import get_current_user +from templating import templates + + +router = APIRouter() + + +def fetch_gruppen(organization_id: str) -> list[dict]: + result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen") + return result.get("gruppen", []) if result.get("success") else [] + + +def find_gruppe_in_organization(gruppe_id: str, organization_id: str) -> dict | None: + """ + Besitz-Validierung analog zu find_oe_in_organization() in + routers/struktur.py (siehe ADR-0011): eine gruppe_id aus der URL wird + nur akzeptiert, wenn sie tatsächlich in der Gruppenliste der eigenen + Organisation auftaucht. + """ + + for gruppe in fetch_gruppen(organization_id): + if gruppe["id"] == gruppe_id: + return gruppe + + return None + + +@router.get("/gruppen") +def gruppen_liste(request: Request, user=Depends(get_current_user)): + gruppen = fetch_gruppen(user.organization_id) + + return templates.TemplateResponse(request, "gruppen_liste.html", {"user": user, "gruppen": gruppen}) + + +@router.post("/gruppen") +def gruppe_anlegen(name: str = Form(...), user=Depends(get_current_user)): + anode_request("POST", f"/api/v1/organizations/{user.organization_id}/gruppen", json={"name": name}) + + return RedirectResponse("/gruppen", status_code=303) + + +@router.get("/gruppen/{gruppe_id}/umbenennen") +def gruppe_umbenennen_form(request: Request, gruppe_id: str, user=Depends(get_current_user)): + gruppe = find_gruppe_in_organization(gruppe_id, user.organization_id) + + if gruppe is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + return templates.TemplateResponse(request, "gruppe_umbenennen.html", {"user": user, "gruppe": gruppe}) + + +@router.post("/gruppen/{gruppe_id}/umbenennen") +def gruppe_umbenennen(gruppe_id: str, name: str = Form(...), user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("PATCH", f"/api/v1/gruppen/{gruppe_id}", json={"name": name}) + + return RedirectResponse("/gruppen", status_code=303) + + +@router.get("/gruppen/{gruppe_id}/loeschen") +def gruppe_loeschen_form(request: Request, gruppe_id: str, user=Depends(get_current_user)): + gruppe = find_gruppe_in_organization(gruppe_id, user.organization_id) + + if gruppe is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + return templates.TemplateResponse(request, "gruppe_loeschen.html", {"user": user, "gruppe": gruppe}) + + +@router.post("/gruppen/{gruppe_id}/loeschen") +def gruppe_loeschen(gruppe_id: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("DELETE", f"/api/v1/gruppen/{gruppe_id}") + + return RedirectResponse("/gruppen", status_code=303) + + +@router.get("/gruppen/{gruppe_id}") +def gruppe_detail(request: Request, gruppe_id: str, user=Depends(get_current_user)): + gruppe = find_gruppe_in_organization(gruppe_id, user.organization_id) + + if gruppe is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + merkmale_result = anode_request("GET", f"/api/v1/gruppen/{gruppe_id}/merkmale") + merkmale = merkmale_result.get("merkmale", []) if merkmale_result.get("success") else [] + + kategorien: dict[str, list[dict]] = {} + for eintrag in merkmale: + kategorie_name = eintrag["kategorie"]["name"] if eintrag["kategorie"] else "Ohne Kategorie" + kategorien.setdefault(kategorie_name, []).append(eintrag) + + devices_result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/devices") + devices = devices_result.get("devices", []) if devices_result.get("success") else [] + + # anode bietet keinen direkten "Geräte einer Gruppe"-Endpunkt an, nur + # "Gruppen eines Geräts" (GET /devices/{id}/gruppen) - daher hier für + # jedes Gerät der Organisation dessen Gruppen abfragen und filtern + # (gleiches Muster wie admin_kunden.py:gruppe_detail()). + mitglied_ids = set() + for device in devices: + dg_result = anode_request("GET", f"/api/v1/devices/{device['id']}/gruppen") + device_gruppen = dg_result.get("gruppen", []) if dg_result.get("success") else [] + if any(g["id"] == gruppe_id for g in device_gruppen): + mitglied_ids.add(device["id"]) + + return templates.TemplateResponse( + request, + "gruppe_detail.html", + { + "user": user, + "gruppe": gruppe, + "kategorien": kategorien, + "devices": devices, + "mitglied_ids": mitglied_ids, + }, + ) + + +@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select") +def gruppe_merkmal_select(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect") +def gruppe_merkmal_deselect(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset") +def gruppe_merkmal_unset(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/geraete/{device_id}/hinzufuegen") +def gruppe_geraet_hinzufuegen(gruppe_id: str, device_id: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + # Besitz-Validierung fürs Gerät reicht hier über den bestehenden + # find_device_in_organization()-Umweg nicht direkt, aber anode prüft + # bei add_device_to_gruppe() bereits serverseitig, dass Gerät und + # Gruppe zur selben Organisation gehören (siehe app.py) - ein Zugriff + # auf ein fremdes device_id würde dort mit invalid_membership abgelehnt. + anode_request("POST", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) + + +@router.post("/gruppen/{gruppe_id}/geraete/{device_id}/entfernen") +def gruppe_geraet_entfernen(gruppe_id: str, device_id: str, user=Depends(get_current_user)): + if find_gruppe_in_organization(gruppe_id, user.organization_id) is None: + raise HTTPException(status_code=404, detail="Gruppe wurde nicht gefunden.") + + anode_request("DELETE", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}") + + return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303) diff --git a/templates/base.html b/templates/base.html index 42adf5d..081806a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -20,6 +20,7 @@
"Nicht festgelegt" überlässt die Entscheidung der OE-Kette bzw. dem Workspace-Standard des Geräts. Gehört ein Gerät mehreren Gruppen mit widersprüchlicher Einstellung an, gewinnt "aktiv".
+ {% if not kategorien %} +Keine katalogfähigen Merkmale vorhanden.
+ {% else %} + {% for kategorie_name, eintraege in kategorien.items() %} +|
+ {{ eintrag.name }}
+ {% if eintrag.description %} {{ eintrag.description }}{% endif %} + |
+ + {% if eintrag.aktiv is none %} + nicht festgelegt + {% elif eintrag.aktiv %} + aktiv + {% else %} + inaktiv + {% endif %} + | ++ {% if eintrag.aktiv != true %} + + {% endif %} + {% if eintrag.aktiv != false %} + + {% endif %} + {% if eintrag.aktiv is not none %} + + {% endif %} + | +
Keine Geräte vorhanden.
+ {% else %} +| Hostname | Kennung | Mitglied | |
|---|---|---|---|
| {{ device.hostname or "—" }} | +{{ device.device_fingerprint }} |
+ {{ "ja" if device.id in mitglied_ids else "nein" }} | ++ {% if device.id in mitglied_ids %} + + {% else %} + + {% endif %} + | +
Soll die Gruppe "{{ gruppe.name }}" wirklich gelöscht werden? Betroffene Geräte verlieren die Merkmal-Einstellungen dieser Gruppe.
+ +Querschnitts-Gruppen (z.B. Lehrer/Schulleitung) — ein Gerät kann in mehreren gleichzeitig sein. Anders als Organisationseinheiten sind Gruppen nicht verschachtelt.
+ +{% if not gruppen %} +Keine Gruppen angelegt.
+{% else %} +| Name | |
|---|---|
| {{ gruppe.name }} | ++ Verwalten + Umbenennen + Löschen + | +