File size: 3,994 Bytes
cdd85a5
 
5a759a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdd85a5
 
5a759a9
cdd85a5
 
 
 
 
 
 
5a759a9
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""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}