diff --git a/app/cis.py b/app/cis.py index f6add81..6f64c11 100644 --- a/app/cis.py +++ b/app/cis.py @@ -58,10 +58,11 @@ class Event: class Person: uid: str name: str - stg: str - semester: str - verband: str - gruppe: str + stg: str = "" + semester: str = "" + verband: str = "" + gruppe: str = "" + kind: str = "student" # student | lektor @dataclass @@ -177,7 +178,8 @@ class CISClient: return f"{self.base}/private/lvplan/stpl_kalender.php?{q}" def _ical_student_params(self, uid: str, begin: int, ende: int) -> dict: - return {"type": "student", "pers_uid": uid, "ort_kurzbz": "", + ptype = "student" if uid.isdigit() else "lektor" + return {"type": ptype, "pers_uid": uid, "ort_kurzbz": "", "stg_kz": "", "sem": "", "ver": "", "grp": "", "gruppe_kurzbz": "", "lva": "", "begin": begin, "ende": ende, "format": "ical", "version": "2", "target": "ical"} @@ -197,25 +199,38 @@ class CISClient: return parse_ical(self._get(self._ical_url(self._ical_stg_params(kz, sem, ver, grp, begin, ende)))) def ical_raw(self, kind: str, **kw) -> str: - if kind == "student": + if kind in ("student", "lektor"): return self._get(self._ical_url(self._ical_student_params(kw["uid"], kw["begin"], kw["ende"]))) return self._get(self._ical_url(self._ical_stg_params(kw["kz"], kw["sem"], kw["ver"], kw.get("grp", ""), kw["begin"], kw["ende"]))) # ---------- person ---------- @lru_cache(maxsize=256) def person(self, uid: str) -> Optional[Person]: + uid = (uid or "").strip() + if not uid: + return None + is_student = uid.isdigit() try: - html = self._get(f"{self.base}/private/lvplan/stpl_week.php?pers_uid={uid}") + url = f"{self.base}/private/lvplan/stpl_week.php?pers_uid={uid}" + if not is_student: + url += "&type=lektor" + html = self._get(url) except requests.HTTPError: return None t = re.sub(r"<[^>]+>", " ", html) t = re.sub(r"\s+", " ", t) - m = re.search(r"Person:\s*(.+?)\s*-\s*(\d{9,10}).*?Studiengang:\s*(.+?)\s+Semester:\s*(\S+?)\s+Verband:\s*(\S+?)\s+Gruppe:", t) + if is_student: + m = re.search(r"Person:\s*(.+?)\s*-\s*(\d{9,10}).*?Studiengang:\s*(.+?)\s+Semester:\s*(\S+?)\s+Verband:\s*(\S+?)\s+Gruppe:", t) + if not m: + return None + name, u, stg, sem, ver = m.groups() + return Person(uid=u, name=name.strip(), stg=stg.strip(), + semester=sem.strip(), verband=ver.strip(), gruppe="", kind="student") + m = re.search(r"Person:\s*(.+?)\s*-\s*([A-Za-z][\w.]*)", t) if not m: return None - name, u, stg, sem, ver = m.groups() - return Person(uid=u, name=name.strip(), stg=stg.strip(), - semester=sem.strip(), verband=ver.strip(), gruppe="") + name, u = m.groups() + return Person(uid=u, name=name.strip(), kind="lektor") # ---------- person_id + photos ---------- def person_id_for(self, uid: str) -> str: @@ -416,7 +431,10 @@ class CISClient: def detail(self, kind: str, datum: str, stunde: int, uid: str = "", kz: str = "", sem: str = "", ver: str = "", grp: str = "", ort: str = "") -> dict: - if kind == "student": + if kind == "lektor": + url = (f"{self.base}/private/lvplan/stpl_detail.php?type=lektor&datum={datum}" + f"&stunde={stunde}&pers_uid={uid}&stg_kz=&sem=&ver=&grp=&ort_kurzbz={ort}") + elif kind == "student": url = (f"{self.base}/private/lvplan/stpl_detail.php?type=student&datum={datum}" f"&stunde={stunde}&pers_uid={uid}&stg_kz={kz}&sem={sem}&ver={ver}" f"&grp= {grp}&ort_kurzbz={ort}") diff --git a/app/main.py b/app/main.py index 12da241..c2f4d5a 100644 --- a/app/main.py +++ b/app/main.py @@ -133,7 +133,7 @@ def _plan_common(request, kind, uid, kz, sem, ver, grp, semester, week="", tab=" @app.get("/plan", response_class=HTMLResponse) def plan(request: Request, - uid: str = Query("", description="Matrikelnummer"), + uid: str = Query("", description="Matrikelnummer oder Lektor-Username (z.B. rmatzinger)"), semester: str = Query(""), week: str = Query("", description="Woche als YYYY-MM-DD (Monatag)"), tab: str = Query("week", description="week|sem")): @@ -444,6 +444,24 @@ def api_person_id(uid: str = Query(...)): return {"uid": uid, "person_id": client.person_id_for(uid)} +@app.get("/api/benutzer") +def api_benutzer(q: str = Query("")): + """Namensuche über Studierende + Lektoren (Autocomplete).""" + q = (q or "").strip().lower() + if len(q) < 2: + return [] + out = [] + for b in client.benutzer_all(): + hay = f"{b['nachname']} {b['vorname']} {b['uid']}".lower() + if q in hay: + out.append({"uid": b["uid"], + "name": f"{b['nachname']}, {b['vorname']}".strip(", "), + "kind": "student" if b["uid"].isdigit() else "lektor"}) + if len(out) >= 12: + break + return out + + @app.get("/api/meta") def api_meta(): return _meta() @@ -497,8 +515,10 @@ def export_ical(kind: str = "student", if x["begin"] <= datetime.now().timestamp() <= x["ende"]), meta["semesters"][0])) if kind == "student": - ical = client.ical_raw("student", uid=uid or OWN_UID, begin=s["begin"], ende=s["ende"]) - fname = f"lvplan_{uid or OWN_UID}_{s['label']}.ics" + uid = uid or OWN_UID + ptype = "student" if uid.isdigit() else "lektor" + ical = client.ical_raw(ptype, uid=uid, begin=s["begin"], ende=s["ende"]) + fname = f"lvplan_{uid}_{s['label']}.ics" else: ical = client.ical_raw("stg", kz=kz, sem=sem, ver=ver, grp=grp, begin=s["begin"], ende=s["ende"]) diff --git a/app/static/app.js b/app/static/app.js index 86eadba..24480b0 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -272,16 +272,19 @@ async function openEvent(ev) { const body = $("#detail-body"); body.innerHTML = `

