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

@@ -113,7 +113,7 @@ func TestAdminCRUDAgainstDatabase(t *testing.T) {
}
sessionID := createdID(t, adminSend(t, h, http.MethodPost, "/jz/bm/class_sessions", token, map[string]any{
"course": courseID, "meeting": meetingID, "starts_at": "2030-10-05 08:00", "ends_at": "2030-10-05 09:30", "room": "", "is_online": true,
"course": courseID, "meeting": meetingID, "starts_at": "2030-10-05T06:00:00Z", "ends_at": "2030-10-05T07:30:00Z", "room": "", "is_online": true,
}))
var stored time.Time
gdb.Raw(`SELECT starts_at FROM jz_bm_class_sessions WHERE id = ?`, sessionID).Scan(&stored)
@@ -122,11 +122,11 @@ func TestAdminCRUDAgainstDatabase(t *testing.T) {
}
var session models.ClassSession
gdb.Take(&session, sessionID)
if session.CourseID != courseID || session.MeetingID == nil || *session.MeetingID != meetingID || session.Room != nil || !session.IsOnline {
if session.CourseID == nil || *session.CourseID != courseID || session.MeetingID == nil || *session.MeetingID != meetingID || session.Room != nil || !session.IsOnline {
t.Errorf("session = %+v", session)
}
rec := adminSend(t, h, http.MethodPost, "/jz/bm/class_sessions", token, map[string]any{
"course": courseID, "starts_at": "2030-10-05 10:00", "ends_at": "wtorek",
"course": courseID, "starts_at": "2030-10-05T08:00:00Z", "ends_at": "2030-10-05T07:00:00Z",
})
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "ends_at") {
t.Errorf("bad ends_at = %d %s", rec.Code, rec.Body.String())
@@ -140,7 +140,7 @@ func TestAdminCRUDAgainstDatabase(t *testing.T) {
tagID := createdID(t, adminSend(t, h, http.MethodPost, "/jz/bm/tags", token, map[string]any{"name": "Geopolityka", "slug": "geopolityka"}))
materialID := createdID(t, adminSend(t, h, http.MethodPost, "/jz/bm/materials", token, map[string]any{
"title": "Notatka", "type": "notatka", "course": courseID, "tags": []uint{tagID}, "published_at": "", "body": "# Treść",
"title": "Notatka", "type": "notatka", "course": courseID, "tags": []uint{tagID}, "published_at": nil, "body": "# Treść",
}))
var material models.Material
gdb.Preload("Tags").Take(&material, materialID)
@@ -148,13 +148,13 @@ func TestAdminCRUDAgainstDatabase(t *testing.T) {
t.Errorf("material = %+v", material)
}
rec = adminSend(t, h, http.MethodPut, "/jz/bm/materials/"+strconv.Itoa(int(materialID)), token, map[string]any{
"title": "Notatka", "type": "notatka", "published_at": "2026-01-15 12:00",
"title": "Notatka", "type": "notatka", "published_at": "2026-01-15T11:00:00Z",
})
if rec.Code != http.StatusOK {
t.Fatalf("publish = %d %s", rec.Code, rec.Body.String())
}
gdb.Take(&material, materialID)
if material.PublishedAt == nil || *material.PublishedAt != "2026-01-15T12:00:00+01:00" {
if material.PublishedAt == nil || !material.PublishedAt.Equal(time.Date(2026, 1, 15, 11, 0, 0, 0, time.UTC)) {
t.Errorf("published_at = %v", material.PublishedAt)
}
@@ -183,3 +183,64 @@ func adminToken2(t *testing.T, gdb *gorm.DB, h http.Handler) string {
}
return body.Data.AccessToken
}
// TestCourseRelationManagersOnUnsavedCourse creates a reading and a class
// session from the course form before the course exists: they wait without
// a course, stay out of the student API, and get the course on its first
// save with the same form session key.
func TestCourseRelationManagersOnUnsavedCourse(t *testing.T) {
app, plugins, gdb := migrated(t)
h, err := surf.Assemble(app, plugins)
if err != nil {
t.Fatal(err)
}
token := adminToken(t, gdb, h, `{"jz.bm.access_courses":1}`)
key := strings.Repeat("k", 40)
send := func(method, path string, body any) *httptest.ResponseRecorder {
t.Helper()
raw, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(method, adminAPI+path, bytes.NewReader(raw))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept-Language", "en")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Session-Key", key)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
readingID := createdID(t, send(http.MethodPost, "/jz/bm/courses/0/relations/readings/records", map[string]any{
"title": "Lektura w toku", "is_required": true,
}))
sessionID := createdID(t, send(http.MethodPost, "/jz/bm/courses/0/relations/sessions/records", map[string]any{
"starts_at": "2030-10-05T06:00:00Z", "ends_at": "2030-10-05T07:30:00Z", "room": "237",
}))
if rec := send(http.MethodPost, "/jz/bm/courses/0/relations/sessions/records", map[string]any{
"starts_at": "2030-10-05T08:00:00Z", "ends_at": "2030-10-05T07:00:00Z",
}); rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "ends_at") {
t.Errorf("session ending before it starts = %d %s", rec.Code, rec.Body.String())
}
var pending models.ClassSession
gdb.Take(&pending, sessionID)
if pending.CourseID != nil {
t.Fatalf("pending session has course %d", *pending.CourseID)
}
courseID := createdID(t, send(http.MethodPost, "/jz/bm/courses", map[string]any{
"name": "Przedmiot z lekturami", "form": "wyklad", "semester": 1,
}))
var reading models.Reading
gdb.Take(&reading, readingID)
var session models.ClassSession
gdb.Take(&session, sessionID)
if reading.CourseID == nil || *reading.CourseID != courseID || session.CourseID == nil || *session.CourseID != courseID {
t.Errorf("after save: reading course %v, session course %v, want %d", reading.CourseID, session.CourseID, courseID)
}
if !session.StartsAt.Equal(time.Date(2030, 10, 5, 6, 0, 0, 0, time.UTC)) {
t.Errorf("session starts_at = %v", session.StartsAt)
}
}