diff --git a/app/cis.py b/app/cis.py index 45621fa..de4db86 100644 --- a/app/cis.py +++ b/app/cis.py @@ -297,6 +297,19 @@ class CISClient: matrikel=mail.split("@")[0] if "@" in mail else "")) 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 ---------- def verbands_for(self, kz: str, sem: str) -> list[str]: fallback = ["V", "B", "G", "A", "C", "D", "E", "F"] diff --git a/app/main.py b/app/main.py index e866994..1647798 100644 --- a/app/main.py +++ b/app/main.py @@ -44,6 +44,23 @@ def _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) def home(request: Request): 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"]) s = next((x for x in meta["semesters"] if x["label"] == semester), meta["semesters"][0]) person = None + sems: list[int] = [] if kind == "student": uid = uid or OWN_UID person = client.person(uid) events = client.student_events(uid, s["begin"], s["ende"]) else: 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" events = client.stg_events(kz, sem, ver, grp, s["begin"], s["ende"]) # 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 "", "kz": kz or "", "sem": sem or "", + "sems": sems, "ver": ver or "", "grp": grp or "", "stg": stg, @@ -144,6 +165,9 @@ def liste(request: Request, sem: str = Query("1"), ver: str = Query("V")): 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) stg = next((x for x in meta["stgs"] if x["kz"] == kz), None) verbands = client.verbands_for(kz, sem) @@ -155,6 +179,7 @@ def liste(request: Request, "meta": meta, "kz": kz, "sem": sem, + "sems": sems, "ver": ver, "verbands": verbands, "stg": stg, @@ -214,6 +239,9 @@ def fotos_page(request: Request, sem: str = Query("1"), ver: str = Query("V")): 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) verbands = client.verbands_for(kz, sem) wall = _photo_wall(kz, sem, ver) @@ -222,6 +250,7 @@ def fotos_page(request: Request, "meta": meta, "kz": kz, "sem": sem, + "sems": sems, "ver": ver, "verbands": verbands, "stg": stg, @@ -248,32 +277,32 @@ def fotos_suche(request: Request, sem: str = Form("1"), ver: str = Form("V"), 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() if not data: - return templates.TemplateResponse(request, "fotos_result.html", { - "request": request, "error": "Leere Datei hochgeladen.", - "kz": kz, "sem": sem, "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, - }, status_code=400) + return templates.TemplateResponse(request, "fotos_result.html", + {**base_ctx, "error": "Leere Datei hochgeladen."}, + status_code=400) if len(data) > MAX_UPLOAD: - return templates.TemplateResponse(request, "fotos_result.html", { - "request": request, "error": "Datei zu groß (max. 8 MB).", - "kz": kz, "sem": sem, "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, - }, status_code=400) + return templates.TemplateResponse(request, "fotos_result.html", + {**base_ctx, "error": "Datei zu groß (max. 8 MB)."}, + status_code=400) emb = engine.embed(data) if emb is None: - return templates.TemplateResponse(request, "fotos_result.html", { - "request": request, "error": "Kein Gesicht im hochgeladenen Bild gefunden.", - "kz": kz, "sem": sem, "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, - }, status_code=422) + return templates.TemplateResponse(request, "fotos_result.html", + {**base_ctx, "error": "Kein Gesicht im hochgeladenen Bild gefunden."}, + status_code=422) wall = _photo_wall(kz, sem, ver) results = [] t0 = time.time() @@ -291,18 +320,12 @@ def fotos_suche(request: Request, results.append({**p, "sim": round(sim, 4), "img": f"/fotos/img/{pid}"}) results.sort(key=lambda r: r["sim"], reverse=True) - meta = _meta() return templates.TemplateResponse(request, "fotos_result.html", { - "request": request, + **base_ctx, "error": None, "took": round(time.time() - t0, 1), "query": {"kz": kz, "sem": sem, "ver": ver}, - "kz": kz, "sem": sem, "ver": ver, "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, }) diff --git a/app/static/app.js b/app/static/app.js index 738a81b..e6b202d 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -380,7 +380,32 @@ function decodeMat() { 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 => + ``).join(""); + if (current && !arr.includes(Number(current))) sel.selectedIndex = 0; + } + } catch (e) { /* Keep static options */ } + reloadVerbands(); +} + async function reloadVerbands() { const sel = $("#ver-select"); if (!sel) return; diff --git a/app/templates/fotos.html b/app/templates/fotos.html index 3379dda..3a81995 100644 --- a/app/templates/fotos.html +++ b/app/templates/fotos.html @@ -19,7 +19,7 @@