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:
+30
-12
@@ -58,10 +58,11 @@ class Event:
|
|||||||
class Person:
|
class Person:
|
||||||
uid: str
|
uid: str
|
||||||
name: str
|
name: str
|
||||||
stg: str
|
stg: str = ""
|
||||||
semester: str
|
semester: str = ""
|
||||||
verband: str
|
verband: str = ""
|
||||||
gruppe: str
|
gruppe: str = ""
|
||||||
|
kind: str = "student" # student | lektor
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -177,7 +178,8 @@ class CISClient:
|
|||||||
return f"{self.base}/private/lvplan/stpl_kalender.php?{q}"
|
return f"{self.base}/private/lvplan/stpl_kalender.php?{q}"
|
||||||
|
|
||||||
def _ical_student_params(self, uid: str, begin: int, ende: int) -> dict:
|
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": "",
|
"stg_kz": "", "sem": "", "ver": "", "grp": "",
|
||||||
"gruppe_kurzbz": "", "lva": "", "begin": begin, "ende": ende,
|
"gruppe_kurzbz": "", "lva": "", "begin": begin, "ende": ende,
|
||||||
"format": "ical", "version": "2", "target": "ical"}
|
"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))))
|
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:
|
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_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"])))
|
return self._get(self._ical_url(self._ical_stg_params(kw["kz"], kw["sem"], kw["ver"], kw.get("grp", ""), kw["begin"], kw["ende"])))
|
||||||
|
|
||||||
# ---------- person ----------
|
# ---------- person ----------
|
||||||
@lru_cache(maxsize=256)
|
@lru_cache(maxsize=256)
|
||||||
def person(self, uid: str) -> Optional[Person]:
|
def person(self, uid: str) -> Optional[Person]:
|
||||||
|
uid = (uid or "").strip()
|
||||||
|
if not uid:
|
||||||
|
return None
|
||||||
|
is_student = uid.isdigit()
|
||||||
try:
|
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:
|
except requests.HTTPError:
|
||||||
return None
|
return None
|
||||||
t = re.sub(r"<[^>]+>", " ", html)
|
t = re.sub(r"<[^>]+>", " ", html)
|
||||||
t = re.sub(r"\s+", " ", t)
|
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:
|
if not m:
|
||||||
return None
|
return None
|
||||||
name, u, stg, sem, ver = m.groups()
|
name, u = m.groups()
|
||||||
return Person(uid=u, name=name.strip(), stg=stg.strip(),
|
return Person(uid=u, name=name.strip(), kind="lektor")
|
||||||
semester=sem.strip(), verband=ver.strip(), gruppe="")
|
|
||||||
|
|
||||||
# ---------- person_id + photos ----------
|
# ---------- person_id + photos ----------
|
||||||
def person_id_for(self, uid: str) -> str:
|
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 = "",
|
def detail(self, kind: str, datum: str, stunde: int, uid: str = "",
|
||||||
kz: str = "", sem: str = "", ver: str = "", grp: str = "",
|
kz: str = "", sem: str = "", ver: str = "", grp: str = "",
|
||||||
ort: str = "") -> dict:
|
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}"
|
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"&stunde={stunde}&pers_uid={uid}&stg_kz={kz}&sem={sem}&ver={ver}"
|
||||||
f"&grp= {grp}&ort_kurzbz={ort}")
|
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)
|
@app.get("/plan", response_class=HTMLResponse)
|
||||||
def plan(request: Request,
|
def plan(request: Request,
|
||||||
uid: str = Query("", description="Matrikelnummer"),
|
uid: str = Query("", description="Matrikelnummer oder Lektor-Username (z.B. rmatzinger)"),
|
||||||
semester: str = Query(""),
|
semester: str = Query(""),
|
||||||
week: str = Query("", description="Woche als YYYY-MM-DD (Monatag)"),
|
week: str = Query("", description="Woche als YYYY-MM-DD (Monatag)"),
|
||||||
tab: str = Query("week", description="week|sem")):
|
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)}
|
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")
|
@app.get("/api/meta")
|
||||||
def api_meta():
|
def api_meta():
|
||||||
return _meta()
|
return _meta()
|
||||||
@@ -497,8 +515,10 @@ def export_ical(kind: str = "student",
|
|||||||
if x["begin"] <= datetime.now().timestamp() <= x["ende"]),
|
if x["begin"] <= datetime.now().timestamp() <= x["ende"]),
|
||||||
meta["semesters"][0]))
|
meta["semesters"][0]))
|
||||||
if kind == "student":
|
if kind == "student":
|
||||||
ical = client.ical_raw("student", uid=uid or OWN_UID, begin=s["begin"], ende=s["ende"])
|
uid = uid or OWN_UID
|
||||||
fname = f"lvplan_{uid or OWN_UID}_{s['label']}.ics"
|
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:
|
else:
|
||||||
ical = client.ical_raw("stg", kz=kz, sem=sem, ver=ver, grp=grp,
|
ical = client.ical_raw("stg", kz=kz, sem=sem, ver=ver, grp=grp,
|
||||||
begin=s["begin"], ende=s["ende"])
|
begin=s["begin"], ende=s["ende"])
|
||||||
|
|||||||
+44
-4
@@ -272,16 +272,19 @@ async function openEvent(ev) {
|
|||||||
const body = $("#detail-body");
|
const body = $("#detail-body");
|
||||||
body.innerHTML = `<p style="color:var(--muted)">Lade Details vom CIS...</p>`;
|
body.innerHTML = `<p style="color:var(--muted)">Lade Details vom CIS...</p>`;
|
||||||
const stunde = stundeOf({ s: parseISO(ev.start) });
|
const stunde = stundeOf({ s: parseISO(ev.start) });
|
||||||
|
const detailKind = plan.kind === "student"
|
||||||
|
? (plan.ptype === "lektor" ? "lektor" : "student")
|
||||||
|
: plan.kind;
|
||||||
const params = new URLSearchParams({
|
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") {
|
if (plan.kind === "stg") {
|
||||||
params.set("uid", plan.uid);
|
|
||||||
} else {
|
|
||||||
params.set("kz", plan.kz);
|
params.set("kz", plan.kz);
|
||||||
params.set("sem", plan.sem);
|
params.set("sem", plan.sem);
|
||||||
params.set("ver", plan.ver);
|
params.set("ver", plan.ver);
|
||||||
params.set("grp", plan.grp);
|
params.set("grp", plan.grp);
|
||||||
|
} else {
|
||||||
|
params.set("uid", plan.uid);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const r = await fetch("/api/detail?" + params.toString());
|
const r = await fetch("/api/detail?" + params.toString());
|
||||||
@@ -477,11 +480,48 @@ function initSearchJob() {
|
|||||||
poll();
|
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 ================= */
|
/* ================= boot ================= */
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (window.__PLAN_DATA__) initPlan();
|
if (window.__PLAN_DATA__) initPlan();
|
||||||
if (window.__STGS__) decodeMat();
|
if (window.__STGS__) decodeMat();
|
||||||
if (window.__SEARCH_JOB__) initSearchJob();
|
if (window.__SEARCH_JOB__) initSearchJob();
|
||||||
|
initUidSuggest();
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape") closeDetail();
|
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-bar > div { background: var(--green); }
|
||||||
.match-tile.match-strong .sim-num { color: 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; }
|
.footer { border-top: 1px solid var(--line); padding: 16px 0; color: var(--muted); font-size: 13px; background: #fff; }
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
+14
-4
@@ -15,9 +15,11 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form class="form-row" method="get" action="/{{ kind if kind != 'student' else 'plan' }}">
|
<form class="form-row" method="get" action="/{{ kind if kind != 'student' else 'plan' }}">
|
||||||
{% if kind == 'student' %}
|
{% if kind == 'student' %}
|
||||||
<div class="field">
|
<div class="field uid-field">
|
||||||
<label>Matrikelnummer</label>
|
<label>Matrikel oder Lektor</label>
|
||||||
<input type="text" name="uid" value="{{ uid }}" placeholder="z.B. 2410640055" required>
|
<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>
|
</div>
|
||||||
{% if uid and uid | length >= 9 %}
|
{% if uid and uid | length >= 9 %}
|
||||||
{% set dec = decode_matrikel(uid, meta.stgs) %}
|
{% set dec = decode_matrikel(uid, meta.stgs) %}
|
||||||
@@ -76,10 +78,17 @@
|
|||||||
{% if kind == 'student' and person %}
|
{% if kind == 'student' and person %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="kv">
|
<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">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">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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -127,6 +136,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.__PLAN_DATA__ = {
|
window.__PLAN_DATA__ = {
|
||||||
kind: "{{ kind }}",
|
kind: "{{ kind }}",
|
||||||
|
ptype: "{{ person.kind if person else '' }}",
|
||||||
tab: "{{ tab }}",
|
tab: "{{ tab }}",
|
||||||
uid: "{{ uid }}",
|
uid: "{{ uid }}",
|
||||||
kz: "{{ kz }}",
|
kz: "{{ kz }}",
|
||||||
|
|||||||
Reference in New Issue
Block a user