feat: Struktur (OE-Selfservice) + Gruppen-Verwaltung (Admin)
Kundenportal:
- Neue Seite /struktur - OE-Baum (rekursives Jinja-Makro), Anlegen/
Umbenennen/Löschen (Standard-OE 'Neue Geräte' geschützt), Merkmale-
Editor pro OE mit Tristate aktiv/inaktiv/nicht festgelegt.
- geraete_liste.html: neue Spalte 'OE', Aktionen-Dropdown um 'In OE
verschieben' erweitert.
- auftragskatalog.html: zeigt jetzt die Herkunft einer Einstellung an
(OE/Gruppe/manuell), sofern sie nicht vom Workspace-Default kommt.
Admin (/admin/kunden/{id}):
- Neue Sektion 'Gruppen' - Anlegen/Löschen, verlinkt auf neue
Detailseite (admin/gruppe_detail.html) mit Merkmale-Editor (wie OE)
und Geräte-Mitgliedschaft-Checkliste. Bewusst nur intern verwaltbar
in v1 (siehe Konfigurationsgruppen-Diskussion).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
af6e93067f
commit
232d621245
2
app.py
2
app.py
@ -10,6 +10,7 @@ from routers import (
|
|||||||
geraete,
|
geraete,
|
||||||
installationsmedium,
|
installationsmedium,
|
||||||
organisation,
|
organisation,
|
||||||
|
struktur,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
|||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
app.include_router(geraete.router)
|
app.include_router(geraete.router)
|
||||||
app.include_router(organisation.router)
|
app.include_router(organisation.router)
|
||||||
|
app.include_router(struktur.router)
|
||||||
app.include_router(installationsmedium.router)
|
app.include_router(installationsmedium.router)
|
||||||
app.include_router(admin_technik.router)
|
app.include_router(admin_technik.router)
|
||||||
app.include_router(admin_geraete.router)
|
app.include_router(admin_geraete.router)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, Form, Request
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from anode_client import anode_request
|
from anode_client import anode_request
|
||||||
@ -60,6 +60,9 @@ def detail(request: Request, organization_id: str, user: dict = Depends(require_
|
|||||||
backends_result = anode_request("GET", "/api/v1/backends")
|
backends_result = anode_request("GET", "/api/v1/backends")
|
||||||
backends = backends_result.get("backends", []) if backends_result.get("success") else []
|
backends = backends_result.get("backends", []) if backends_result.get("success") else []
|
||||||
|
|
||||||
|
gruppen_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen")
|
||||||
|
gruppen = gruppen_result.get("gruppen", []) if gruppen_result.get("success") else []
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/kunden_detail.html",
|
"admin/kunden_detail.html",
|
||||||
@ -72,10 +75,104 @@ def detail(request: Request, organization_id: str, user: dict = Depends(require_
|
|||||||
"bereitstellungsvorlagen": bereitstellungsvorlagen,
|
"bereitstellungsvorlagen": bereitstellungsvorlagen,
|
||||||
"workspaces": workspaces,
|
"workspaces": workspaces,
|
||||||
"backends": backends,
|
"backends": backends,
|
||||||
|
"gruppen": gruppen,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen")
|
||||||
|
def gruppe_anlegen(organization_id: str, name: str = Form(...), user: dict = Depends(require_admin)):
|
||||||
|
anode_request("POST", f"/api/v1/organizations/{organization_id}/gruppen", json={"name": name})
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen/{gruppe_id}/loeschen")
|
||||||
|
def gruppe_loeschen(organization_id: str, gruppe_id: str, user: dict = Depends(require_admin)):
|
||||||
|
anode_request("DELETE", f"/api/v1/gruppen/{gruppe_id}")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{organization_id}/gruppen/{gruppe_id}")
|
||||||
|
def gruppe_detail(request: Request, organization_id: str, gruppe_id: str, user: dict = Depends(require_admin)):
|
||||||
|
gruppen_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen")
|
||||||
|
gruppen = gruppen_result.get("gruppen", []) if gruppen_result.get("success") else []
|
||||||
|
gruppe = next((g for g in gruppen if g["id"] == gruppe_id), None)
|
||||||
|
|
||||||
|
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/{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.
|
||||||
|
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,
|
||||||
|
"admin/gruppe_detail.html",
|
||||||
|
{
|
||||||
|
"user": user,
|
||||||
|
"organization_id": organization_id,
|
||||||
|
"gruppe": gruppe,
|
||||||
|
"kategorien": kategorien,
|
||||||
|
"devices": devices,
|
||||||
|
"mitglied_ids": mitglied_ids,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select")
|
||||||
|
def gruppe_merkmal_select(organization_id: str, gruppe_id: str, merkmal_key: str, user: dict = Depends(require_admin)):
|
||||||
|
anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect")
|
||||||
|
def gruppe_merkmal_deselect(organization_id: str, gruppe_id: str, merkmal_key: str, user: dict = Depends(require_admin)):
|
||||||
|
anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/deselect")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset")
|
||||||
|
def gruppe_merkmal_unset(organization_id: str, gruppe_id: str, merkmal_key: str, user: dict = Depends(require_admin)):
|
||||||
|
anode_request("POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/unset")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen/{gruppe_id}/geraete/{device_id}/hinzufuegen")
|
||||||
|
def gruppe_geraet_hinzufuegen(organization_id: str, gruppe_id: str, device_id: str, user: dict = Depends(require_admin)):
|
||||||
|
anode_request("POST", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/gruppen/{gruppe_id}/geraete/{device_id}/entfernen")
|
||||||
|
def gruppe_geraet_entfernen(organization_id: str, gruppe_id: str, device_id: str, user: dict = Depends(require_admin)):
|
||||||
|
anode_request("DELETE", f"/api/v1/devices/{device_id}/gruppen/{gruppe_id}")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}/gruppen/{gruppe_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{organization_id}/bereitstellungsvorlagen")
|
@router.post("/{organization_id}/bereitstellungsvorlagen")
|
||||||
def bereitstellungsvorlage_anlegen(
|
def bereitstellungsvorlage_anlegen(
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from anode_client import anode_request
|
from anode_client import anode_request
|
||||||
@ -58,8 +58,13 @@ def geraete_liste(request: Request, user: dict = Depends(get_current_user)):
|
|||||||
result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/devices")
|
result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/devices")
|
||||||
devices = result.get("devices", []) if result.get("success") else []
|
devices = result.get("devices", []) if result.get("success") else []
|
||||||
|
|
||||||
|
oe_result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/organisationseinheiten")
|
||||||
|
organisationseinheiten = oe_result.get("organisationseinheiten", []) if oe_result.get("success") else []
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "geraete_liste.html", {"user": user, "devices": devices}
|
request,
|
||||||
|
"geraete_liste.html",
|
||||||
|
{"user": user, "devices": devices, "organisationseinheiten": organisationseinheiten},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -195,3 +200,13 @@ def loeschen(device_id: str, user: dict = Depends(get_current_user)):
|
|||||||
anode_request("POST", f"/api/v1/devices/{device_id}/archive")
|
anode_request("POST", f"/api/v1/devices/{device_id}/archive")
|
||||||
|
|
||||||
return RedirectResponse("/geraete", status_code=303)
|
return RedirectResponse("/geraete", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/geraete/{device_id}/oe")
|
||||||
|
def oe_verschieben(device_id: str, oe_id: str = Form(...), user: dict = Depends(get_current_user)):
|
||||||
|
if find_device_in_organization(device_id, user.organization_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
|
||||||
|
|
||||||
|
anode_request("POST", f"/api/v1/devices/{device_id}/oe", json={"oe_id": oe_id})
|
||||||
|
|
||||||
|
return RedirectResponse("/geraete", status_code=303)
|
||||||
|
|||||||
156
routers/struktur.py
Normal file
156
routers/struktur.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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_organisationseinheiten(organization_id: str) -> list[dict]:
|
||||||
|
result = anode_request("GET", f"/api/v1/organizations/{organization_id}/organisationseinheiten")
|
||||||
|
return result.get("organisationseinheiten", []) if result.get("success") else []
|
||||||
|
|
||||||
|
|
||||||
|
def find_oe_in_organization(oe_id: str, organization_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Besitz-Validierung analog zu find_device_in_organization() in
|
||||||
|
routers/geraete.py (siehe ADR-0011): eine oe_id aus der URL wird nur
|
||||||
|
akzeptiert, wenn sie tatsächlich in der OE-Liste der eigenen
|
||||||
|
Organisation auftaucht.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for oe in fetch_organisationseinheiten(organization_id):
|
||||||
|
if oe["id"] == oe_id:
|
||||||
|
return oe
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/struktur")
|
||||||
|
def struktur_ansicht(request: Request, user=Depends(get_current_user)):
|
||||||
|
oes = fetch_organisationseinheiten(user.organization_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "struktur.html", {"user": user, "organisationseinheiten": oes}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/struktur")
|
||||||
|
def oe_anlegen(
|
||||||
|
name: str = Form(...),
|
||||||
|
parent_id: str = Form(default=""),
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
anode_request(
|
||||||
|
"POST",
|
||||||
|
f"/api/v1/organizations/{user.organization_id}/organisationseinheiten",
|
||||||
|
json={"name": name, "parent_id": parent_id or None},
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse("/struktur", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/struktur/{oe_id}/umbenennen")
|
||||||
|
def oe_umbenennen_form(request: Request, oe_id: str, user=Depends(get_current_user)):
|
||||||
|
oe = find_oe_in_organization(oe_id, user.organization_id)
|
||||||
|
|
||||||
|
if oe is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
return templates.TemplateResponse(request, "struktur_umbenennen.html", {"user": user, "oe": oe})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/struktur/{oe_id}/umbenennen")
|
||||||
|
def oe_umbenennen(oe_id: str, name: str = Form(...), user=Depends(get_current_user)):
|
||||||
|
oe = find_oe_in_organization(oe_id, user.organization_id)
|
||||||
|
|
||||||
|
if oe is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
anode_request(
|
||||||
|
"PATCH",
|
||||||
|
f"/api/v1/organisationseinheiten/{oe_id}",
|
||||||
|
json={"name": name, "parent_id": oe["parent_id"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse("/struktur", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/struktur/{oe_id}/loeschen")
|
||||||
|
def oe_loeschen_form(request: Request, oe_id: str, user=Depends(get_current_user)):
|
||||||
|
oe = find_oe_in_organization(oe_id, user.organization_id)
|
||||||
|
|
||||||
|
if oe is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
return templates.TemplateResponse(request, "struktur_loeschen.html", {"user": user, "oe": oe, "fehler": None})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/struktur/{oe_id}/loeschen")
|
||||||
|
def oe_loeschen(request: Request, oe_id: str, user=Depends(get_current_user)):
|
||||||
|
oe = find_oe_in_organization(oe_id, user.organization_id)
|
||||||
|
|
||||||
|
if oe is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
result = anode_request("DELETE", f"/api/v1/organisationseinheiten/{oe_id}")
|
||||||
|
|
||||||
|
if not result.get("success"):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "struktur_loeschen.html", {"user": user, "oe": oe, "fehler": result.get("message")}
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse("/struktur", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/struktur/{oe_id}/merkmale")
|
||||||
|
def oe_merkmale_ansicht(request: Request, oe_id: str, user=Depends(get_current_user)):
|
||||||
|
oe = find_oe_in_organization(oe_id, user.organization_id)
|
||||||
|
|
||||||
|
if oe is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
result = anode_request("GET", f"/api/v1/organisationseinheiten/{oe_id}/merkmale")
|
||||||
|
merkmale = result.get("merkmale", []) if 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)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request, "struktur_merkmale.html", {"user": user, "oe": oe, "kategorien": kategorien}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/select")
|
||||||
|
def oe_merkmal_select(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
|
||||||
|
if find_oe_in_organization(oe_id, user.organization_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/select")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/deselect")
|
||||||
|
def oe_merkmal_deselect(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
|
||||||
|
if find_oe_in_organization(oe_id, user.organization_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/deselect")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/unset")
|
||||||
|
def oe_merkmal_unset(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
|
||||||
|
if find_oe_in_organization(oe_id, user.organization_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
|
||||||
|
|
||||||
|
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/unset")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)
|
||||||
88
templates/admin/gruppe_detail.html
Normal file
88
templates/admin/gruppe_detail.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{% extends "admin/layout.html" %}
|
||||||
|
{% block title %}Gruppe {{ gruppe.name }} — Tuxflotte Admin{% endblock %}
|
||||||
|
{% block admin_content %}
|
||||||
|
<p><a href="/admin/kunden/{{ organization_id }}">← Organisation</a></p>
|
||||||
|
<h2>Gruppe: {{ gruppe.name }}</h2>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Merkmale</h3>
|
||||||
|
<p><small>"Nicht festgelegt" überlässt die Entscheidung der OE-Kette bzw. dem Workspace-Standard des Geräts.</small></p>
|
||||||
|
{% if not kategorien %}
|
||||||
|
<p>Keine katalogfähigen Merkmale vorhanden.</p>
|
||||||
|
{% else %}
|
||||||
|
{% for kategorie_name, eintraege in kategorien.items() %}
|
||||||
|
<h4>{{ kategorie_name }}</h4>
|
||||||
|
<figure>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for eintrag in eintraege %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ eintrag.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if eintrag.aktiv is none %}
|
||||||
|
<span class="state-absent">nicht festgelegt</span>
|
||||||
|
{% elif eintrag.aktiv %}
|
||||||
|
<span class="state-present">aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="state-absent">inaktiv</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if eintrag.aktiv != true %}
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}/merkmale/{{ eintrag.merkmal }}/select" style="display:inline">
|
||||||
|
<button type="submit">Aktiv</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if eintrag.aktiv != false %}
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}/merkmale/{{ eintrag.merkmal }}/deselect" style="display:inline">
|
||||||
|
<button type="submit" class="secondary">Inaktiv</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if eintrag.aktiv is not none %}
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}/merkmale/{{ eintrag.merkmal }}/unset" style="display:inline">
|
||||||
|
<button type="submit" class="outline">Zurücksetzen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</figure>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Geräte in dieser Gruppe</h3>
|
||||||
|
{% if not devices %}
|
||||||
|
<p>Keine Geräte in dieser Organisation.</p>
|
||||||
|
{% else %}
|
||||||
|
<figure>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Hostname</th><th>Kennung</th><th>Mitglied</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for device in devices %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ device.hostname or "—" }}</td>
|
||||||
|
<td><code>{{ device.device_fingerprint }}</code></td>
|
||||||
|
<td>{{ "ja" if device.id in mitglied_ids else "nein" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if device.id in mitglied_ids %}
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}/geraete/{{ device.id }}/entfernen">
|
||||||
|
<button type="submit" class="outline secondary">Entfernen</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}/geraete/{{ device.id }}/hinzufuegen">
|
||||||
|
<button type="submit" class="outline">Hinzufügen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</figure>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@ -117,4 +117,39 @@
|
|||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Gruppen</h3>
|
||||||
|
<p><small>Querschnitts-Gruppen (z.B. Lehrer/Schulleitung) — ein Gerät kann in mehreren gleichzeitig sein. Anders als OEs vorerst nur hier intern verwaltbar, siehe Konfigurationsgruppen-ADR.</small></p>
|
||||||
|
{% if not gruppen %}
|
||||||
|
<p>Keine Gruppen angelegt.</p>
|
||||||
|
{% else %}
|
||||||
|
<figure>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Name</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for gruppe in gruppen %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ gruppe.name }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}" role="button" class="outline">Verwalten</a>
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen/{{ gruppe.id }}/loeschen" style="display:inline">
|
||||||
|
<button type="submit" class="outline secondary">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</figure>
|
||||||
|
{% endif %}
|
||||||
|
<details>
|
||||||
|
<summary role="button" class="secondary outline">Neue Gruppe anlegen</summary>
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/gruppen">
|
||||||
|
<label for="gruppe_name">Name</label>
|
||||||
|
<input type="text" id="gruppe_name" name="name" required>
|
||||||
|
<button type="submit">Anlegen</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -21,6 +21,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="state-{{ eintrag.state }}">
|
<td class="state-{{ eintrag.state }}">
|
||||||
{{ "ausgewählt" if eintrag.state == "present" else "nicht ausgewählt" }}
|
{{ "ausgewählt" if eintrag.state == "present" else "nicht ausgewählt" }}
|
||||||
|
{% if eintrag.quelle and eintrag.quelle != "workspace" %}
|
||||||
|
<br><small>via {{ {"oe": "OE", "gruppe": "Gruppe", "geraet": "manuell"}[eintrag.quelle] }}</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if eintrag.state == "present" %}
|
{% if eintrag.state == "present" %}
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
{% if user %}
|
{% if user %}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/geraete">Geräte</a></li>
|
<li><a href="/geraete">Geräte</a></li>
|
||||||
|
<li><a href="/struktur">Struktur</a></li>
|
||||||
<li><a href="/installationsmedium">Installationsmedium</a></li>
|
<li><a href="/installationsmedium">Installationsmedium</a></li>
|
||||||
<li><a href="/organisation">Organisation</a></li>
|
<li><a href="/organisation">Organisation</a></li>
|
||||||
{% if user.is_superuser %}<li><a href="/admin/technik">Admin</a></li>{% endif %}
|
{% if user.is_superuser %}<li><a href="/admin/technik">Admin</a></li>{% endif %}
|
||||||
|
|||||||
@ -8,13 +8,14 @@
|
|||||||
<figure>
|
<figure>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Hostname</th><th>Kennung</th><th>Letzter Check-in</th><th>Status</th><th></th><th></th></tr>
|
<tr><th>Hostname</th><th>Kennung</th><th>OE</th><th>Letzter Check-in</th><th>Status</th><th></th><th></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for device in devices %}
|
{% for device in devices %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ device.hostname or "—" }}</td>
|
<td>{{ device.hostname or "—" }}</td>
|
||||||
<td><code>{{ device.device_fingerprint }}</code></td>
|
<td><code>{{ device.device_fingerprint }}</code></td>
|
||||||
|
<td>{{ device.oe_name or "—" }}</td>
|
||||||
<td>{{ device.agent_last_checkin or "noch nie" }}</td>
|
<td>{{ device.agent_last_checkin or "noch nie" }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if device.deprovisioned_at %}
|
{% if device.deprovisioned_at %}
|
||||||
@ -41,6 +42,16 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<form method="post" action="/geraete/{{ device.id }}/oe">
|
||||||
|
<select name="oe_id" aria-label="OE auswählen">
|
||||||
|
{% for oe in organisationseinheiten %}
|
||||||
|
<option value="{{ oe.id }}" {% if oe.id == device.oe_id %}selected{% endif %}>{{ oe.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="outline">In OE verschieben</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
<li><a href="/geraete/{{ device.id }}/loeschen">Gerät löschen</a></li>
|
<li><a href="/geraete/{{ device.id }}/loeschen">Gerät löschen</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
54
templates/struktur.html
Normal file
54
templates/struktur.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Struktur — Tuxflotte{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Struktur</h2>
|
||||||
|
<p>Organisationseinheiten (OEs) zum Verorten Ihrer Geräte — z.B. Gebäude oder Klassen. Ein Gerät gehört immer zu genau einer OE, neu aktivierte Geräte landen automatisch in "Neue Geräte".</p>
|
||||||
|
|
||||||
|
{% macro knoten(oe, oes) %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ oe.name }}</strong>{% if oe.ist_standard %} <small>(Standard)</small>{% endif %}
|
||||||
|
<small>
|
||||||
|
· <a href="/struktur/{{ oe.id }}/merkmale">Merkmale bearbeiten</a>
|
||||||
|
{% if not oe.ist_standard %}
|
||||||
|
· <a href="/struktur/{{ oe.id }}/umbenennen">Umbenennen</a>
|
||||||
|
· <a href="/struktur/{{ oe.id }}/loeschen">Löschen</a>
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
<details>
|
||||||
|
<summary role="button" class="secondary outline">Unter-OE anlegen</summary>
|
||||||
|
<form method="post" action="/struktur">
|
||||||
|
<input type="hidden" name="parent_id" value="{{ oe.id }}">
|
||||||
|
<input type="text" name="name" placeholder="Name" required>
|
||||||
|
<button type="submit">Anlegen</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
{% set kinder = oes | selectattr("parent_id", "equalto", oe.id) | list %}
|
||||||
|
{% if kinder %}
|
||||||
|
<ul>
|
||||||
|
{% for kind in kinder %}
|
||||||
|
{{ knoten(kind, oes) }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% set wurzeln = organisationseinheiten | selectattr("parent_id", "equalto", none) | list %}
|
||||||
|
{% if not wurzeln %}
|
||||||
|
<p>Keine OEs gefunden.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for wurzel in wurzeln %}
|
||||||
|
{{ knoten(wurzel, organisationseinheiten) }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary role="button" class="secondary outline">Neue OE auf oberster Ebene anlegen</summary>
|
||||||
|
<form method="post" action="/struktur">
|
||||||
|
<input type="text" name="name" placeholder="Name" required>
|
||||||
|
<button type="submit">Anlegen</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
{% endblock %}
|
||||||
17
templates/struktur_loeschen.html
Normal file
17
templates/struktur_loeschen.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}OE löschen — Tuxflotte{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<p><a href="/struktur">← Struktur</a></p>
|
||||||
|
<h2>OE löschen</h2>
|
||||||
|
|
||||||
|
{% if fehler %}
|
||||||
|
<p><mark>{{ fehler }}</mark></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>Soll die OE "{{ oe.name }}" wirklich gelöscht werden?</p>
|
||||||
|
|
||||||
|
<form method="post" action="/struktur/{{ oe.id }}/loeschen">
|
||||||
|
<button type="submit">Ja, OE löschen</button>
|
||||||
|
<a href="/struktur" role="button" class="secondary outline">Abbrechen</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
57
templates/struktur_merkmale.html
Normal file
57
templates/struktur_merkmale.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Merkmale {{ oe.name }} — Tuxflotte{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<p><a href="/struktur">← Struktur</a></p>
|
||||||
|
<h2>Merkmale: {{ oe.name }}</h2>
|
||||||
|
<p>Legt fest, was Geräte in dieser OE (und ihren Unter-OEs, sofern dort nichts Spezifischeres gilt) standardmäßig bekommen. "Nicht festgelegt" überlässt die Entscheidung der nächsten Schicht (Gruppen, dann Workspace-Standard).</p>
|
||||||
|
|
||||||
|
{% if not kategorien %}
|
||||||
|
<p>Keine katalogfähigen Merkmale vorhanden.</p>
|
||||||
|
{% else %}
|
||||||
|
{% for kategorie_name, eintraege in kategorien.items() %}
|
||||||
|
<section>
|
||||||
|
<h3>{{ kategorie_name }}</h3>
|
||||||
|
<figure>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for eintrag in eintraege %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ eintrag.name }}</strong>
|
||||||
|
{% if eintrag.description %}<br><small>{{ eintrag.description }}</small>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if eintrag.aktiv is none %}
|
||||||
|
<span class="state-absent">nicht festgelegt</span>
|
||||||
|
{% elif eintrag.aktiv %}
|
||||||
|
<span class="state-present">aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="state-absent">inaktiv</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if eintrag.aktiv != true %}
|
||||||
|
<form method="post" action="/struktur/{{ oe.id }}/merkmale/{{ eintrag.merkmal }}/select">
|
||||||
|
<button type="submit">Aktiv setzen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if eintrag.aktiv != false %}
|
||||||
|
<form method="post" action="/struktur/{{ oe.id }}/merkmale/{{ eintrag.merkmal }}/deselect">
|
||||||
|
<button type="submit" class="secondary">Inaktiv setzen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if eintrag.aktiv is not none %}
|
||||||
|
<form method="post" action="/struktur/{{ oe.id }}/merkmale/{{ eintrag.merkmal }}/unset">
|
||||||
|
<button type="submit" class="outline">Zurücksetzen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
12
templates/struktur_umbenennen.html
Normal file
12
templates/struktur_umbenennen.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}OE umbenennen — Tuxflotte{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<p><a href="/struktur">← Struktur</a></p>
|
||||||
|
<h2>OE umbenennen</h2>
|
||||||
|
|
||||||
|
<form method="post" action="/struktur/{{ oe.id }}/umbenennen">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{ oe.name }}" required>
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user