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:
118
controllers/api/courses.go
Normal file
118
controllers/api/courses.go
Normal file
@@ -0,0 +1,118 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user