Studien-Semester-Select: parit\u00e4ts- und umfangsgerecht (WS ungerade, SS gerade; BSc 1-6, MSc 1-4) aus CIS-Endpoint

This commit is contained in:
Nicolas Gruber
2026-10-19 12:13:51 +02:00
parent d97164cddc
commit 567bcb40ee
7 changed files with 104 additions and 43 deletions
+13
View File
@@ -297,6 +297,19 @@ class CISClient:
matrikel=mail.split("@")[0] if "@" in mail else "")) matrikel=mail.split("@")[0] if "@" in mail else ""))
return out return out
# ---------- Studien-Semester pro StG ----------
@lru_cache(maxsize=64)
def study_semesters(self, kz: str) -> list[int]:
"""Von CIS bekannte Studien-Semester eines StG (z.B. [1..6] BSc, [1..4] MSc)."""
try:
r = self.s.post(f"{self.base}/private/lvplan/lvplan_autocomplete.php",
data={"autocomplete": "getSemester", "stg_kz": kz}, timeout=30)
data = r.json()
vals = data.values() if isinstance(data, dict) else data
return sorted({int(x) for x in vals if str(x).strip().isdigit()})
except Exception:
return []
# ---------- Verbände pro StG/Semester ---------- # ---------- Verbände pro StG/Semester ----------
def verbands_for(self, kz: str, sem: str) -> list[str]: def verbands_for(self, kz: str, sem: str) -> list[str]:
fallback = ["V", "B", "G", "A", "C", "D", "E", "F"] fallback = ["V", "B", "G", "A", "C", "D", "E", "F"]
+52 -29
View File
@@ -44,6 +44,23 @@ def _meta():
return client.meta() return client.meta()
def _study_sem_options(kz: str, semester_label: str) -> list[int]:
"""Gültige Studien-Semester eines StG, gefiltert nach Plan-Semester
(Winter=ungerade, Sommer=gerade)."""
sems = client.study_semesters(kz)
if not sems:
code = next((s["code"] for s in _meta()["stgs"] if s["kz"] == kz), "") or ""
max_n = 4 if code[:1] in ("M", "L") else 6
sems = list(range(1, max_n + 1))
label = (semester_label or "").upper()
if label.startswith("WS"):
sems = [n for n in sems if n % 2 == 1]
elif label.startswith("SS"):
sems = [n for n in sems if n % 2 == 0]
sems = sorted(set(sems))
return sems or [1]
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def home(request: Request): def home(request: Request):
meta = _meta() meta = _meta()
@@ -60,13 +77,16 @@ def _plan_common(request, kind, uid, kz, sem, ver, grp, semester, week="", tab="
semester = semester or current_semester(meta["semesters"]) semester = semester or current_semester(meta["semesters"])
s = next((x for x in meta["semesters"] if x["label"] == semester), meta["semesters"][0]) s = next((x for x in meta["semesters"] if x["label"] == semester), meta["semesters"][0])
person = None person = None
sems: list[int] = []
if kind == "student": if kind == "student":
uid = uid or OWN_UID uid = uid or OWN_UID
person = client.person(uid) person = client.person(uid)
events = client.student_events(uid, s["begin"], s["ende"]) events = client.student_events(uid, s["begin"], s["ende"])
else: else:
kz = kz or "640" kz = kz or "640"
sem = sem or "1" sems = _study_sem_options(kz, semester)
if sem not in [str(n) for n in sems]:
sem = str(sems[0])
ver = ver or "V" ver = ver or "V"
events = client.stg_events(kz, sem, ver, grp, s["begin"], s["ende"]) events = client.stg_events(kz, sem, ver, grp, s["begin"], s["ende"])
# initial week: explicit param, today if inside semester, else first event week # initial week: explicit param, today if inside semester, else first event week
@@ -93,6 +113,7 @@ def _plan_common(request, kind, uid, kz, sem, ver, grp, semester, week="", tab="
"uid": uid or "", "uid": uid or "",
"kz": kz or "", "kz": kz or "",
"sem": sem or "", "sem": sem or "",
"sems": sems,
"ver": ver or "", "ver": ver or "",
"grp": grp or "", "grp": grp or "",
"stg": stg, "stg": stg,
@@ -144,6 +165,9 @@ def liste(request: Request,
sem: str = Query("1"), sem: str = Query("1"),
ver: str = Query("V")): ver: str = Query("V")):
meta = _meta() meta = _meta()
sems = _study_sem_options(kz, current_semester(meta["semesters"]))
if sem not in [str(n) for n in sems]:
sem = str(sems[0])
students = client.namensliste(kz, sem, ver) students = client.namensliste(kz, sem, ver)
stg = next((x for x in meta["stgs"] if x["kz"] == kz), None) stg = next((x for x in meta["stgs"] if x["kz"] == kz), None)
verbands = client.verbands_for(kz, sem) verbands = client.verbands_for(kz, sem)
@@ -155,6 +179,7 @@ def liste(request: Request,
"meta": meta, "meta": meta,
"kz": kz, "kz": kz,
"sem": sem, "sem": sem,
"sems": sems,
"ver": ver, "ver": ver,
"verbands": verbands, "verbands": verbands,
"stg": stg, "stg": stg,
@@ -214,6 +239,9 @@ def fotos_page(request: Request,
sem: str = Query("1"), sem: str = Query("1"),
ver: str = Query("V")): ver: str = Query("V")):
meta = _meta() meta = _meta()
sems = _study_sem_options(kz, current_semester(meta["semesters"]))
if sem not in [str(n) for n in sems]:
sem = str(sems[0])
stg = next((x for x in meta["stgs"] if x["kz"] == kz), None) stg = next((x for x in meta["stgs"] if x["kz"] == kz), None)
verbands = client.verbands_for(kz, sem) verbands = client.verbands_for(kz, sem)
wall = _photo_wall(kz, sem, ver) wall = _photo_wall(kz, sem, ver)
@@ -222,6 +250,7 @@ def fotos_page(request: Request,
"meta": meta, "meta": meta,
"kz": kz, "kz": kz,
"sem": sem, "sem": sem,
"sems": sems,
"ver": ver, "ver": ver,
"verbands": verbands, "verbands": verbands,
"stg": stg, "stg": stg,
@@ -248,32 +277,32 @@ def fotos_suche(request: Request,
sem: str = Form("1"), sem: str = Form("1"),
ver: str = Form("V"), ver: str = Form("V"),
file: UploadFile = File(...)): file: UploadFile = File(...)):
meta = _meta()
sems = _study_sem_options(kz, current_semester(meta["semesters"]))
if sem not in [str(n) for n in sems]:
sem = str(sems[0])
base_ctx = {
"request": request,
"kz": kz, "sem": sem, "sems": sems, "ver": ver,
"results": [], "meta": meta,
"stg": next((x for x in meta["stgs"] if x["kz"] == kz), None),
"verbands": client.verbands_for(kz, sem),
"own_uid": OWN_UID,
}
data = file.file.read() data = file.file.read()
if not data: if not data:
return templates.TemplateResponse(request, "fotos_result.html", { return templates.TemplateResponse(request, "fotos_result.html",
"request": request, "error": "Leere Datei hochgeladen.", {**base_ctx, "error": "Leere Datei hochgeladen."},
"kz": kz, "sem": sem, "ver": ver, "results": [], "meta": _meta(), status_code=400)
"stg": next((x for x in _meta()["stgs"] if x["kz"] == kz), None),
"verbands": client.verbands_for(kz, sem),
"own_uid": OWN_UID,
}, status_code=400)
if len(data) > MAX_UPLOAD: if len(data) > MAX_UPLOAD:
return templates.TemplateResponse(request, "fotos_result.html", { return templates.TemplateResponse(request, "fotos_result.html",
"request": request, "error": "Datei zu groß (max. 8 MB).", {**base_ctx, "error": "Datei zu groß (max. 8 MB)."},
"kz": kz, "sem": sem, "ver": ver, "results": [], "meta": _meta(), status_code=400)
"stg": next((x for x in _meta()["stgs"] if x["kz"] == kz), None),
"verbands": client.verbands_for(kz, sem),
"own_uid": OWN_UID,
}, status_code=400)
emb = engine.embed(data) emb = engine.embed(data)
if emb is None: if emb is None:
return templates.TemplateResponse(request, "fotos_result.html", { return templates.TemplateResponse(request, "fotos_result.html",
"request": request, "error": "Kein Gesicht im hochgeladenen Bild gefunden.", {**base_ctx, "error": "Kein Gesicht im hochgeladenen Bild gefunden."},
"kz": kz, "sem": sem, "ver": ver, "results": [], "meta": _meta(), status_code=422)
"stg": next((x for x in _meta()["stgs"] if x["kz"] == kz), None),
"verbands": client.verbands_for(kz, sem),
"own_uid": OWN_UID,
}, status_code=422)
wall = _photo_wall(kz, sem, ver) wall = _photo_wall(kz, sem, ver)
results = [] results = []
t0 = time.time() t0 = time.time()
@@ -291,18 +320,12 @@ def fotos_suche(request: Request,
results.append({**p, "sim": round(sim, 4), results.append({**p, "sim": round(sim, 4),
"img": f"/fotos/img/{pid}"}) "img": f"/fotos/img/{pid}"})
results.sort(key=lambda r: r["sim"], reverse=True) results.sort(key=lambda r: r["sim"], reverse=True)
meta = _meta()
return templates.TemplateResponse(request, "fotos_result.html", { return templates.TemplateResponse(request, "fotos_result.html", {
"request": request, **base_ctx,
"error": None, "error": None,
"took": round(time.time() - t0, 1), "took": round(time.time() - t0, 1),
"query": {"kz": kz, "sem": sem, "ver": ver}, "query": {"kz": kz, "sem": sem, "ver": ver},
"kz": kz, "sem": sem, "ver": ver,
"results": results[:30], "results": results[:30],
"meta": meta,
"stg": next((x for x in meta["stgs"] if x["kz"] == kz), None),
"verbands": client.verbands_for(kz, sem),
"own_uid": OWN_UID,
}) })
+26 -1
View File
@@ -380,7 +380,32 @@ function decodeMat() {
box.classList.add("show"); box.classList.add("show");
} }
/* ================= Verband-Selects dynamisch ================= */ /* ================= Seminar- + Verband-Selects dynamisch ================= */
async function reloadSemesters() {
const sel = $("#sem-select");
const stgSel = $("#stg-select");
if (!sel || !stgSel) return;
const kz = stgSel.value;
const planSem = (($('select[name="semester"]') || {}).value || "").toUpperCase();
try {
const r = await fetch(`/api/semesters_for_stg?kz=${encodeURIComponent(kz)}`);
if (!r.ok) throw new Error("bad status");
let data = await r.json();
let arr = (Array.isArray(data) ? data : Object.values(data))
.map(Number).filter(n => Number.isInteger(n) && n > 0);
if (planSem.startsWith("WS")) arr = arr.filter(n => n % 2 === 1);
else if (planSem.startsWith("SS")) arr = arr.filter(n => n % 2 === 0);
arr = [...new Set(arr)].sort((a, b) => a - b);
if (arr.length) {
const current = sel.value;
sel.innerHTML = arr.map(n =>
`<option value="${n}"${n === Number(current) ? " selected" : ""}>${n}</option>`).join("");
if (current && !arr.includes(Number(current))) sel.selectedIndex = 0;
}
} catch (e) { /* Keep static options */ }
reloadVerbands();
}
async function reloadVerbands() { async function reloadVerbands() {
const sel = $("#ver-select"); const sel = $("#ver-select");
if (!sel) return; if (!sel) return;
+1 -1
View File
@@ -19,7 +19,7 @@
<div class="field"> <div class="field">
<label>Studien-Semester</label> <label>Studien-Semester</label>
<select name="sem"> <select name="sem">
{% for n in range(1, 9) %} {% for n in sems %}
<option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option> <option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option>
{% endfor %} {% endfor %}
</select> </select>
+1 -1
View File
@@ -18,7 +18,7 @@
<div class="field"> <div class="field">
<label>Studien-Semester</label> <label>Studien-Semester</label>
<select name="sem"> <select name="sem">
{% for n in range(1, 9) %} {% for n in sems %}
<option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option> <option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option>
{% endfor %} {% endfor %}
</select> </select>
+5 -5
View File
@@ -18,11 +18,11 @@
</div> </div>
<div class="field"> <div class="field">
<label>Studien-Semester</label> <label>Studien-Semester</label>
<select name="sem" id="sem-select" onchange="reloadVerbands()"> <select name="sem" id="sem-select" onchange="reloadVerbands()">
{% for n in range(1, 9) %} {% for n in sems %}
<option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option> <option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label>Verband</label> <label>Verband</label>
+6 -6
View File
@@ -35,7 +35,7 @@
{% else %} {% else %}
<div class="field"> <div class="field">
<label>Studiengang</label> <label>Studiengang</label>
<select name="kz" id="stg-select" required onchange="reloadVerbands()"> <select name="kz" id="stg-select" required onchange="reloadSemesters()">
{% for s in meta.stgs %} {% for s in meta.stgs %}
<option value="{{ s.kz }}" {{ 'selected' if s.kz == kz }}>{{ s.code }} - {{ s.name }}</option> <option value="{{ s.kz }}" {{ 'selected' if s.kz == kz }}>{{ s.code }} - {{ s.name }}</option>
{% endfor %} {% endfor %}
@@ -43,11 +43,11 @@
</div> </div>
<div class="field"> <div class="field">
<label>Studien-Sem.</label> <label>Studien-Sem.</label>
<select name="sem" id="sem-select" onchange="reloadVerbands()"> <select name="sem" id="sem-select" onchange="reloadVerbands()">
{% for n in range(1, 9) %} {% for n in sems %}
<option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option> <option value="{{ n }}" {{ 'selected' if (n | string) == sem }}>{{ n }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label>Verband</label> <label>Verband</label>