"""Academic-calendar key dates — the cohort filter the Today feed uses, and the unfiltered term list behind the My VU page's `/api/key-dates`. The academic calendar publishes cohort-specific deadlines with no audience tag — `Fall 2026-AC` is on 78 of the 85 records and `key-dates` on only 16 — so the title is the only signal available. Matching it is narrow and explicit rather than clever: an unrecognized title is always kept. The real fix is per-audience tagging in the collector, the same gap the event domain tags have. """ from __future__ import annotations import re from .kb import queries # (keyword in the title, the school it belongs to) COHORT_KEY_DATES = ( ("business module", "Owen Graduate School of Management"), ("engineering module", "School of Engineering"), ("with no music fee", "Blair School of Music"), ) # Only meaningful to students enrolled in the English Language Center, which the # background question is the closest signal we have for. ELC = "english language center" # Registrar-internal milestones. A student can't act on them and has no idea what # they mean; they're on the public calendar because staff read it too. INTERNAL_KEY_DATES = ("discrepancy reporting", "census data") # Wide enough to hold every record on the calendar (85 today). _LIMIT = 200 def norm_name(value: str | None) -> str: return re.sub(r"\s+", " ", (value or "").strip()).casefold() def student_schools(profile: dict, index) -> set[str]: """The schools the student's majors and minors sit in. Resolved through the index rather than by reading `programs.json` again, so the server and the catalog picker in the frontend agree on what a major is called. Empty when nothing resolves — and an empty set means "don't filter", never "filter everything". """ if index is None: return set() wanted = {norm_name(p) for key in ("majors", "minors") for p in (profile.get(key) or []) if isinstance(p, str)} if not wanted: return set() out: set[str] = set() for doc in index.docs: if doc.kind != "program" or not doc.extra.get("schools"): continue # Catalog titles carry a "(Major)" / "(Minor)" suffix the picker strips. bare = norm_name(re.sub(r"\s*\((?:major|minor)\)\s*$", "", doc.title, flags=re.I)) if bare in wanted: out.update(doc.extra["schools"]) return out def relevant_to_student(title: str, schools: set[str], background: list) -> bool: """Whether this academic-calendar entry is this student's business.""" low = (title or "").casefold() if any(n in low for n in INTERNAL_KEY_DATES): return False if ELC in low: return "international" in (background or []) for needle, school in COHORT_KEY_DATES: if needle in low: # Unknown school: keep it rather than hide something on a guess. return not schools or school in schools return True def payload(index, start: str, end: str) -> dict: """Every academic-calendar date in [start, end], soonest first — unfiltered. Deliberately the WHOLE feed (everything events.vanderbilt.edu publishes under the academic-calendar flag), by product decision: on the My VU page the student is browsing the calendar itself, so cohort dates for other schools and registrar milestones stay visible rather than silently vanishing. The Today *feed* is the opposite surface — five interleaved cards where an irrelevant date costs real attention — so it keeps filtering through `relevant_to_student` above. """ items = [] for hit in queries.key_dates_between(index, start, end, limit=_LIMIT): items.append({ "date": (hit.when or "")[:10], "title": hit.doc.title, "url": hit.doc.url, "terms": hit.doc.extra.get("terms") or [], }) return {"ready": True, "items": items}