Optionen-Eingabe auf OE-/Gruppen-Ebene + Text-/Passwort-Feldtypen

Bisher konnten Merkmal-Optionen nur pro Gerät gesetzt werden (nur
Checkboxen, siehe alte OPTIONEN_FELDER in geraete.py) - für die neuen
AD/Fileserver/Update-Proxy-Merkmale (admin/ansible-content) muss das aber
auf OE-/Gruppen-Ebene passieren (Entscheidung vom 09.09.2026: Geräte-
Einzel-Konfiguration ist nicht der vorgesehene Weg).

Neues gemeinsames Modul optionen_felder.py (ersetzt die bisher lokale,
duplizierte Map in geraete.py): Feld-Definitionen mit type
(checkbox/text/password), genutzt von geraete.py, struktur.py (neu) und
gruppen.py (neu). parse_optionen_form() ersetzt die bisherige reine
Checkbox-Parsing-Logik - Text-/Passwort-Felder werden nur übernommen,
wenn tatsächlich etwas eingegeben wurde (leer = "nicht ändern", siehe
merge_optionen() in admin/provisioning-server).

Templates (auftragskatalog.html, struktur_merkmale.html neu,
gruppe_detail.html neu) rendern je nach type ein Checkbox- oder Text-/
Passwort-Eingabefeld, mit Hinweis-Platzhalter, wenn für ein Passwort-Feld
bereits ein Wert hinterlegt ist (aus der maskierten optionen-Anzeige des
Servers, nie der Klartext).

