Lektoren app-weit: Plan/Details/iCal mit type=lektor, Namenssuche (Autocomplete) für Studierende und Lektoren, Lektor-Karte mit Profil-Link
This commit is contained in:
+27
-9
@@ -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)
|
||||
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="")
|
||||
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 = 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}")
|
||||
|
||||
+23
-3
@@ -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"])
|
||||
|
||||
+44
-4
@@ -272,16 +272,19 @@ async function openEvent(ev) {
|
||||
const body = $("#detail-body");
|
||||
body.innerHTML = `<p style="color:var(--muted)">Lade Details vom CIS...</p>`;
|
||||
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 =>
|
||||
`<a href="#" data-uid="${esc(p.uid)}">${esc(p.name)}
|
||||
<span class="mono muted">${esc(p.uid)}</span>
|
||||
${p.kind === "lektor" ? '<span class="badge badge-muted">Lektor</span>' : ""}</a>`
|
||||
).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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+14
-4
@@ -15,9 +15,11 @@
|
||||
<div class="card">
|
||||
<form class="form-row" method="get" action="/{{ kind if kind != 'student' else 'plan' }}">
|
||||
{% if kind == 'student' %}
|
||||
<div class="field">
|
||||
<label>Matrikelnummer</label>
|
||||
<input type="text" name="uid" value="{{ uid }}" placeholder="z.B. 2410640055" required>
|
||||
<div class="field uid-field">
|
||||
<label>Matrikel oder Lektor</label>
|
||||
<input type="text" id="uid-input" name="uid" value="{{ uid }}"
|
||||
placeholder="z.B. 2410640055 oder rmatzinger" required>
|
||||
<div class="suggest" id="uid-suggest"></div>
|
||||
</div>
|
||||
{% if uid and uid | length >= 9 %}
|
||||
{% set dec = decode_matrikel(uid, meta.stgs) %}
|
||||
@@ -76,10 +78,17 @@
|
||||
{% if kind == 'student' and person %}
|
||||
<div class="card">
|
||||
<div class="kv">
|
||||
<div class="item"><div class="k">Person</div><div class="v">{{ person.name }}</div></div>
|
||||
<div class="item"><div class="k">Person</div><div class="v">{{ person.name }}
|
||||
{% if person.kind == 'lektor' %}<span class="badge">Lektor</span>{% endif %}
|
||||
<a class="btn btn-sm" href="/person/{{ person.uid }}" style="margin-left:10px">Profil</a>
|
||||
</div></div>
|
||||
{% if person.kind == 'student' %}
|
||||
<div class="item"><div class="k">Matrikelnummer</div><div class="v mono">{{ person.uid }}</div></div>
|
||||
<div class="item"><div class="k">Studiengang</div><div class="v">{{ person.stg }}</div></div>
|
||||
<div class="item"><div class="k">Semester / Verband</div><div class="v">{{ person.semester }} / {{ person.verband }}{% if person.gruppe %} / Grp. {{ person.gruppe }}{% endif %}</div></div>
|
||||
{% else %}
|
||||
<div class="item"><div class="k">Username</div><div class="v mono">{{ person.uid }}</div></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -127,6 +136,7 @@
|
||||
<script>
|
||||
window.__PLAN_DATA__ = {
|
||||
kind: "{{ kind }}",
|
||||
ptype: "{{ person.kind if person else '' }}",
|
||||
tab: "{{ tab }}",
|
||||
uid: "{{ uid }}",
|
||||
kz: "{{ kz }}",
|
||||
|
||||
Reference in New Issue
Block a user