diff --git a/controllers/api/json.go b/controllers/api/json.go index 98eccf7..79a143f 100644 --- a/controllers/api/json.go +++ b/controllers/api/json.go @@ -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"` diff --git a/controllers/api/json_test.go b/controllers/api/json_test.go new file mode 100644 index 0000000..5aee156 --- /dev/null +++ b/controllers/api/json_test.go @@ -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) + } + } +} diff --git a/controllers/api/readings.go b/controllers/api/readings.go index cdf5749..05433b9 100644 --- a/controllers/api/readings.go +++ b/controllers/api/readings.go @@ -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, diff --git a/controllers/class_sessions.go b/controllers/class_sessions.go index 290c15f..9025db2 100644 --- a/controllers/class_sessions.go +++ b/controllers/class_sessions.go @@ -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(), } } diff --git a/controllers/hooks.go b/controllers/hooks.go index b26acb6..722103a 100644 --- a/controllers/hooks.go +++ b/controllers/hooks.go @@ -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", + } +} diff --git a/controllers/readings.go b/controllers/readings.go index 7a81d92..785bb48 100644 --- a/controllers/readings.go +++ b/controllers/readings.go @@ -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. diff --git a/lang/en/lang.yaml b/lang/en/lang.yaml index 5f71fb8..61dd874 100644 --- a/lang/en/lang.yaml +++ b/lang/en/lang.yaml @@ -23,6 +23,8 @@ common: sort_order: Sort order created_at: Created none: — none — + meeting: For meeting + meeting_comment: The dashboard suggests this item before that meeting. forms: wyklad: Lecture cwiczenia: Tutorial diff --git a/lang/pl/lang.yaml b/lang/pl/lang.yaml index 361e9e0..cb190ad 100644 --- a/lang/pl/lang.yaml +++ b/lang/pl/lang.yaml @@ -23,6 +23,8 @@ common: sort_order: Kolejność created_at: Utworzono none: — brak — + meeting: Na zjazd + meeting_comment: Pulpit poleca tę pozycję przed tym zjazdem. forms: wyklad: Wykład cwiczenia: Ćwiczenia diff --git a/models/reading.go b/models/reading.go index af0f178..e7bd158 100644 --- a/models/reading.go +++ b/models/reading.go @@ -13,6 +13,8 @@ type Reading struct { ID uint `gorm:"column:id;primaryKey"` CourseID *uint `gorm:"column:course_id"` Course *Course `gorm:"foreignKey:CourseID"` + MeetingID *uint `gorm:"column:meeting_id"` + Meeting *Meeting `gorm:"foreignKey:MeetingID"` Title string `gorm:"column:title"` Author *string `gorm:"column:author"` Source *string `gorm:"column:source"` @@ -28,7 +30,7 @@ func (Reading) TableName() string { return "jz_bm_readings" } // Fillable lists the columns mass assignment may set. func (Reading) Fillable() []string { - return []string{"course_id", "title", "author", "source", "url", "is_required", "sort_order"} + return []string{"course_id", "meeting_id", "title", "author", "source", "url", "is_required", "sort_order"} } // Rules are checked by the admin API on every save. diff --git a/models/readings/fields.yaml b/models/readings/fields.yaml index 729e7c2..7b2e10e 100644 --- a/models/readings/fields.yaml +++ b/models/readings/fields.yaml @@ -31,3 +31,10 @@ fields: type: number span: left default: 0 + meeting: + label: jz.bm::lang.common.meeting + type: relation + nameFrom: number + span: right + emptyOption: jz.bm::lang.common.none + comment: jz.bm::lang.common.meeting_comment diff --git a/models/readings/fields_course.yaml b/models/readings/fields_course.yaml index 3a09c23..8974f06 100644 --- a/models/readings/fields_course.yaml +++ b/models/readings/fields_course.yaml @@ -1,5 +1,6 @@ # The reading form inside the course form's relation manager: the course is -# the parent, so there is no course picker. +# the parent, so there is no course picker, and a relation form has no +# relation pickers, so the meeting (zjazd) is set on the readings screen. fields: title: label: jz.bm::lang.common.title diff --git a/plugin_test.go b/plugin_test.go index 2b4ca9e..563f087 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -77,8 +77,8 @@ func TestPluginActivates(t *testing.T) { if n := len(p.Models()); n != 11 { t.Errorf("Models = %d, want 11", n) } - if n := len(p.Migrations()); n != 15 { - t.Errorf("Migrations = %d, want 15", n) + if n := len(p.Migrations()); n != 16 { + t.Errorf("Migrations = %d, want 16", n) } if !slices.ContainsFunc(p.Commands(), func(c bonfire.Command) bool { return c.Name == "bm:seed" }) { t.Error("bm:seed is not registered") diff --git a/registry.gen.go b/registry.gen.go index a184189..094e41a 100644 --- a/registry.gen.go +++ b/registry.gen.go @@ -44,6 +44,7 @@ func generatedMigrations() []*gormigrate.Migration { updates.AddLecturerToClassSessions(), updates.CreateReadingOwnership(), updates.SeedChatGroups(), + updates.AddMeetingToReadingsAndMaterials(), } } diff --git a/routes.go b/routes.go index b1ce478..91f2f03 100644 --- a/routes.go +++ b/routes.go @@ -166,8 +166,8 @@ func (p *Plugin) Routes(r pact.Router) error { References: map[string]string{"course_id": "jz_bm_courses", "meeting_id": "jz_bm_meetings"}, }, true) mountRecords(g, h, "/readings", controllers.PermCourses, api.Record[models.Reading, *models.Reading]{ - Filters: []string{"course_id", "is_required"}, Order: "course_id, sort_order, id", - References: map[string]string{"course_id": "jz_bm_courses"}, + Filters: []string{"course_id", "meeting_id", "is_required"}, Order: "course_id, sort_order, id", + References: map[string]string{"course_id": "jz_bm_courses", "meeting_id": "jz_bm_meetings"}, }, true) mountRecords(g, h, "/deadlines", controllers.PermSchedule, api.Record[models.Deadline, *models.Deadline]{ Filters: []string{"course_id", "kind"}, Order: "due_at, id", diff --git a/updates/20261027120000_add_meeting_to_readings_and_materials.go b/updates/20261027120000_add_meeting_to_readings_and_materials.go new file mode 100644 index 0000000..547f520 --- /dev/null +++ b/updates/20261027120000_add_meeting_to_readings_and_materials.go @@ -0,0 +1,29 @@ +package updates + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// AddMeetingToReadingsAndMaterials returns the +// 20261027120000_add_meeting_to_readings_and_materials gormigrate entry: a +// reading or a material may be assigned to the zjazd (meeting) it should be +// read for, so the dashboard can suggest it before that zjazd. Deleting the +// meeting keeps the rows and clears the assignment. +func AddMeetingToReadingsAndMaterials() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "20261027120000_add_meeting_to_readings_and_materials", + Migrate: func(tx *gorm.DB) error { + return execAll(tx, ` +ALTER TABLE jz_bm_readings ADD COLUMN meeting_id BIGINT NULL REFERENCES jz_bm_meetings (id) ON DELETE SET NULL; +CREATE INDEX jz_bm_readings_meeting_id_index ON jz_bm_readings (meeting_id); +ALTER TABLE jz_bm_materials ADD COLUMN meeting_id BIGINT NULL REFERENCES jz_bm_meetings (id) ON DELETE SET NULL; +CREATE INDEX jz_bm_materials_meeting_id_index ON jz_bm_materials (meeting_id)`) + }, + Rollback: func(tx *gorm.DB) error { + return execAll(tx, ` +ALTER TABLE jz_bm_readings DROP COLUMN IF EXISTS meeting_id; +ALTER TABLE jz_bm_materials DROP COLUMN IF EXISTS meeting_id`) + }, + } +}