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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user