Refine report filtering and date formatting
CI / test (push) Successful in 28s
CI / publish (push) Failing after 1m14s

This commit is contained in:
Joe Julian
2026-03-30 18:05:54 -07:00
parent 86856abda2
commit edb66e3a5a
2 changed files with 200 additions and 23 deletions
+106 -23
View File
@@ -56,6 +56,12 @@ type Assignment struct {
Category string
}
type ClassInfo struct {
URL string
Name string
Grade string
}
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
@@ -88,16 +94,16 @@ func run() error {
if err != nil {
return err
}
missing = filterMissingAssignments(missing, cfg.StartDate)
classLinks, err := fetchClassLinks(client, cfg.ScheduleURL)
classes, err := fetchScheduleClasses(client, cfg.ScheduleURL)
if err != nil {
return err
}
missing = filterMissingAssignments(missing, cfg.StartDate, classesWithCurrentA(classes))
var upcoming []Assignment
for _, classURL := range classLinks {
items, err := fetchUpcomingAssignmentsForClass(client, classURL, now)
for _, classInfo := range classes {
items, err := fetchUpcomingAssignmentsForClass(client, classInfo.URL, now)
if err != nil {
return err
}
@@ -429,31 +435,57 @@ func fetchMissingAssignments(client *http.Client, assignmentsURL string, now tim
return assignments, nil
}
func fetchClassLinks(client *http.Client, scheduleURL string) ([]string, error) {
func fetchScheduleClasses(client *http.Client, scheduleURL string) ([]ClassInfo, error) {
root, baseURL, err := fetchHTML(client, scheduleURL)
if err != nil {
return nil, err
}
return extractScheduleClasses(root, baseURL)
}
func extractScheduleClasses(root *html.Node, baseURL *url.URL) ([]ClassInfo, error) {
table := findNode(root, func(n *html.Node) bool {
return n.Type == html.ElementNode && n.Data == "table" && hasClass(n, "current-classes")
})
if table == nil {
return nil, errors.New("current classes table not found")
}
seen := map[string]struct{}{}
var links []string
for _, link := range findNodes(root, func(n *html.Node) bool {
return n.Type == html.ElementNode && n.Data == "a" &&
strings.Contains(attr(n, "href"), "/class/") &&
attr(n, "data-track") == "Class Home Page"
var classes []ClassInfo
for _, row := range findNodes(table, func(n *html.Node) bool {
return n.Type == html.ElementNode && n.Data == "tr"
}) {
href := attr(link, "href")
if href == "" {
cells := directChildCells(row)
if len(cells) < 5 {
continue
}
link := findNode(cells[0], func(n *html.Node) bool {
return n.Type == html.ElementNode && n.Data == "a" &&
strings.Contains(attr(n, "href"), "/class/") &&
attr(n, "data-track") == "Class Home Page"
})
if link == nil {
continue
}
classURL := resolveURL(baseURL, strings.SplitN(attr(link, "href"), "?", 2)[0])
if classURL == "" {
continue
}
classURL := resolveURL(baseURL, strings.SplitN(href, "?", 2)[0])
if _, ok := seen[classURL]; ok {
continue
}
seen[classURL] = struct{}{}
links = append(links, classURL)
classes = append(classes, ClassInfo{
URL: classURL,
Name: firstNonEmpty(ownText(link), nodeText(link)),
Grade: nodeText(cells[4]),
})
}
slices.Sort(links)
return links, nil
slices.SortFunc(classes, func(a, b ClassInfo) int {
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
return classes, nil
}
func fetchUpcomingAssignmentsForClass(client *http.Client, classURL string, now time.Time) ([]Assignment, error) {
@@ -519,12 +551,16 @@ func fetchHTML(client *http.Client, rawURL string) (*html.Node, *url.URL, error)
return doc, resp.Request.URL, nil
}
func filterMissingAssignments(assignments []Assignment, startDate time.Time) []Assignment {
func filterMissingAssignments(assignments []Assignment, startDate time.Time, coursesWithCurrentA map[string]struct{}) []Assignment {
var filtered []Assignment
for _, assignment := range assignments {
if !assignment.DueDate.Before(startOfDay(startDate)) {
filtered = append(filtered, assignment)
if assignment.DueDate.Before(startOfDay(startDate)) {
continue
}
if hasCurrentA(coursesWithCurrentA, assignment.Course) && isRevisionStatus(assignment.Status) {
continue
}
filtered = append(filtered, assignment)
}
return filtered
}
@@ -568,15 +604,15 @@ func buildSubject(missing, upcoming []Assignment, startDate time.Time, upcomingD
func buildBody(missing, upcoming []Assignment, startDate time.Time, assignmentsURL, scheduleURL string, now time.Time, upcomingDays int) string {
var b strings.Builder
fmt.Fprintf(&b, "Daily Alma missing assignments report for %s\n", now.Format("2006-01-02"))
fmt.Fprintf(&b, "Assignments due on or after %s\n\n", startDate.Format("2006-01-02"))
fmt.Fprintf(&b, "Daily Alma missing assignments report for %s\n", formatReportDate(now))
fmt.Fprintf(&b, "Assignments due on or after %s\n\n", formatReportDate(startDate))
b.WriteString("Missing / Revision Needed\n")
b.WriteString("-------------------------\n")
if len(missing) == 0 {
b.WriteString("No missing or revision-needed assignments matched the filter.\n")
} else {
for _, assignment := range missing {
fmt.Fprintf(&b, "- %s | %s | %s\n", assignment.DueDate.Format("2006-01-02"), assignment.Course, assignment.Title)
fmt.Fprintf(&b, "- %s | %s | %s\n", formatReportDate(assignment.DueDate), assignment.Course, assignment.Title)
fmt.Fprintf(&b, " Status: %s\n", assignment.Status)
fmt.Fprintf(&b, " Link: %s\n", assignment.Link)
}
@@ -587,7 +623,7 @@ func buildBody(missing, upcoming []Assignment, startDate time.Time, assignmentsU
b.WriteString("No upcoming assignments were due in the configured window.\n")
} else {
for _, assignment := range upcoming {
fmt.Fprintf(&b, "- %s | %s | %s\n", assignment.DueDate.Format("2006-01-02"), assignment.Course, assignment.Title)
fmt.Fprintf(&b, "- %s | %s | %s\n", formatReportDate(assignment.DueDate), assignment.Course, assignment.Title)
fmt.Fprintf(&b, " Category: %s\n", firstNonEmpty(assignment.Category, "Upcoming"))
fmt.Fprintf(&b, " Link: %s\n", assignment.Link)
}
@@ -677,6 +713,30 @@ func normalizeSMTPHeloName(value string) string {
return value + ".localdomain"
}
func formatReportDate(t time.Time) string {
return t.Format("Monday 2006-01-02")
}
func classesWithCurrentA(classes []ClassInfo) map[string]struct{} {
courses := map[string]struct{}{}
for _, classInfo := range classes {
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(classInfo.Grade)), "A") {
courses[strings.ToLower(strings.TrimSpace(classInfo.Name))] = struct{}{}
}
}
return courses
}
func hasCurrentA(courses map[string]struct{}, course string) bool {
_, ok := courses[strings.ToLower(strings.TrimSpace(course))]
return ok
}
func isRevisionStatus(status string) bool {
status = strings.ToLower(strings.TrimSpace(status))
return strings.Contains(status, "revision") || strings.Contains(status, "revise")
}
func inferAssignmentDate(monthDay string, now time.Time) time.Time {
return inferDate(monthDay, now, false)
}
@@ -778,6 +838,29 @@ func directChildrenByTag(node *html.Node, tag string) []*html.Node {
return children
}
func directChildCells(node *html.Node) []*html.Node {
var children []*html.Node
for child := node.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode && (child.Data == "th" || child.Data == "td") {
children = append(children, child)
}
}
return children
}
func ownText(node *html.Node) string {
var parts []string
for child := node.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.TextNode {
text := strings.TrimSpace(child.Data)
if text != "" {
parts = append(parts, text)
}
}
}
return strings.Join(parts, " ")
}
func hasClass(node *html.Node, class string) bool {
for _, item := range strings.Fields(attr(node, "class")) {
if item == class {