Files
sm-bm-plugin/controllers/api/courses.go
Jakub Zych 98cabfd51d 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.
2026-10-02 14:26:34 +02:00

119 lines
3.4 KiB
Go

package api
import (
"net/http"
"time"
"git.golem15.com/golem15/summercms/modules/surf"
"git.golem15.com/jakub/sm-bm-plugin/models"
"gorm.io/gorm"
)
// courseRow is a course with its counters, as the counting query returns it.
type courseRow struct {
models.Course `gorm:"embedded"`
SessionsCount int64 `gorm:"column:sessions_count"`
ReadingsCount int64 `gorm:"column:readings_count"`
ReadingsRead int64 `gorm:"column:readings_read"`
MaterialsCount int64 `gorm:"column:materials_count"`
}
func (c courseRow) counts() courseCounts {
return courseCounts{Sessions: c.SessionsCount, Readings: c.ReadingsCount, ReadingsRead: c.ReadingsRead, Materials: c.MaterialsCount}
}
// coursesWithCounts selects courses with their session, reading and
// published material counts and the caller's read readings.
func coursesWithCounts(db *gorm.DB, userID uint, now time.Time) *gorm.DB {
return db.Table("jz_bm_courses AS c").Select(`c.*,
(SELECT COUNT(*) FROM jz_bm_class_sessions s WHERE s.course_id = c.id) AS sessions_count,
(SELECT COUNT(*) FROM jz_bm_readings r WHERE r.course_id = c.id) AS readings_count,
(SELECT COUNT(*) FROM jz_bm_reading_progress p JOIN jz_bm_readings r ON r.id = p.reading_id
WHERE r.course_id = c.id AND p.user_id = ?) AS readings_read,
(SELECT COUNT(*) FROM jz_bm_materials m WHERE m.course_id = c.id
AND m.published_at IS NOT NULL AND m.published_at <= ?) AS materials_count`, userID, now.UTC())
}
// Courses answers GET /courses?semester=N: every course in programme order
// with its counters.
func (h *Handlers) Courses(w http.ResponseWriter, r *http.Request) {
userID, ok := h.caller(w, r)
if !ok {
return
}
in := query(r, "semester")
if !h.validate(w, r, in, coursesRules) {
return
}
db, ok := h.db(w, r)
if !ok {
return
}
q := coursesWithCounts(db, userID, h.now())
if semester := intParam(in, "semester", 0); semester != 0 {
q = q.Where("c.semester = ?", semester)
}
var rows []courseRow
if err := q.Order("c.semester, c.sort_order, c.name, c.id").Scan(&rows).Error; err != nil {
writeOpaque500(w)
return
}
out := make([]courseListJSON, 0, len(rows))
for _, row := range rows {
out = append(out, courseListJSON{courseJSON: newCourseJSON(row.Course), Counts: row.counts()})
}
writeData(w, out)
}
// Course answers GET /courses/{id}: the course with its counters, all its
// class sessions, its readings with the caller's read state and its
// published materials.
func (h *Handlers) Course(w http.ResponseWriter, r *http.Request) {
userID, ok := h.caller(w, r)
if !ok {
return
}
db, ok := h.db(w, r)
if !ok {
return
}
id, ok := surf.IntParam(r, "id")
if !ok {
writeNotFound(w)
return
}
now := h.now()
var rows []courseRow
if err := coursesWithCounts(db, userID, now).Where("c.id = ?", id).Limit(1).Scan(&rows).Error; err != nil {
writeOpaque500(w)
return
}
if len(rows) == 0 {
writeNotFound(w)
return
}
row := rows[0]
var sessions []models.ClassSession
if err := preloadSession(db).Where("course_id = ?", row.ID).Order("starts_at, id").Find(&sessions).Error; err != nil {
writeOpaque500(w)
return
}
readings, err := listReadings(db, userID, row.ID)
if err != nil {
writeOpaque500(w)
return
}
materials, err := publishedMaterialsOfCourse(db, now, row.ID)
if err != nil {
writeOpaque500(w)
return
}
writeData(w, courseDetailJSON{
courseJSON: newCourseJSON(row.Course),
Counts: row.counts(),
Sessions: newSessionsJSON(sessions),
Readings: readings,
Materials: materials,
})
}