Files
sm-bm-plugin/controllers/api/schedule.go
Jakub Zych 71b848a376 Adopt SummerCMS v0.1.1 admin features
Datepickers on native time.Time / lagoon.Date fields, file uploads on
materials, readings and class sessions managed from the course form
(course_id made nullable).
2026-10-03 06:03:34 +02:00

141 lines
3.7 KiB
Go

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
}