Adopt SummerCMS v0.1.1 admin features

Datepickers on native time.Time / lagoon.Date fields, file uploads on
materials, readings and class sessions managed from the course form
(course_id made nullable).
This commit is contained in:
Jakub Zych
2026-10-03 06:03:34 +02:00
parent ac2e2ddd7b
commit 71b848a376
39 changed files with 475 additions and 373 deletions

View File

@@ -75,7 +75,7 @@ func (h *Handlers) newFeedJSON(r *http.Request, f *models.CalendarFeed) calendar
IncludeDeadlines: f.IncludeDeadlines,
}
if f.LastFetchedAt != nil {
s := string(models.NewDateTime(*f.LastFetchedAt))
s := models.FormatDateTime(*f.LastFetchedAt)
out.LastFetchedAt = &s
}
return out

View File

@@ -1,6 +1,7 @@
package api
import (
"git.golem15.com/golem15/summercms/modules/lagoon"
"net/http"
"git.golem15.com/jakub/sm-bm-plugin/models"
@@ -46,7 +47,7 @@ func (h *Handlers) Dashboard(w http.ResponseWriter, r *http.Request) {
}
out.UpcomingDeadlines = newDeadlinesJSON(deadlines)
day := today(h.now()).Format(models.DateLayout)
day := lagoon.DateOf(today(h.now())).String()
var err error
if out.CurrentMeeting, err = meetingWhere(db, "starts_on <= ? AND ends_on >= ?", day, day); err != nil {
writeOpaque500(w)

View File

@@ -2,6 +2,15 @@ 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"`
@@ -83,7 +92,7 @@ type meetingJSON struct {
}
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}
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.
@@ -104,10 +113,10 @@ type sessionJSON struct {
func newSessionJSON(s models.ClassSession) sessionJSON {
out := sessionJSON{
ID: s.ID,
CourseID: s.CourseID,
CourseID: idOf(s.CourseID),
MeetingID: s.MeetingID,
StartsAt: string(s.StartsAt),
EndsAt: string(s.EndsAt),
StartsAt: models.FormatDateTime(s.StartsAt),
EndsAt: models.FormatDateTime(s.EndsAt),
Room: s.Room,
Kind: s.Kind,
Notes: s.Notes,
@@ -146,7 +155,7 @@ func newDeadlinesJSON(rows []models.Deadline) []deadlineJSON {
out = append(out, deadlineJSON{
ID: d.ID,
Title: d.Title,
DueAt: string(d.DueAt),
DueAt: models.FormatDateTime(d.DueAt),
Kind: d.Kind,
Description: d.Description,
CourseID: d.CourseID,
@@ -212,7 +221,7 @@ func newMaterialListJSON(m models.Material, files int64) materialListJSON {
FilesCount: files,
}
if m.PublishedAt != nil {
out.PublishedAt = string(*m.PublishedAt)
out.PublishedAt = models.FormatDateTime(*m.PublishedAt)
}
return out
}

View File

@@ -99,11 +99,11 @@ func dayStart(in map[string]any, name string) (time.Time, bool) {
if s == "" {
return time.Time{}, false
}
t, err := models.ParseDate(s)
d, err := models.ParseDate(s)
if err != nil {
return time.Time{}, false
}
return t, true
return d.Time(models.Location), true
}
// today returns midnight of now's calendar day in Europe/Warsaw.

View File

@@ -37,8 +37,8 @@ func listReadings(db *gorm.DB, userID, courseID uint) ([]readingJSON, error) {
for _, row := range rows {
item := readingJSON{
ID: row.ID,
CourseID: row.CourseID,
Course: newCourseRef(courses[row.CourseID]),
CourseID: idOf(row.CourseID),
Course: newCourseRef(courses[idOf(row.CourseID)]),
Title: row.Title,
Author: row.Author,
Source: row.Source,
@@ -47,7 +47,7 @@ func listReadings(db *gorm.DB, userID, courseID uint) ([]readingJSON, error) {
SortOrder: row.SortOrder,
}
if row.ReadAt != nil {
s := string(models.NewDateTime(*row.ReadAt))
s := models.FormatDateTime(*row.ReadAt)
item.ReadAt = &s
}
out = append(out, item)
@@ -59,9 +59,9 @@ func readingCourseIDs(rows []readingRow) []uint {
seen := map[uint]bool{}
var ids []uint
for _, row := range rows {
if !seen[row.CourseID] {
seen[row.CourseID] = true
ids = append(ids, row.CourseID)
if id := idOf(row.CourseID); !seen[id] {
seen[id] = true
ids = append(ids, id)
}
}
return ids
@@ -121,7 +121,7 @@ func readingFor(w http.ResponseWriter, r *http.Request, db *gorm.DB) (uint, bool
return 0, false
}
var reading models.Reading
res := db.Select("id").Where("id = ?", id).Limit(1).Find(&reading)
res := db.Select("id").Where("id = ? AND course_id IS NOT NULL", id).Limit(1).Find(&reading)
if res.Error != nil {
writeOpaque500(w)
return 0, false
@@ -160,7 +160,7 @@ ON CONFLICT (reading_id, user_id) DO NOTHING`, readingID, userID, now).Error
writeOpaque500(w)
return
}
readAt := string(models.NewDateTime(progress.ReadAt))
readAt := models.FormatDateTime(progress.ReadAt)
writeData(w, progressJSON{ReadingID: readingID, ReadAt: &readAt})
}

View File

@@ -32,8 +32,11 @@ func bounds(in map[string]any, lower time.Time) (time.Time, *time.Time) {
return lower, nil
}
// preloadSession queries the class sessions students see, with their course
// and meeting. A session without a course is still pending on an unsaved
// course form and is left out.
func preloadSession(q *gorm.DB) *gorm.DB {
return q.Preload("Course").Preload("Meeting")
return q.Preload("Course").Preload("Meeting").Where("jz_bm_class_sessions.course_id IS NOT NULL")
}
// Schedule answers GET /schedule?from=YYYY-MM-DD&to=YYYY-MM-DD: the class