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

@@ -69,7 +69,7 @@ func BuildCalendar(data CalendarData, opt CalendarOptions) []byte {
w.Prop("REFRESH-INTERVAL;VALUE=DURATION", "PT6H")
w.Prop("X-PUBLISHED-TTL", "PT6H")
for _, s := range data.Sessions {
if excluded[s.CourseID] {
if s.CourseID == nil || excluded[*s.CourseID] {
continue
}
writeSession(&w, s, opt.SiteURL)
@@ -129,8 +129,8 @@ func writeSession(w *ICS, s models.ClassSession, siteURL string) {
w.Prop("UID", uid("session", s.ID))
stamp(w, s.UpdatedAt)
w.Prop("SEQUENCE", "0")
w.UTC("DTSTART", s.StartsAt.MustTime())
w.UTC("DTEND", s.EndsAt.MustTime())
w.UTC("DTSTART", s.StartsAt)
w.UTC("DTEND", s.EndsAt)
w.Text("SUMMARY", SessionSummary(s))
switch {
case s.IsOnline:
@@ -152,7 +152,7 @@ func writeSession(w *ICS, s models.ClassSession, siteURL string) {
lines = append(lines, "Przedmiot do wyboru")
}
if siteURL != "" {
link := siteURL + "/przedmioty/" + strconv.FormatUint(uint64(s.CourseID), 10)
link := siteURL + "/przedmioty/" + strconv.FormatUint(uint64(*s.CourseID), 10)
lines = append(lines, link)
w.Prop("URL", link)
}
@@ -180,12 +180,13 @@ func MeetingSummary(m models.Meeting) string {
}
func writeMeeting(w *ICS, m models.Meeting) {
start, err := m.StartsOn.Time()
if err != nil {
if m.StartsOn.IsZero() {
return
}
end, err := m.EndsOn.Time()
if err != nil || end.Before(start) {
// Midnight UTC keeps the calendar date for w.Date.
start := m.StartsOn.Time(nil)
end := m.EndsOn.Time(nil)
if m.EndsOn.IsZero() || end.Before(start) {
end = start
}
w.Begin("VEVENT")
@@ -204,8 +205,8 @@ func writeMeeting(w *ICS, m models.Meeting) {
}
func writeDeadline(w *ICS, d models.Deadline) {
due, err := d.DueAt.Time()
if err != nil {
due := d.DueAt
if due.IsZero() {
return
}
summary := "Termin: " + d.Title

View File

@@ -5,6 +5,7 @@ import (
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/jakub/sm-bm-plugin/classes"
"git.golem15.com/jakub/sm-bm-plugin/models"
)
@@ -17,19 +18,19 @@ func calendarFixture() classes.CalendarData {
glob := &models.Course{ID: 1, Name: "Globalizacja i regionalizacja", Form: models.FormWyklad, Lecturer: sp("prof. dr hab. Piotr Mikiewicz")}
hist := &models.Course{ID: 8, Name: "Historia wojen i konfliktów do 1945 roku", Form: models.FormInne, ElectiveGroup: sp("historia-koncepcje")}
konc := &models.Course{ID: 9, Name: "Koncepcje wojny i pokoju – historia i współczesność", Form: models.FormInne, ElectiveGroup: sp("historia-koncepcje")}
m3 := &models.Meeting{ID: 3, Number: 3, StartsOn: "2026-10-24", EndsOn: "2026-10-25", Notes: sp("stacjonarny (Koszarowa 3, bud. 21)"), UpdatedAt: updated}
m2 := &models.Meeting{ID: 2, Number: 2, StartsOn: "2026-10-17", EndsOn: "2026-10-18", Notes: sp("online"), UpdatedAt: updated}
m3 := &models.Meeting{ID: 3, Number: 3, StartsOn: day("2026-10-24"), EndsOn: day("2026-10-25"), Notes: sp("stacjonarny (Koszarowa 3, bud. 21)"), UpdatedAt: updated}
m2 := &models.Meeting{ID: 2, Number: 2, StartsOn: day("2026-10-17"), EndsOn: day("2026-10-18"), Notes: sp("online"), UpdatedAt: updated}
return classes.CalendarData{
Sessions: []models.ClassSession{
{ID: 11, CourseID: 1, Course: glob, MeetingID: up(3), Meeting: m3, StartsAt: "2026-10-24T08:00:00+02:00", EndsAt: "2026-10-24T09:30:00+02:00", Room: sp("237"), Notes: sp("prof. P. Mikiewicz"), UpdatedAt: updated},
{ID: 12, CourseID: 8, Course: hist, MeetingID: up(3), Meeting: m3, StartsAt: "2026-10-25T08:00:00+01:00", EndsAt: "2026-10-25T09:30:00+01:00", Room: sp("110"), UpdatedAt: updated},
{ID: 13, CourseID: 9, Course: konc, MeetingID: up(2), Meeting: m2, StartsAt: "2026-10-17T10:00:00+02:00", EndsAt: "2026-10-17T11:30:00+02:00", IsOnline: true, Room: sp("237"), UpdatedAt: updated},
{ID: 11, CourseID: up(1), Course: glob, MeetingID: up(3), Meeting: m3, StartsAt: ts("2026-10-24T08:00:00+02:00"), EndsAt: ts("2026-10-24T09:30:00+02:00"), Room: sp("237"), Notes: sp("prof. P. Mikiewicz"), UpdatedAt: updated},
{ID: 12, CourseID: up(8), Course: hist, MeetingID: up(3), Meeting: m3, StartsAt: ts("2026-10-25T08:00:00+01:00"), EndsAt: ts("2026-10-25T09:30:00+01:00"), Room: sp("110"), UpdatedAt: updated},
{ID: 13, CourseID: up(9), Course: konc, MeetingID: up(2), Meeting: m2, StartsAt: ts("2026-10-17T10:00:00+02:00"), EndsAt: ts("2026-10-17T11:30:00+02:00"), IsOnline: true, Room: sp("237"), UpdatedAt: updated},
},
Meetings: []models.Meeting{*m2, *m3},
Deadlines: []models.Deadline{
{ID: 21, Title: "Esej; wersja 1, szkic", DueAt: "2026-11-30T23:59:00+01:00", CourseID: up(1), Course: glob, Description: sp("Wysłać\nmailem"), UpdatedAt: updated},
{ID: 22, Title: "Esej z koncepcji", DueAt: "2026-12-01T12:00:00+01:00", CourseID: up(9), Course: konc, UpdatedAt: updated},
{ID: 23, Title: "Ślubowanie", DueAt: "2026-10-05T23:59:00+02:00", UpdatedAt: updated},
{ID: 21, Title: "Esej; wersja 1, szkic", DueAt: ts("2026-11-30T23:59:00+01:00"), CourseID: up(1), Course: glob, Description: sp("Wysłać\nmailem"), UpdatedAt: updated},
{ID: 22, Title: "Esej z koncepcji", DueAt: ts("2026-12-01T12:00:00+01:00"), CourseID: up(9), Course: konc, UpdatedAt: updated},
{ID: 23, Title: "Ślubowanie", DueAt: ts("2026-10-05T23:59:00+02:00"), UpdatedAt: updated},
},
}
}
@@ -211,3 +212,19 @@ func TestSummaries(t *testing.T) {
t.Errorf("meeting without notes = %q", got)
}
}
func day(s string) lagoon.Date {
d, err := lagoon.ParseDate(s)
if err != nil {
panic(err)
}
return d
}
func ts(s string) time.Time {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
panic(err)
}
return t
}

View File

@@ -9,6 +9,7 @@ import (
"io"
"slices"
"strings"
"time"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/jakub/sm-bm-plugin/models"
@@ -191,7 +192,7 @@ func (f *SeedFile) Validate() error {
if err2 != nil {
add("%s: ends_on: %v", at, err2)
}
if err1 == nil && err2 == nil && end.Before(start) {
if err1 == nil && err2 == nil && end.Time(nil).Before(start.Time(nil)) {
add("%s: ends_on is before starts_on", at)
}
}
@@ -314,10 +315,8 @@ func SeedWith(ctx context.Context, db *gorm.DB, f *SeedFile, opts SeedOptions) (
m := models.Meeting{}
created, changed, err := upsert(tx, &m, clause.Eq{Column: "number", Value: in.Number}, func(m *models.Meeting) {
m.Number = in.Number
start, _ := models.ParseDate(in.StartsOn)
end, _ := models.ParseDate(in.EndsOn)
m.StartsOn = models.NewDate(start)
m.EndsOn = models.NewDate(end)
m.StartsOn, _ = models.ParseDate(in.StartsOn)
m.EndsOn, _ = models.ParseDate(in.EndsOn)
m.Notes = nz(in.Notes)
})
if err != nil {
@@ -366,15 +365,16 @@ func SeedWith(ctx context.Context, db *gorm.DB, f *SeedFile, opts SeedOptions) (
clause.Eq{Column: "starts_at", Value: start.UTC()},
)
created, changed, err := upsert(tx, &s, where, func(s *models.ClassSession) {
s.CourseID = courseIDs[in.Course]
courseID := courseIDs[in.Course]
s.CourseID = &courseID
s.MeetingID = nil
if in.Meeting != nil {
id := meetingIDs[*in.Meeting]
s.MeetingID = &id
}
s.StartsAt = models.NewDateTime(start)
end, _ := models.ParseDateTime(in.EndsAt)
s.EndsAt = models.NewDateTime(end)
setInstant(&s.StartsAt, start)
setInstant(&s.EndsAt, end)
s.Room = nz(in.Room)
s.Kind = nz(in.Kind)
s.IsOnline = in.IsOnline
@@ -396,7 +396,7 @@ func SeedWith(ctx context.Context, db *gorm.DB, f *SeedFile, opts SeedOptions) (
)
created, changed, err := upsert(tx, &d, where, func(d *models.Deadline) {
d.Title = in.Title
d.DueAt = models.NewDateTime(due)
setInstant(&d.DueAt, due)
d.CourseID = nil
if in.Course != nil {
id := courseIDs[*in.Course]
@@ -419,7 +419,8 @@ func SeedWith(ctx context.Context, db *gorm.DB, f *SeedFile, opts SeedOptions) (
clause.Eq{Column: "title", Value: in.Title},
)
created, changed, err := upsert(tx, &r, where, func(r *models.Reading) {
r.CourseID = courseIDs[in.Course]
courseID := courseIDs[in.Course]
r.CourseID = &courseID
r.Title = in.Title
r.Author = nz(in.Author)
r.Source = nz(in.Source)
@@ -486,7 +487,7 @@ func prune(tx *gorm.DB, sum *SeedSummary, semesters map[int]bool, courseIDs map[
return err
}
for _, s := range sessions {
sum.Removed = append(sum.Removed, fmt.Sprintf("class session of course %d at %s", s.CourseID, string(s.StartsAt)))
sum.Removed = append(sum.Removed, fmt.Sprintf("class session of course %d at %s", s.CourseID, models.FormatDateTime(s.StartsAt)))
if err := tx.Delete(&models.ClassSession{}, s.ID).Error; err != nil {
return fmt.Errorf("prune class session %d: %w", s.ID, err)
}
@@ -520,6 +521,15 @@ func nz(s *string) *string {
// an insert when there was no row, an update when set changed a column, and
// nothing otherwise. The comparison is on the row's own fields, before and
// after set, as JSON.
// setInstant sets *dst to t unless it already holds the same instant, so a
// time read back from the database in another zone does not count as a
// change in upsert.
func setInstant(dst *time.Time, t time.Time) {
if !dst.Equal(t) {
*dst = t
}
}
func upsert[T any](tx *gorm.DB, row *T, where clause.Expression, set func(*T)) (created, changed bool, err error) {
res := tx.Clauses(where).Limit(1).Find(row)
if res.Error != nil {