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,
|
||||
installationsmedium,
|
||||
organisation,
|
||||
struktur,
|
||||
)
|
||||
|
||||
|
||||
@ -19,6 +20,7 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
app.include_router(auth.router)
|
||||
app.include_router(geraete.router)
|
||||
app.include_router(organisation.router)
|
||||
app.include_router(struktur.router)
|
||||
app.include_router(installationsmedium.router)
|
||||
app.include_router(admin_technik.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 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 = 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(
|
||||
request,
|
||||
"admin/kunden_detail.html",
|
||||
@ -72,10 +75,104 @@ def detail(request: Request, organization_id: str, user: dict = Depends(require_
|
||||
"bereitstellungsvorlagen": bereitstellungsvorlagen,
|
||||
"workspaces": workspaces,
|
||||
"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")
|
||||
def bereitstellungsvorlage_anlegen(
|
||||
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 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")
|
||||
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(
|
||||
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")
|
||||
|
||||
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>
|
||||
</details>
|
||||
</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 %}
|
||||
|
||||
@ -21,6 +21,9 @@
|
||||
</td>
|
||||
<td class="state-{{ eintrag.state }}">
|
||||
{{ "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>
|
||||
{% if eintrag.state == "present" %}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
{% if user %}
|
||||
<ul>
|
||||
<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="/organisation">Organisation</a></li>
|
||||
{% if user.is_superuser %}<li><a href="/admin/technik">Admin</a></li>{% endif %}
|
||||
|
||||
@ -8,13 +8,14 @@
|
||||
<figure>
|
||||
<table>
|
||||
<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>
|
||||
<tbody>
|
||||
{% for device in devices %}
|
||||
<tr>
|
||||
<td>{{ device.hostname or "—" }}</td>
|
||||
<td><code>{{ device.device_fingerprint }}</code></td>
|
||||
<td>{{ device.oe_name or "—" }}</td>
|
||||
<td>{{ device.agent_last_checkin or "noch nie" }}</td>
|
||||
<td>
|
||||
{% if device.deprovisioned_at %}
|
||||
@ -41,6 +42,16 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</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>
|
||||
</ul>
|
||||
</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