Gehört zusammen mit admin/provisioning-server (encrypt/decrypt/merge-
Logik, erweiterte OE-/Gruppen-Select-Endpunkte) und admin/ansible-content
(die drei neuen Rollen, die diese Optionen konsumieren).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-10 09:39:02 +02:00
1 parent 3b98d682d0
commit 2618e5e468
7 files changed
+109 -18

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
# Gemeinsame Feld-Definitionen für Merkmal-Optionen (ADR-0010), verwendet
# von geraete.py (Geräte-Ebene), struktur.py (OE-Ebene) und gruppen.py
# (Gruppen-Ebene) - bisher lag das nur lokal+dupliziert in geraete.py und
# kannte nur Checkboxen (siehe browser-brave/standardbrowser). Seit den
# AD/OIDC/Fileserver-Merkmalen (10.09.2026) gibt es auch Text- und
# Passwort-Felder - "type" steuert den HTML-Input-Typ und die
# Parsing-Logik in parse_optionen_form().
#
# Passwort-Felder werden nie mit ihrem aktuellen Wert vorausgefüllt (siehe
# Templates) und nur übernommen, wenn tatsächlich etwas eingegeben wurde -
# ein leer gelassenes Passwort-Feld beim Bearbeiten überschreibt also
# nicht versehentlich ein bereits gesetztes Passwort mit nichts.
OPTIONEN_FELDER = {
"browser-brave": [
{"key": "standardbrowser", "label": "Als Standardbrowser festlegen", "type": "checkbox"},
],
"ad-anbindung": [
{"key": "ad_domain", "label": "AD-Domäne (z.B. schule.local)", "type": "text"},
{"key": "ad_join_user", "label": "Beitritts-Benutzer (z.B. Administrator)", "type": "text"},
{"key": "ad_join_passwort", "label": "Beitritts-Passwort", "type": "password"},
],
"fileserver": [
{"key": "fileserver_adresse", "label": "Server-Adresse (Host oder IP)", "type": "text"},
{"key": "fileserver_protokoll", "label": "Protokoll (smb oder webdav)", "type": "text"},
{"key": "fileserver_home_freigabe", "label": "Freigabename für $HOME (leer = kein Home-Mount)", "type": "text"},
{"key": "fileserver_user", "label": "Zugangs-Benutzer (leer = Kerberos/AD-Anmeldung verwenden)", "type": "text"},
{"key": "fileserver_passwort", "label": "Zugangs-Passwort (leer = Kerberos/AD-Anmeldung verwenden)", "type": "password"},
],
"update-proxy": [
{"key": "proxy_adresse", "label": "Proxy-Gerät (Host oder IP, Port 3142)", "type": "text"},
],
}
def parse_optionen_form(form, merkmal_key: str) -> dict:
"""Liest die zu einem Merkmal gehörenden optionen-Felder aus einem
FastAPI-Formular gemäß OPTIONEN_FELDER - Checkboxen werden zu bool,
Text-/Passwort-Felder nur übernommen, wenn tatsächlich etwas eingegeben
wurde (leer = "nicht ändern", siehe Modul-Kommentar oben)."""
ergebnis = {}
for feld in OPTIONEN_FELDER.get(merkmal_key, []):
roh = form.get(f"optionen.{feld['key']}")
if feld["type"] == "checkbox":
ergebnis[feld["key"]] = roh == "on"
elif roh:
ergebnis[feld["key"]] = roh
return ergebnis
+2 -13
View File
@@ -3,21 +3,13 @@ from fastapi.responses import RedirectResponse
from anode_client import anode_request
from auth import get_current_user
from optionen_felder import OPTIONEN_FELDER, parse_optionen_form
from routers.gruppen import fetch_gruppen
from templating import templates
router = APIRouter()
# Merkmale, für die die UI eine zusätzliche Auswahl-Option anbietet (siehe
# ADR-0010 optionen-Mechanismus). Bewusst hier als kleine, explizite Map
# statt eines generischen Options-Schemas - nur ein Fall existiert aktuell.
OPTIONEN_FELDER = {
"browser-brave": [
{"key": "standardbrowser", "label": "Als Standardbrowser festlegen"},
],
}
def find_device_in_organization(device_id: str, organization_id: str) -> dict | None:
"""
@@ -177,10 +169,7 @@ async def auftrag_select(
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
form = await request.form()
optionen = {
feld["key"]: (form.get(f"optionen.{feld['key']}") == "on")
for feld in OPTIONEN_FELDER.get(merkmal_key, [])
}
optionen = parse_optionen_form(form, merkmal_key)
anode_request(
"POST",
+10 -2
View File
@@ -3,6 +3,7 @@ from fastapi.responses import RedirectResponse
from anode_client import anode_request
from auth import get_current_user
from optionen_felder import OPTIONEN_FELDER, parse_optionen_form
from templating import templates
@@ -121,16 +122,23 @@ def gruppe_detail(request: Request, gruppe_id: str, user=Depends(get_current_use
"kategorien": kategorien,
"devices": devices,
"mitglied_ids": mitglied_ids,
"optionen_felder": OPTIONEN_FELDER,
},
)
@router.post("/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select")
def gruppe_merkmal_select(gruppe_id: str, merkmal_key: str, user=Depends(get_current_user)):
async def gruppe_merkmal_select(request: Request, 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")
form = await request.form()
optionen = parse_optionen_form(form, merkmal_key)
anode_request(
"POST", f"/api/v1/gruppen/{gruppe_id}/merkmale/{merkmal_key}/select",
json={"optionen": optionen},
)
return RedirectResponse(f"/gruppen/{gruppe_id}", status_code=303)
+12 -3
View File
@@ -3,6 +3,7 @@ from fastapi.responses import RedirectResponse
from anode_client import anode_request
from auth import get_current_user
from optionen_felder import OPTIONEN_FELDER, parse_optionen_form
from templating import templates
@@ -122,16 +123,24 @@ def oe_merkmale_ansicht(request: Request, oe_id: str, user=Depends(get_current_u
kategorien.setdefault(kategorie_name, []).append(eintrag)
return templates.TemplateResponse(
request, "struktur_merkmale.html", {"user": user, "oe": oe, "kategorien": kategorien}
request,
"struktur_merkmale.html",
{"user": user, "oe": oe, "kategorien": kategorien, "optionen_felder": OPTIONEN_FELDER},
)
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/select")
def oe_merkmal_select(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
async def oe_merkmal_select(request: Request, 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")
form = await request.form()
optionen = parse_optionen_form(form, merkmal_key)
anode_request(
"POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/select",
json={"optionen": optionen},
)
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)
+5
View File
@@ -56,8 +56,13 @@
<div class="optionen">
{% for feld in optionen_felder[eintrag.merkmal] %}
<label>
{% if feld.type == "checkbox" %}
<input type="checkbox" name="optionen.{{ feld.key }}">
{{ feld.label }}
{% else %}
{{ feld.label }}
<input type="{{ feld.type }}" name="optionen.{{ feld.key }}" autocomplete="off">
{% endif %}
</label>
{% endfor %}
</div>
+16
View File
@@ -33,6 +33,22 @@
<td>
{% if eintrag.aktiv != true %}
<form method="post" action="/gruppen/{{ gruppe.id }}/merkmale/{{ eintrag.merkmal }}/select">
{% if optionen_felder.get(eintrag.merkmal) %}
<div class="optionen">
{% for feld in optionen_felder[eintrag.merkmal] %}
<label>
{% if feld.type == "checkbox" %}
<input type="checkbox" name="optionen.{{ feld.key }}">
{{ feld.label }}
{% else %}
{{ feld.label }}
<input type="{{ feld.type }}" name="optionen.{{ feld.key }}" autocomplete="off"
{% if eintrag.optionen.get(feld.key) %}placeholder="(bereits gesetzt - leer lassen zum Beibehalten)"{% endif %}>
{% endif %}
</label>
{% endfor %}
</div>
{% endif %}
<button type="submit">Aktiv setzen</button>
</form>
{% endif %}
+16
View File
@@ -32,6 +32,22 @@
<td>
{% if eintrag.aktiv != true %}
<form method="post" action="/struktur/{{ oe.id }}/merkmale/{{ eintrag.merkmal }}/select">
{% if optionen_felder.get(eintrag.merkmal) %}
<div class="optionen">
{% for feld in optionen_felder[eintrag.merkmal] %}
<label>
{% if feld.type == "checkbox" %}
<input type="checkbox" name="optionen.{{ feld.key }}">
{{ feld.label }}
{% else %}
{{ feld.label }}
<input type="{{ feld.type }}" name="optionen.{{ feld.key }}" autocomplete="off"
{% if eintrag.optionen.get(feld.key) %}placeholder="(bereits gesetzt - leer lassen zum Beibehalten)"{% endif %}>
{% endif %}
</label>
{% endfor %}
</div>
{% endif %}
<button type="submit">Aktiv setzen</button>
</form>
{% endif %}