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:
242
controllers/api/json.go
Normal file
242
controllers/api/json.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package api
|
||||
|
||||
import "git.golem15.com/jakub/sm-bm-plugin/models"
|
||||
|
||||
// courseRef is the short form of a course embedded in other records.
|
||||
type courseRef struct {
|
||||
ID uint `json:"id"`
|
||||
Code *string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Form string `json:"form"`
|
||||
Semester int `json:"semester"`
|
||||
}
|
||||
|
||||
func newCourseRef(c *models.Course) *courseRef {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return &courseRef{ID: c.ID, Code: c.Code, Name: c.Name, Form: c.Form, Semester: c.Semester}
|
||||
}
|
||||
|
||||
// courseJSON is a course's own fields.
|
||||
type courseJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Code *string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Form string `json:"form"`
|
||||
Lecturer *string `json:"lecturer"`
|
||||
Semester int `json:"semester"`
|
||||
ECTS *float64 `json:"ects"`
|
||||
Assessment *string `json:"assessment"`
|
||||
ElectiveGroup *string `json:"elective_group"`
|
||||
Description *string `json:"description"`
|
||||
SyllabusURL *string `json:"syllabus_url"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
func newCourseJSON(c models.Course) courseJSON {
|
||||
return courseJSON{
|
||||
ID: c.ID,
|
||||
Code: c.Code,
|
||||
Name: c.Name,
|
||||
Form: c.Form,
|
||||
Lecturer: c.Lecturer,
|
||||
Semester: c.Semester,
|
||||
ECTS: c.ECTS,
|
||||
Assessment: c.Assessment,
|
||||
ElectiveGroup: c.ElectiveGroup,
|
||||
Description: c.Description,
|
||||
SyllabusURL: c.SyllabusURL,
|
||||
SortOrder: c.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
// courseCounts are the per-course counters of /courses and /courses/{id}.
|
||||
type courseCounts struct {
|
||||
Sessions int64 `json:"sessions"`
|
||||
Readings int64 `json:"readings"`
|
||||
ReadingsRead int64 `json:"readings_read"`
|
||||
Materials int64 `json:"materials"`
|
||||
}
|
||||
|
||||
type courseListJSON struct {
|
||||
courseJSON
|
||||
Counts courseCounts `json:"counts"`
|
||||
}
|
||||
|
||||
type courseDetailJSON struct {
|
||||
courseJSON
|
||||
Counts courseCounts `json:"counts"`
|
||||
Sessions []sessionJSON `json:"sessions"`
|
||||
Readings []readingJSON `json:"readings"`
|
||||
Materials []materialListJSON `json:"materials"`
|
||||
}
|
||||
|
||||
// meetingJSON is one zjazd.
|
||||
type meetingJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Number int `json:"number"`
|
||||
StartsOn string `json:"starts_on"`
|
||||
EndsOn string `json:"ends_on"`
|
||||
Notes *string `json:"notes"`
|
||||
SessionsCount int64 `json:"sessions_count"`
|
||||
}
|
||||
|
||||
func newMeetingJSON(m models.Meeting, sessions int64) meetingJSON {
|
||||
return meetingJSON{ID: m.ID, Number: m.Number, StartsOn: string(m.StartsOn), EndsOn: string(m.EndsOn), Notes: m.Notes, SessionsCount: sessions}
|
||||
}
|
||||
|
||||
// sessionJSON is one class session with its course.
|
||||
type sessionJSON struct {
|
||||
ID uint `json:"id"`
|
||||
CourseID uint `json:"course_id"`
|
||||
MeetingID *uint `json:"meeting_id"`
|
||||
MeetingNumber *int `json:"meeting_number"`
|
||||
StartsAt string `json:"starts_at"`
|
||||
EndsAt string `json:"ends_at"`
|
||||
Room *string `json:"room"`
|
||||
Kind *string `json:"kind"`
|
||||
Notes *string `json:"notes"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
Course *courseRef `json:"course"`
|
||||
}
|
||||
|
||||
func newSessionJSON(s models.ClassSession) sessionJSON {
|
||||
out := sessionJSON{
|
||||
ID: s.ID,
|
||||
CourseID: s.CourseID,
|
||||
MeetingID: s.MeetingID,
|
||||
StartsAt: string(s.StartsAt),
|
||||
EndsAt: string(s.EndsAt),
|
||||
Room: s.Room,
|
||||
Kind: s.Kind,
|
||||
Notes: s.Notes,
|
||||
IsOnline: s.IsOnline,
|
||||
Course: newCourseRef(s.Course),
|
||||
}
|
||||
if s.Meeting != nil {
|
||||
n := s.Meeting.Number
|
||||
out.MeetingNumber = &n
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func newSessionsJSON(rows []models.ClassSession) []sessionJSON {
|
||||
out := make([]sessionJSON, 0, len(rows))
|
||||
for _, s := range rows {
|
||||
out = append(out, newSessionJSON(s))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// deadlineJSON is one terminarz entry.
|
||||
type deadlineJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DueAt string `json:"due_at"`
|
||||
Kind string `json:"kind"`
|
||||
Description *string `json:"description"`
|
||||
CourseID *uint `json:"course_id"`
|
||||
Course *courseRef `json:"course"`
|
||||
}
|
||||
|
||||
func newDeadlinesJSON(rows []models.Deadline) []deadlineJSON {
|
||||
out := make([]deadlineJSON, 0, len(rows))
|
||||
for _, d := range rows {
|
||||
out = append(out, deadlineJSON{
|
||||
ID: d.ID,
|
||||
Title: d.Title,
|
||||
DueAt: string(d.DueAt),
|
||||
Kind: d.Kind,
|
||||
Description: d.Description,
|
||||
CourseID: d.CourseID,
|
||||
Course: newCourseRef(d.Course),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// readingJSON is one reading with the caller's read state.
|
||||
type readingJSON struct {
|
||||
ID uint `json:"id"`
|
||||
CourseID uint `json:"course_id"`
|
||||
Course *courseRef `json:"course"`
|
||||
Title string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Source *string `json:"source"`
|
||||
URL *string `json:"url"`
|
||||
IsRequired bool `json:"is_required"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
ReadAt *string `json:"read_at"`
|
||||
}
|
||||
|
||||
// tagJSON is one material tag.
|
||||
type tagJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
func newTagsJSON(tags []models.Tag) []tagJSON {
|
||||
out := make([]tagJSON, 0, len(tags))
|
||||
for _, t := range tags {
|
||||
out = append(out, tagJSON{ID: t.ID, Name: t.Name, Slug: t.Slug})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// materialListJSON is a published material in a list.
|
||||
type materialListJSON struct {
|
||||
ID uint `json:"id"`
|
||||
CourseID *uint `json:"course_id"`
|
||||
Course *courseRef `json:"course"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Source *string `json:"source"`
|
||||
URL *string `json:"url"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Tags []tagJSON `json:"tags"`
|
||||
FilesCount int64 `json:"files_count"`
|
||||
}
|
||||
|
||||
func newMaterialListJSON(m models.Material, files int64) materialListJSON {
|
||||
out := materialListJSON{
|
||||
ID: m.ID,
|
||||
CourseID: m.CourseID,
|
||||
Course: newCourseRef(m.Course),
|
||||
Title: m.Title,
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
URL: m.URL,
|
||||
Tags: newTagsJSON(m.Tags),
|
||||
FilesCount: files,
|
||||
}
|
||||
if m.PublishedAt != nil {
|
||||
out.PublishedAt = string(*m.PublishedAt)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// materialDetailJSON is a published material with its body and files.
|
||||
type materialDetailJSON struct {
|
||||
materialListJSON
|
||||
Body *string `json:"body"`
|
||||
Files []fileJSON `json:"files"`
|
||||
}
|
||||
|
||||
// fileJSON is one attached file.
|
||||
type fileJSON struct {
|
||||
ID uint `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Mime string `json:"mime"`
|
||||
}
|
||||
|
||||
// readingsSummary is the reading progress block of /dashboard.
|
||||
type readingsSummary struct {
|
||||
Total int64 `json:"total"`
|
||||
Read int64 `json:"read"`
|
||||
RequiredTotal int64 `json:"required_total"`
|
||||
RequiredRead int64 `json:"required_read"`
|
||||
}
|
||||
Reference in New Issue
Block a user