Files
sm-bm-plugin/controllers/api/json.go
Jakub Zych 418f8240a4 feat: private materials with per-user shares
A material may be private: only its author and the users it is shared
with see it. Every student read (list, detail, course detail and
counters, the checklist linked material, the token twins) goes through
one visibility fragment. Existing materials stay public with no author.

GET/PUT /_bm/api/v1/materials/{id}/access let the author, a material
manager, toggle the flag and pick the users; the first change of an
author-less material makes the caller its author.
2026-11-08 11:52:47 +01:00

263 lines
7.5 KiB
Go

package api
import "git.golem15.com/jakub/sm-bm-plugin/models"
// idOf is the value of an optional id, 0 when it is nil. The API serves only
// sessions and readings that have a course, so their course_id is set.
func idOf(id *uint) uint {
if id == nil {
return 0
}
return *id
}
// 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"`
Lecturer *string `json:"lecturer"`
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, Lecturer: c.Lecturer, 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: m.StartsOn.String(), EndsOn: m.EndsOn.String(), 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"`
Lecturer *string `json:"lecturer"`
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: idOf(s.CourseID),
MeetingID: s.MeetingID,
StartsAt: models.FormatDateTime(s.StartsAt),
EndsAt: models.FormatDateTime(s.EndsAt),
Room: s.Room,
Kind: s.Kind,
Lecturer: s.Lecturer,
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: models.FormatDateTime(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 and owned state.
type readingJSON struct {
ID uint `json:"id"`
CourseID uint `json:"course_id"`
Course *courseRef `json:"course"`
MeetingID *uint `json:"meeting_id"`
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"`
OwnedAt *string `json:"owned_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 the caller may see, in a list.
type materialListJSON struct {
ID uint `json:"id"`
CourseID *uint `json:"course_id"`
Course *courseRef `json:"course"`
MeetingID *uint `json:"meeting_id"`
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"`
IsPrivate bool `json:"is_private"`
}
func newMaterialListJSON(m models.Material, files int64) materialListJSON {
out := materialListJSON{
ID: m.ID,
CourseID: m.CourseID,
Course: newCourseRef(m.Course),
MeetingID: m.MeetingID,
Title: m.Title,
Type: m.Type,
Source: m.Source,
URL: m.URL,
Tags: newTagsJSON(m.Tags),
FilesCount: files,
IsPrivate: m.IsPrivate,
}
if m.PublishedAt != nil {
out.PublishedAt = models.FormatDateTime(*m.PublishedAt)
}
return out
}
// materialDetailJSON is a published material the caller may see, with its
// body and files. CanManage is true for the caller who may change its access.
type materialDetailJSON struct {
materialListJSON
Body *string `json:"body"`
Files []fileJSON `json:"files"`
CanManage bool `json:"can_manage"`
}
// 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"`
}