Lade Details vom CIS...

`; const stunde = stundeOf({ s: parseISO(ev.start) }); + const detailKind = plan.kind === "student" + ? (plan.ptype === "lektor" ? "lektor" : "student") + : plan.kind; const params = new URLSearchParams({ - kind: plan.kind, datum: ev.datum, stunde, ort: ev.ort, + kind: detailKind, datum: ev.datum, stunde, ort: ev.ort, }); - if (plan.kind === "student") { - params.set("uid", plan.uid); - } else { + if (plan.kind === "stg") { params.set("kz", plan.kz); params.set("sem", plan.sem); params.set("ver", plan.ver); params.set("grp", plan.grp); + } else { + params.set("uid", plan.uid); } try { const r = await fetch("/api/detail?" + params.toString()); @@ -477,11 +480,48 @@ function initSearchJob() { poll(); } +/* ================= Matrikel/Lektor Vorschläge ================= */ +function initUidSuggest() { + const inp = $("#uid-input"); + if (!inp) return; + const box = $("#uid-suggest"); + let timer = null; + inp.addEventListener("input", () => { + clearTimeout(timer); + const q = inp.value.trim(); + if (q.length < 2) { box.classList.remove("show"); return; } + timer = setTimeout(async () => { + try { + const r = await fetch("/api/benutzer?q=" + encodeURIComponent(q)); + const arr = await r.json(); + if (!arr.length) { box.classList.remove("show"); return; } + box.innerHTML = arr.map(p => + `${esc(p.name)} + ${esc(p.uid)} + ${p.kind === "lektor" ? 'Lektor' : ""}` + ).join(""); + box.classList.add("show"); + } catch (e) { box.classList.remove("show"); } + }, 250); + }); + box.addEventListener("click", (e) => { + const a = e.target.closest("a[data-uid]"); + if (!a) return; + e.preventDefault(); + inp.value = a.dataset.uid; + box.classList.remove("show"); + }); + document.addEventListener("click", (e) => { + if (!e.target.closest(".uid-field")) box.classList.remove("show"); + }); +} + /* ================= boot ================= */ document.addEventListener("DOMContentLoaded", () => { if (window.__PLAN_DATA__) initPlan(); if (window.__STGS__) decodeMat(); if (window.__SEARCH_JOB__) initSearchJob(); + initUidSuggest(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDetail(); diff --git a/app/static/style.css b/app/static/style.css index b67ab01..49ee851 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -256,6 +256,22 @@ table.data .num { text-align: right; font-variant-numeric: tabular-nums; } .match-tile.match-strong .sim-bar > div { background: var(--green); } .match-tile.match-strong .sim-num { color: var(--green); } +/* ---------- Matrikel/Lektor Vorschläge ---------- */ +.uid-field { position: relative; } +.suggest { + display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 60; + background: #fff; border: 1px solid var(--line); border-radius: 8px; + box-shadow: 0 8px 24px rgba(0,0,0,.14); margin-top: 4px; overflow: hidden; +} +.suggest.show { display: block; } +.suggest a { + display: flex; align-items: center; gap: 8px; padding: 9px 12px; font-size: 13.5px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.suggest a:hover { background: var(--blue-light); text-decoration: none; } +.suggest a .mono { margin-left: auto; font-size: 12px; } +.muted { color: var(--muted); } + .footer { border-top: 1px solid var(--line); padding: 16px 0; color: var(--muted); font-size: 13px; background: #fff; } @media (max-width: 720px) { diff --git a/app/templates/plan.html b/app/templates/plan.html index 8d21ffc..fbe9bdf 100644 --- a/app/templates/plan.html +++ b/app/templates/plan.html @@ -15,9 +15,11 @@
{% if kind == 'student' %} -
- - +
+ + +
{% if uid and uid | length >= 9 %} {% set dec = decode_matrikel(uid, meta.stgs) %} @@ -76,10 +78,17 @@ {% if kind == 'student' and person %}
-
Person
{{ person.name }}
+
Person
{{ person.name }} + {% if person.kind == 'lektor' %}Lektor{% endif %} + Profil +
+ {% if person.kind == 'student' %}
Matrikelnummer
{{ person.uid }}
Studiengang
{{ person.stg }}
Semester / Verband
{{ person.semester }} / {{ person.verband }}{% if person.gruppe %} / Grp. {{ person.gruppe }}{% endif %}
+ {% else %} +
Username
{{ person.uid }}
+ {% endif %}
{% endif %} @@ -127,6 +136,7 @@