foresight / server /key_dates.py
umangchaudhry's picture
Deploy 887f4d9 from GitHub
cdd85a5 verified
Raw
History Blame Contribute Delete
3.99 kB
"""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}