Einstieg Schritt 6: Unterrichtssteuerung (Veyon) am Standort/Zweck einschalten

Raum bleibt der Standort/Zweck (Schlüssel, Raumliste je OE). Auf der
Einstellungsseite des Standorts: Veyon einschalten (ohne Häkchen) und
Lehrergerät(e) ankreuzen; die kommen in die Gruppe "Lehrergeräte" (eine je
Organisation, Häkchen veyon_lehrer gesetzt), alle anderen dort sind
Schülergeräte. Ausschalten nimmt die Lehrergeräte aus der Gruppe. Hinweis
auf die alte Falle (Häkchen am Standort), Speichern behebt sie.
Geräteseite: "Als Lehrergerät festlegen" / "Kein Lehrergerät mehr".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-25 16:44:35 +01:00
1 parent fed0e9e48a
commit 2d1b45e973
6 files changed
+234 -2

No files matched your search

+19
View File
@@ -9,6 +9,7 @@ from einladungen import PERSONALISIERUNG_STATUS, personalisierung_einschalten, p
from routers.gruppen import fetch_gruppen
from routers.personalisierung import _passwort_hash, einladung_senden
from templating import templates
from unterrichtssteuerung import lehrer_ids, lehrergeraet_setzen
router = APIRouter()
@@ -215,6 +216,12 @@ def geraete_info(
"email_domaenen": org_result["organization"].get("email_domaenen", []) if org_result.get("success") else [],
"einladung": einladung,
"neues_passwort": neues_passwort,
# Schritt 6 (25.10.2026): Lehrergerät direkt schaltbar.
"unterricht": {
"lehrer": device["id"] in lehrer_ids(user.organization_id, [device]),
"veyon_an": any(e.get("merkmal") == "veyon" and e.get("state") == "present" for e in katalog),
} if any(e.get("merkmal") == "veyon" for e in katalog) else None,
"unterricht_fehler": request.query_params.get("unterricht_fehler"),
},
status_code=status_code,
)
@@ -366,6 +373,18 @@ def personalisierung_zurueckziehen(device_id: str, user: dict = Depends(get_curr
return RedirectResponse(f"/geraete/{device_id}#personalisierung", status_code=303)
@router.post("/geraete/{device_id}/lehrergeraet")
def lehrergeraet(device_id: str, lehrer: 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.")
fehler = lehrergeraet_setzen(str(user.organization_id), device_id, lehrer == "1")
ziel = f"/geraete/{device_id}"
if fehler:
ziel += "?" + urlencode({"unterricht_fehler": fehler})
return RedirectResponse(ziel + "#unterricht", status_code=303)
@router.post("/geraete/{device_id}/eigene-gruppe")
def eigene_gruppe_anlegen(device_id: str, user: dict = Depends(get_current_user)):
"""
+77 -2
View File
@@ -8,6 +8,7 @@ from einladungen import PERSONALISIERUNG_MERKMAL, PERSONALISIERUNG_STATUS, einla
from optionen_felder import OPTIONEN_FELDER, parse_optionen_form
from routers.workspace import creator_frei, zuweisbare_workspaces
from templating import templates
from unterrichtssteuerung import lehrer_gruppe, lehrer_ids, lehrergeraet_setzen, veyon_eintrag
import workspace_creator
@@ -175,6 +176,24 @@ def oe_merkmale_ansicht(request: Request, oe_id: str, user=Depends(get_current_u
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 []
# Schritt 6 (25.10.2026): Unterrichtssteuerung für die Geräte genau
# dieses Standorts/Zwecks (Schlüssel und Raumliste gelten je OE, nicht
# für die darunter).
veyon = veyon_eintrag(merkmale)
unterricht = None
if veyon is not None:
hier = [d for d in devices if d.get("oe_id") == oe_id]
unterricht = {
"an": veyon.get("aktiv") is True,
"devices": hier,
"lehrer": lehrer_ids(user.organization_id, hier),
# Alte Falle (B18): Häkchen am Standort macht jedes Gerät zum Lehrer.
"falle": veyon.get("aktiv") is True and bool((veyon.get("optionen") or {}).get("veyon_lehrer")),
}
# Schritt 7 (25.10.2026): wie bei Gruppen, für die Geräte hier und
# in den Standorten/Zwecken darunter.
einladungen = None
@@ -186,8 +205,6 @@ def oe_merkmale_ansicht(request: Request, oe_id: str, user=Depends(get_current_u
if not neu:
break
unten |= neu
devices_result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/devices")
devices = devices_result.get("devices", []) if devices_result.get("success") else []
einladungen = einladungs_zeilen([d for d in devices if d.get("oe_id") in unten])
return templates.TemplateResponse(
@@ -202,10 +219,68 @@ def oe_merkmale_ansicht(request: Request, oe_id: str, user=Depends(get_current_u
"email_domaenen": email_domaenen(user.organization_id) if einladungen is not None else [],
"personalisierung_status": PERSONALISIERUNG_STATUS,
"einladung_zurueck": f"/struktur/{oe_id}/merkmale",
"unterricht": unterricht,
"hinweis": request.query_params.get("hinweis"),
"fehler": request.query_params.get("fehler"),
},
)
def _unterricht_zurueck(oe_id: str, **meldung) -> RedirectResponse:
return RedirectResponse(f"/struktur/{oe_id}/merkmale?{urlencode(meldung)}#unterricht", status_code=303)
@router.post("/struktur/{oe_id}/unterrichtssteuerung")
async def unterrichtssteuerung_speichern(request: Request, oe_id: str, user=Depends(get_current_user)):
"""Schritt 6: Veyon am Standort/Zweck einschalten (ohne Häkchen) und die
gewählten Geräte hier zu Lehrergeräten machen, die übrigen hier nicht."""
organization_id = str(user.organization_id)
if find_oe_in_organization(oe_id, organization_id) is None:
raise HTTPException(status_code=404, detail="Standort/Zweck wurde nicht gefunden.")
form = await request.form()
gewaehlt = set(form.getlist("lehrer"))
result = anode_request(
"POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/veyon/select",
json={"optionen": {"veyon_lehrer": False}},
)
if not result.get("success"):
return _unterricht_zurueck(oe_id, fehler=result.get("message", "Veyon ließ sich nicht einschalten."))
devices_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/devices")
hier = [d for d in devices_result.get("devices", []) if d.get("oe_id") == oe_id]
if gewaehlt:
_, fehler = lehrer_gruppe(organization_id)
if fehler:
return _unterricht_zurueck(oe_id, fehler=fehler)
for d in hier:
fehler = lehrergeraet_setzen(organization_id, d["id"], d["id"] in gewaehlt)
if fehler:
return _unterricht_zurueck(oe_id, fehler=fehler)
hinweis = "Unterrichtssteuerung gespeichert."
if not gewaehlt:
hinweis += " Noch kein Lehrergerät gewählt."
return _unterricht_zurueck(oe_id, hinweis=hinweis)
@router.post("/struktur/{oe_id}/unterrichtssteuerung/aus")
def unterrichtssteuerung_aus(oe_id: str, user=Depends(get_current_user)):
"""Veyon am Standort/Zweck zurücksetzen und die Lehrergeräte hier aus der
Gruppe nehmen; sonst behielten sie Veyon über die Gruppe."""
organization_id = str(user.organization_id)
if find_oe_in_organization(oe_id, organization_id) is None:
raise HTTPException(status_code=404, detail="Standort/Zweck wurde nicht gefunden.")
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/veyon/unset")
devices_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/devices")
for d in devices_result.get("devices", []):
if d.get("oe_id") == oe_id:
lehrergeraet_setzen(organization_id, d["id"], False)
return _unterricht_zurueck(oe_id, hinweis="Unterrichtssteuerung ausgeschaltet.")
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/select")
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:
+18
View File
@@ -47,6 +47,24 @@
{% endif %}
</section>
{% if unterricht %}
<section id="unterricht">
<h3>Unterrichtssteuerung (Veyon)</h3>
{% if unterricht_fehler %}<p><mark>{{ unterricht_fehler }}</mark></p>{% endif %}
{% if unterricht.lehrer %}
<p>Lehrergerät: Hier läuft der Veyon Master für alle Geräte von „{{ device.oe_name }}“.</p>
{% elif unterricht.veyon_an %}
<p>Schülergerät in „{{ device.oe_name }}“.</p>
{% else %}
<p><small>Für „{{ device.oe_name }}“ ist die Unterrichtssteuerung nicht eingeschaltet (<a href="/struktur/{{ device.oe_id }}/merkmale#unterricht">einschalten</a>).</small></p>
{% endif %}
<form method="post" action="/geraete/{{ device.id }}/lehrergeraet">
<input type="hidden" name="lehrer" value="{{ '0' if unterricht.lehrer else '1' }}">
<button type="submit" class="outline">{% if unterricht.lehrer %}Kein Lehrergerät mehr{% else %}Als Lehrergerät festlegen{% endif %}</button>
</form>
</section>
{% endif %}
{% if personalisierungen is defined %}
<section id="personalisierung">
<h3>Personalisierung</h3>
+1
View File
@@ -25,6 +25,7 @@
{% if oe.anzahl_geraete is defined %}<small> · {{ oe.anzahl_geraete }} Gerät(e){% if oe.distros %}: {{ oe.distros | join(", ") }}{% endif %}</small>{% endif %}
<small>
· <a href="/struktur/{{ oe.id }}/merkmale">Einstellungen</a>
· <a href="/struktur/{{ oe.id }}/merkmale#unterricht">Unterrichtssteuerung</a>
{% if not oe.ist_standard %}
· <a href="/struktur/{{ oe.id }}/umbenennen">Umbenennen</a>
· <a href="/struktur/{{ oe.id }}/loeschen">Löschen</a>
+40
View File
@@ -5,6 +5,46 @@
<h2>Merkmale: {{ oe.name }}</h2>
<p>Legt fest, was Geräte hier (und darunter, sofern dort nichts anderes gilt) standardmäßig bekommen. "Nicht festgelegt" überlässt die Entscheidung der nächsten Schicht (Gruppen, dann Workspace-Standard).</p>
{% if fehler %}<p><mark>{{ fehler }}</mark></p>{% endif %}
{% if hinweis %}<p><ins>{{ hinweis }}</ins></p>{% endif %}
{% if unterricht %}
<section id="unterricht">
<h3>Unterrichtssteuerung (Veyon)</h3>
<p><small>Am Lehrergerät sieht die Lehrkraft mit dem Veyon Master die Bildschirme aller anderen Geräte von „{{ oe.name }}“ und kann sie steuern. Alle Geräte müssen im selben Netz sein. Standorte/Zwecke darunter bekommen Veyon ebenfalls, sind aber jeweils ein eigener Raum mit eigenem Lehrergerät.</small></p>
{% if unterricht.falle %}
<p><mark>Hier ist „Lehrergerät“ für den ganzen Standort/Zweck angehakt, damit ist jedes Gerät Lehrergerät. Speichern unten behebt das.</mark></p>
{% endif %}
{% if not unterricht.devices %}
<p>Hier sind noch keine Geräte. Verschiebe sie auf der Seite des Geräts hierher.</p>
{% endif %}
{% if unterricht.an or unterricht.devices %}
<form method="post" action="/struktur/{{ oe.id }}/unterrichtssteuerung">
{% if unterricht.devices %}
<fieldset>
<legend>{% if unterricht.an %}Lehrergerät(e){% else %}Welches ist das Lehrergerät?{% endif %}</legend>
{% for d in unterricht.devices %}
<label>
<input type="checkbox" name="lehrer" value="{{ d.id }}" {% if d.id in unterricht.lehrer %}checked{% endif %}>
{{ d.hostname or d.device_fingerprint }}
<small>{% if unterricht.an %}{{ "Lehrergerät" if d.id in unterricht.lehrer else "Schülergerät" }}{% endif %}</small>
</label>
{% endfor %}
<small>Alle nicht angehakten Geräte hier sind Schülergeräte.</small>
</fieldset>
{% endif %}
<button type="submit">{% if unterricht.an %}Speichern{% else %}Unterrichtssteuerung einschalten{% endif %}</button>
</form>
{% endif %}
{% if unterricht.an %}
<form method="post" action="/struktur/{{ oe.id }}/unterrichtssteuerung/aus">
<button type="submit" class="secondary outline">Unterrichtssteuerung ausschalten</button>
</form>
{% endif %}
</section>
{% endif %}
<h3>Alle Merkmale</h3>
{% if not kategorien %}
<p>Keine katalogfähigen Merkmale vorhanden.</p>
{% else %}
+79
View File
@@ -0,0 +1,79 @@
"""
Unterrichtssteuerung (Veyon) ohne Gruppen-Handarbeit (Einstieg vereinfachen,
Schritt 6, 25.10.2026). Der Raum bleibt der Standort/Zweck: dort hängen
Schlüssel und Raumliste (provisioning-server, VEYON_MERKMAL). Die Rolle
Lehrergerät kommt aus einer gemeinsamen Gruppe "Lehrergeräte" je
Organisation mit gesetztem Häkchen veyon_lehrer; alle anderen Geräte am
Standort sind Schülergeräte.
"""
from anode_client import anode_request
VEYON_MERKMAL = "veyon"
LEHRER_GRUPPE = "Lehrergeräte"
def _gruppe_finden(organization_id: str) -> dict | None:
result = anode_request("GET", f"/api/v1/organizations/{organization_id}/gruppen")
gruppen = result.get("gruppen", []) if result.get("success") else []
return next((g for g in gruppen if g["name"] == LEHRER_GRUPPE), None)
def lehrer_gruppe(organization_id: str) -> tuple[dict | None, str | None]:
"""Gruppe "Lehrergeräte", bei Bedarf angelegt; das Häkchen wird jedes
Mal gesetzt, falls jemand es an der Gruppe zurückgenommen hat.
Rückgabe: (gruppe, fehlermeldung)."""
gruppe = _gruppe_finden(organization_id)
if gruppe is None:
result = anode_request(
"POST", f"/api/v1/organizations/{organization_id}/gruppen", json={"name": LEHRER_GRUPPE}
)
if not result.get("success"):
return None, result.get("message") or "Die Gruppe „Lehrergeräte“ ließ sich nicht anlegen."
gruppe = result["gruppe"]
result = anode_request(
"POST", f"/api/v1/gruppen/{gruppe['id']}/merkmale/{VEYON_MERKMAL}/select",
json={"optionen": {"veyon_lehrer": True}},
)
if not result.get("success"):
return None, result.get("message") or "Veyon ließ sich an der Gruppe „Lehrergeräte“ nicht setzen."
return gruppe, None
def lehrer_ids(organization_id: str, devices: list[dict]) -> set[str]:
"""Welche dieser Geräte in der Gruppe "Lehrergeräte" sind."""
gruppe = _gruppe_finden(organization_id)
if gruppe is None:
return set()
ids = set()
for device in devices:
result = anode_request("GET", f"/api/v1/devices/{device['id']}/gruppen")
if any(g["id"] == gruppe["id"] for g in result.get("gruppen", [])):
ids.add(device["id"])
return ids
def lehrergeraet_setzen(organization_id: str, device_id: str, lehrer: bool) -> str | None:
"""Gerät in die Gruppe "Lehrergeräte" aufnehmen oder herausnehmen.
Rückgabe: Fehlermeldung oder None."""
if lehrer:
gruppe, fehler = lehrer_gruppe(organization_id)
if fehler:
return fehler
result = anode_request("POST", f"/api/v1/devices/{device_id}/gruppen/{gruppe['id']}")
else:
gruppe = _gruppe_finden(organization_id)
if gruppe is None:
return None
result = anode_request("DELETE", f"/api/v1/devices/{device_id}/gruppen/{gruppe['id']}")
if not result.get("success"):
return result.get("message") or "Das Lehrergerät ließ sich nicht ändern."
return None
def veyon_eintrag(merkmale: list[dict]) -> dict | None:
return next((e for e in merkmale if e.get("merkmal") == VEYON_MERKMAL), None)