feat: zjazd (meeting) on readings and materials

The Pulpit hero lists what to read for the current or next zjazd, so
readings and materials get a nullable meeting_id (FK to jz_bm_meetings,
ON DELETE SET NULL, indexed). The reading admin form gets a "Na zjazd"
picker, the student reading JSON carries meeting_id, and the management
/readings records filter and validate it.
This commit is contained in:
Jakub Zych
2026-10-27 01:41:48 +01:00
parent 8843e73f98
commit 3241cc9894
15 changed files with 94 additions and 15 deletions

View File

@@ -173,6 +173,7 @@ 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"`

View File

@@ -0,0 +1,28 @@
package api
import (
"encoding/json"
"strings"
"testing"
)
// TestReadingJSONCarriesMeetingID checks that meeting_id is always present
// in a reading, as a number or as null.
func TestReadingJSONCarriesMeetingID(t *testing.T) {
id := uint(4)
for _, tc := range []struct {
meeting *uint
want string
}{
{&id, `"meeting_id":4`},
{nil, `"meeting_id":null`},
} {
b, err := json.Marshal(readingJSON{ID: 1, Title: "Lektura", MeetingID: tc.meeting})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), tc.want) {
t.Errorf("reading JSON %s does not contain %s", b, tc.want)
}
}
}

View File

@@ -42,6 +42,7 @@ func listReadings(db *gorm.DB, userID, courseID uint) ([]readingJSON, error) {
ID: row.ID,
CourseID: idOf(row.CourseID),
Course: newCourseRef(courses[idOf(row.CourseID)]),
MeetingID: row.MeetingID,
Title: row.Title,
Author: row.Author,
Source: row.Source,

View File

@@ -33,13 +33,7 @@ func (classSessionsAdmin) RequiredPermissions() []string {
func (classSessionsAdmin) AdminFieldRelations() []cabana.FieldRelationContract {
return []cabana.FieldRelationContract{
courseRelation("course_id"),
{
Field: "meeting",
Kind: "belongsTo",
NewRelated: func() any { return &models.Meeting{} },
ForeignKey: "meeting_id",
LabelColumn: "number",
},
meetingRelation(),
}
}

View File

@@ -54,3 +54,14 @@ func courseRelation(foreignKey string) cabana.FieldRelationContract {
LabelColumn: "name",
}
}
// meetingRelation is the belongsTo picker of a meeting_id column (zjazd).
func meetingRelation() cabana.FieldRelationContract {
return cabana.FieldRelationContract{
Field: "meeting",
Kind: "belongsTo",
NewRelated: func() any { return &models.Meeting{} },
ForeignKey: "meeting_id",
LabelColumn: "number",
}
}

View File

@@ -29,9 +29,9 @@ func (readingsAdmin) RequiredPermissions() []string {
return []string{PermCourses}
}
// AdminFieldRelations binds the course picker.
// AdminFieldRelations binds the course and meeting pickers.
func (readingsAdmin) AdminFieldRelations() []cabana.FieldRelationContract {
return []cabana.FieldRelationContract{courseRelation("course_id")}
return []cabana.FieldRelationContract{courseRelation("course_id"), meetingRelation()}
}
// ReadingsController returns the jz.bm.readings admin controller.