package api import ( "net/http" "time" "git.golem15.com/jakub/sm-bm-plugin/models" "gorm.io/gorm" ) // validRange validates ?from=&to= and returns the decoded input. func (h *Handlers) validRange(w http.ResponseWriter, r *http.Request) (map[string]any, bool) { in := query(r, "from", "to") rules := rangeRules if str(in, "from") != "" { rules = rangeRulesFrom } return in, h.validate(w, r, in, rules) } // bounds turns validated from and to dates into a half-open instant range: // from midnight of from to midnight after to, in Europe/Warsaw. Without // from the lower bound is lower; without to there is no upper bound. func bounds(in map[string]any, lower time.Time) (time.Time, *time.Time) { if from, ok := dayStart(in, "from"); ok { lower = from } if to, ok := dayStart(in, "to"); ok { upper := to.AddDate(0, 0, 1) return lower, &upper } return lower, nil } // preloadSession queries the class sessions students see, with their course // and meeting. A session without a course is still pending on an unsaved // course form and is left out. func preloadSession(q *gorm.DB) *gorm.DB { return q.Preload("Course").Preload("Meeting").Where("jz_bm_class_sessions.course_id IS NOT NULL") } // Schedule answers GET /schedule?from=YYYY-MM-DD&to=YYYY-MM-DD: the class // sessions starting in the range, by start time. from defaults to today. func (h *Handlers) Schedule(w http.ResponseWriter, r *http.Request) { if _, ok := h.caller(w, r); !ok { return } in, ok := h.validRange(w, r) if !ok { return } db, ok := h.db(w, r) if !ok { return } lower, upper := bounds(in, today(h.now())) q := preloadSession(db).Where("starts_at >= ?", lower.UTC()) if upper != nil { q = q.Where("starts_at < ?", upper.UTC()) } var rows []models.ClassSession if err := q.Order("starts_at, id").Find(&rows).Error; err != nil { writeOpaque500(w) return } writeData(w, newSessionsJSON(rows)) } // Meetings answers GET /meetings: every zjazd by number, with its session // count. func (h *Handlers) Meetings(w http.ResponseWriter, r *http.Request) { if _, ok := h.caller(w, r); !ok { return } db, ok := h.db(w, r) if !ok { return } var rows []models.Meeting if err := db.Order("number, id").Find(&rows).Error; err != nil { writeOpaque500(w) return } counts, err := countBy(db, "jz_bm_class_sessions", "meeting_id") if err != nil { writeOpaque500(w) return } out := make([]meetingJSON, 0, len(rows)) for _, m := range rows { out = append(out, newMeetingJSON(m, counts[m.ID])) } writeData(w, out) } // Deadlines answers GET /deadlines?from=&to=. Without from it lists the // deadlines not yet due; from and to are calendar days in Europe/Warsaw. func (h *Handlers) Deadlines(w http.ResponseWriter, r *http.Request) { if _, ok := h.caller(w, r); !ok { return } in, ok := h.validRange(w, r) if !ok { return } db, ok := h.db(w, r) if !ok { return } lower, upper := bounds(in, h.now()) q := db.Preload("Course").Where("due_at >= ?", lower.UTC()) if upper != nil { q = q.Where("due_at < ?", upper.UTC()) } var rows []models.Deadline if err := q.Order("due_at, id").Find(&rows).Error; err != nil { writeOpaque500(w) return } writeData(w, newDeadlinesJSON(rows)) } // countBy counts the rows of table per value of a nullable foreign key // column. table and column are constants of this package, never input. func countBy(db *gorm.DB, table, column string) (map[uint]int64, error) { var rows []struct { Key uint Count int64 } err := db.Table(table). Select(column + " AS key, COUNT(*) AS count"). Where(column + " IS NOT NULL"). Group(column). Scan(&rows).Error out := make(map[uint]int64, len(rows)) for _, row := range rows { out[row.Key] = row.Count } return out, err }