feat: add the student API under /_bm/api/v1
Dashboard, courses with counters, schedule, meetings, deadlines, readings with per-user progress (mark and unmark) and paginated published materials with tags and files, behind golem15.user's jwt.auth. Responses are built field by field with Warsaw RFC 3339 timestamps; bad query parameters answer 422 in Laravel's shape.
This commit is contained in:
88
controllers/api/dashboard.go
Normal file
88
controllers/api/dashboard.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.golem15.com/jakub/sm-bm-plugin/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// dashboardJSON is the answer of /dashboard.
|
||||
type dashboardJSON struct {
|
||||
NextSessions []sessionJSON `json:"next_sessions"`
|
||||
UpcomingDeadlines []deadlineJSON `json:"upcoming_deadlines"`
|
||||
CurrentMeeting *meetingJSON `json:"current_meeting"`
|
||||
NextMeeting *meetingJSON `json:"next_meeting"`
|
||||
Readings readingsSummary `json:"readings"`
|
||||
}
|
||||
|
||||
// Dashboard answers GET /dashboard: the next class sessions (those not yet
|
||||
// over), the deadlines not yet due, the meeting under way today and the next
|
||||
// one, and the caller's reading progress.
|
||||
func (h *Handlers) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := h.caller(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
db, ok := h.db(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
now := h.now().UTC()
|
||||
limit := h.dashboardLimit()
|
||||
out := dashboardJSON{}
|
||||
|
||||
var sessions []models.ClassSession
|
||||
if err := preloadSession(db).Where("ends_at > ?", now).Order("starts_at, id").Limit(limit).Find(&sessions).Error; err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
out.NextSessions = newSessionsJSON(sessions)
|
||||
|
||||
var deadlines []models.Deadline
|
||||
if err := db.Preload("Course").Where("due_at >= ?", now).Order("due_at, id").Limit(limit).Find(&deadlines).Error; err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
out.UpcomingDeadlines = newDeadlinesJSON(deadlines)
|
||||
|
||||
day := today(h.now()).Format(models.DateLayout)
|
||||
var err error
|
||||
if out.CurrentMeeting, err = meetingWhere(db, "starts_on <= ? AND ends_on >= ?", day, day); err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
if out.NextMeeting, err = meetingWhere(db, "starts_on > ?", day); err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Raw(`SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(p.id) AS read,
|
||||
COUNT(*) FILTER (WHERE r.is_required) AS required_total,
|
||||
COUNT(p.id) FILTER (WHERE r.is_required) AS required_read
|
||||
FROM jz_bm_readings r
|
||||
LEFT JOIN jz_bm_reading_progress p ON p.reading_id = r.id AND p.user_id = ?`, userID).Scan(&out.Readings).Error
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
writeData(w, out)
|
||||
}
|
||||
|
||||
// meetingWhere returns the first meeting by start date that matches the
|
||||
// condition, with its session count, or nil.
|
||||
func meetingWhere(db *gorm.DB, cond string, args ...any) (*meetingJSON, error) {
|
||||
var m models.Meeting
|
||||
res := db.Where(cond, args...).Order("starts_on, number").Limit(1).Find(&m)
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
return nil, res.Error
|
||||
}
|
||||
var sessions int64
|
||||
if err := db.Model(&models.ClassSession{}).Where("meeting_id = ?", m.ID).Count(&sessions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := newMeetingJSON(m, sessions)
|
||||
return &out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user