From 577fa14d3d94c028f4c6692f0be953cad25d552c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 2 Oct 2026 14:26:26 +0200 Subject: [PATCH] feat: add admin screens, permissions and navigation Seven admin controllers in a "Studia BM" navigation group, guarded by jz.bm.access_courses, jz.bm.access_schedule and jz.bm.access_materials, with Polish and English labels. Date and time fields are checked by the models before a save and answered as 422 on the field. --- controllers/admin_test.go | 35 ++++++++ controllers/class_sessions.go | 47 ++++++++++ controllers/class_sessions/config_form.yaml | 12 +++ controllers/class_sessions/config_list.yaml | 15 ++++ controllers/courses.go | 38 ++++++++ controllers/courses/config_form.yaml | 12 +++ controllers/courses/config_list.yaml | 15 ++++ controllers/deadlines.go | 38 ++++++++ controllers/deadlines/config_form.yaml | 12 +++ controllers/deadlines/config_list.yaml | 15 ++++ controllers/hooks.go | 55 ++++++++++++ controllers/materials.go | 53 +++++++++++ controllers/materials/config_form.yaml | 12 +++ controllers/materials/config_list.yaml | 15 ++++ controllers/meetings.go | 32 +++++++ controllers/meetings/config_form.yaml | 12 +++ controllers/meetings/config_list.yaml | 15 ++++ controllers/readings.go | 38 ++++++++ controllers/readings/config_form.yaml | 12 +++ controllers/readings/config_list.yaml | 15 ++++ controllers/tags.go | 32 +++++++ controllers/tags/config_form.yaml | 12 +++ controllers/tags/config_list.yaml | 15 ++++ lang/en/lang.yaml | 98 ++++++++++++++++++++- lang/pl/lang.yaml | 97 ++++++++++++++++++++ models/class_sessions/columns.yaml | 16 ++++ models/class_sessions/fields.yaml | 41 +++++++++ models/courses/columns.yaml | 17 ++++ models/courses/fields.yaml | 58 ++++++++++++ models/deadlines/columns.yaml | 12 +++ models/deadlines/fields.yaml | 33 +++++++ models/materials/columns.yaml | 15 ++++ models/materials/fields.yaml | 46 ++++++++++ models/meetings/columns.yaml | 11 +++ models/meetings/fields.yaml | 22 +++++ models/readings/columns.yaml | 17 ++++ models/readings/fields.yaml | 33 +++++++ models/tags/columns.yaml | 7 ++ models/tags/fields.yaml | 11 +++ plugin.go | 73 +++++++++++++-- registry.gen.go | 38 +++++++- updates/updates_test.go | 43 +++++++++ 42 files changed, 1234 insertions(+), 11 deletions(-) create mode 100644 controllers/admin_test.go create mode 100644 controllers/class_sessions.go create mode 100644 controllers/class_sessions/config_form.yaml create mode 100644 controllers/class_sessions/config_list.yaml create mode 100644 controllers/courses.go create mode 100644 controllers/courses/config_form.yaml create mode 100644 controllers/courses/config_list.yaml create mode 100644 controllers/deadlines.go create mode 100644 controllers/deadlines/config_form.yaml create mode 100644 controllers/deadlines/config_list.yaml create mode 100644 controllers/hooks.go create mode 100644 controllers/materials.go create mode 100644 controllers/materials/config_form.yaml create mode 100644 controllers/materials/config_list.yaml create mode 100644 controllers/meetings.go create mode 100644 controllers/meetings/config_form.yaml create mode 100644 controllers/meetings/config_list.yaml create mode 100644 controllers/readings.go create mode 100644 controllers/readings/config_form.yaml create mode 100644 controllers/readings/config_list.yaml create mode 100644 controllers/tags.go create mode 100644 controllers/tags/config_form.yaml create mode 100644 controllers/tags/config_list.yaml create mode 100644 lang/pl/lang.yaml create mode 100644 models/class_sessions/columns.yaml create mode 100644 models/class_sessions/fields.yaml create mode 100644 models/courses/columns.yaml create mode 100644 models/courses/fields.yaml create mode 100644 models/deadlines/columns.yaml create mode 100644 models/deadlines/fields.yaml create mode 100644 models/materials/columns.yaml create mode 100644 models/materials/fields.yaml create mode 100644 models/meetings/columns.yaml create mode 100644 models/meetings/fields.yaml create mode 100644 models/readings/columns.yaml create mode 100644 models/readings/fields.yaml create mode 100644 models/tags/columns.yaml create mode 100644 models/tags/fields.yaml create mode 100644 updates/updates_test.go diff --git a/controllers/admin_test.go b/controllers/admin_test.go new file mode 100644 index 0000000..df2011b --- /dev/null +++ b/controllers/admin_test.go @@ -0,0 +1,35 @@ +package controllers + +import ( + "context" + "errors" + "testing" + + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +func TestTemporalHooksAnswerOnTheField(t *testing.T) { + var hooks temporalHooks + bad := &models.ClassSession{StartsAt: "2026-10-03 08:00", EndsAt: "wtorek"} + for name, fn := range map[string]func(context.Context, any) error{ + "create": hooks.FormBeforeCreate, + "update": hooks.FormBeforeUpdate, + } { + err := fn(context.Background(), bad) + var ve *cabana.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("%s: err = %v, want a cabana.ValidationError", name, err) + } + if _, ok := ve.Details["ends_at"]; !ok { + t.Errorf("%s: details %v name no ends_at", name, ve.Details) + } + } + good := &models.ClassSession{StartsAt: "2026-10-03 08:00", EndsAt: "2026-10-03 09:30"} + if err := hooks.FormBeforeCreate(context.Background(), good); err != nil { + t.Errorf("valid session: %v", err) + } + if err := hooks.FormBeforeUpdate(context.Background(), &models.Tag{}); err != nil { + t.Errorf("model without dates: %v", err) + } +} diff --git a/controllers/class_sessions.go b/controllers/class_sessions.go new file mode 100644 index 0000000..290c15f --- /dev/null +++ b/controllers/class_sessions.go @@ -0,0 +1,47 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = classSessionsAdmin{} + _ pact.AdminRecordSource = classSessionsAdmin{} + _ pact.AdminPermissioned = classSessionsAdmin{} +) + +// classSessionsAdmin is the admin controller for class sessions (zajęcia). +type classSessionsAdmin struct{ temporalHooks } + +func (classSessionsAdmin) ID() string { return "jz.bm.class_sessions" } +func (classSessionsAdmin) ModelName() string { return "ClassSession" } +func (classSessionsAdmin) ConfigDir() string { return "controllers/class_sessions" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (classSessionsAdmin) NewRecord() any { return &models.ClassSession{} } + +// RequiredPermissions is checked before any schema or record is served. +func (classSessionsAdmin) RequiredPermissions() []string { + return []string{PermSchedule} +} + +// AdminFieldRelations binds the course and meeting pickers. +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", + }, + } +} + +// ClassSessionsController returns the jz.bm.class_sessions admin controller. +func ClassSessionsController() pact.AdminController { return classSessionsAdmin{} } diff --git a/controllers/class_sessions/config_form.yaml b/controllers/class_sessions/config_form.yaml new file mode 100644 index 0000000..486f071 --- /dev/null +++ b/controllers/class_sessions/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.sessions.form +form: ~/plugins/jz/bm/models/class_sessions/fields.yaml +modelClass: ClassSession +defaultRedirect: jz/bm/class_sessions + +create: + redirect: jz/bm/class_sessions/update/:id + redirectClose: jz/bm/class_sessions + +update: + redirect: jz/bm/class_sessions + redirectClose: jz/bm/class_sessions diff --git a/controllers/class_sessions/config_list.yaml b/controllers/class_sessions/config_list.yaml new file mode 100644 index 0000000..e609b7c --- /dev/null +++ b/controllers/class_sessions/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/class_sessions/columns.yaml +modelClass: ClassSession +title: jz.bm::lang.sessions.title +recordUrl: jz/bm/class_sessions/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: starts_at + direction: asc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/controllers/courses.go b/controllers/courses.go new file mode 100644 index 0000000..ec0d69a --- /dev/null +++ b/controllers/courses.go @@ -0,0 +1,38 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = coursesAdmin{} + _ pact.AdminRecordSource = coursesAdmin{} + _ pact.AdminPermissioned = coursesAdmin{} +) + +// coursesAdmin is the admin controller for courses (przedmioty). +type coursesAdmin struct{} + +func (coursesAdmin) ID() string { return "jz.bm.courses" } +func (coursesAdmin) ModelName() string { return "Course" } +func (coursesAdmin) ConfigDir() string { return "controllers/courses" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (coursesAdmin) NewRecord() any { return &models.Course{} } + +// RequiredPermissions is checked before any schema or record is served. +func (coursesAdmin) RequiredPermissions() []string { + return []string{PermCourses} +} + +// TODO: requires SummerCMS change. The course form should embed relation +// managers for its readings and class sessions, but cabana relation managers +// only link and unlink belongsToMany rows through a pivot; hasMany children +// cannot be listed or edited from the parent form. Readings and class +// sessions have their own screens, filtered by course. + +// CoursesController returns the jz.bm.courses admin controller. +func CoursesController() pact.AdminController { return coursesAdmin{} } diff --git a/controllers/courses/config_form.yaml b/controllers/courses/config_form.yaml new file mode 100644 index 0000000..37425e2 --- /dev/null +++ b/controllers/courses/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.courses.form +form: ~/plugins/jz/bm/models/courses/fields.yaml +modelClass: Course +defaultRedirect: jz/bm/courses + +create: + redirect: jz/bm/courses/update/:id + redirectClose: jz/bm/courses + +update: + redirect: jz/bm/courses + redirectClose: jz/bm/courses diff --git a/controllers/courses/config_list.yaml b/controllers/courses/config_list.yaml new file mode 100644 index 0000000..c207a06 --- /dev/null +++ b/controllers/courses/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/courses/columns.yaml +modelClass: Course +title: jz.bm::lang.courses.title +recordUrl: jz/bm/courses/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: sort_order + direction: asc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/controllers/deadlines.go b/controllers/deadlines.go new file mode 100644 index 0000000..037b5b5 --- /dev/null +++ b/controllers/deadlines.go @@ -0,0 +1,38 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = deadlinesAdmin{} + _ pact.AdminRecordSource = deadlinesAdmin{} + _ pact.AdminPermissioned = deadlinesAdmin{} +) + +// deadlinesAdmin is the admin controller for deadlines (terminarz). +type deadlinesAdmin struct{ temporalHooks } + +func (deadlinesAdmin) ID() string { return "jz.bm.deadlines" } +func (deadlinesAdmin) ModelName() string { return "Deadline" } +func (deadlinesAdmin) ConfigDir() string { return "controllers/deadlines" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (deadlinesAdmin) NewRecord() any { return &models.Deadline{} } + +// RequiredPermissions is checked before any schema or record is served. +func (deadlinesAdmin) RequiredPermissions() []string { + return []string{PermSchedule} +} + +// AdminFieldRelations binds the course picker. +func (deadlinesAdmin) AdminFieldRelations() []cabana.FieldRelationContract { + return []cabana.FieldRelationContract{courseRelation("course_id")} +} + +// DeadlinesController returns the jz.bm.deadlines admin controller. +func DeadlinesController() pact.AdminController { return deadlinesAdmin{} } diff --git a/controllers/deadlines/config_form.yaml b/controllers/deadlines/config_form.yaml new file mode 100644 index 0000000..d33eb70 --- /dev/null +++ b/controllers/deadlines/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.deadlines.form +form: ~/plugins/jz/bm/models/deadlines/fields.yaml +modelClass: Deadline +defaultRedirect: jz/bm/deadlines + +create: + redirect: jz/bm/deadlines/update/:id + redirectClose: jz/bm/deadlines + +update: + redirect: jz/bm/deadlines + redirectClose: jz/bm/deadlines diff --git a/controllers/deadlines/config_list.yaml b/controllers/deadlines/config_list.yaml new file mode 100644 index 0000000..7c198cf --- /dev/null +++ b/controllers/deadlines/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/deadlines/columns.yaml +modelClass: Deadline +title: jz.bm::lang.deadlines.title +recordUrl: jz/bm/deadlines/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: due_at + direction: asc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/controllers/hooks.go b/controllers/hooks.go new file mode 100644 index 0000000..59bb335 --- /dev/null +++ b/controllers/hooks.go @@ -0,0 +1,55 @@ +package controllers + +import ( + "context" + + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +// Permission codes of the plugin's admin screens. +const ( + PermCourses = "jz.bm.access_courses" + PermSchedule = "jz.bm.access_schedule" + PermMaterials = "jz.bm.access_materials" +) + +// temporalHooks checks a model's Date and DateTime columns before every admin +// save and answers a bad value as a 422 on its field. lagoon.Validate has no +// date rules, so the model checks them itself (models.TemporalValidator). +type temporalHooks struct{} + +func (temporalHooks) FormBeforeCreate(_ context.Context, model any) error { + return checkTemporal(model) +} + +func (temporalHooks) FormBeforeUpdate(_ context.Context, model any) error { + return checkTemporal(model) +} + +func checkTemporal(model any) error { + v, ok := model.(models.TemporalValidator) + if !ok { + return nil + } + msgs := v.TemporalErrors() + if len(msgs) == 0 { + return nil + } + details := make(map[string]any, len(msgs)) + for field, m := range msgs { + details[field] = m + } + return &cabana.ValidationError{Details: details} +} + +// courseRelation is the belongsTo picker of a course_id column. +func courseRelation(foreignKey string) cabana.FieldRelationContract { + return cabana.FieldRelationContract{ + Field: "course", + Kind: "belongsTo", + NewRelated: func() any { return &models.Course{} }, + ForeignKey: foreignKey, + LabelColumn: "name", + } +} diff --git a/controllers/materials.go b/controllers/materials.go new file mode 100644 index 0000000..111e672 --- /dev/null +++ b/controllers/materials.go @@ -0,0 +1,53 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = materialsAdmin{} + _ pact.AdminRecordSource = materialsAdmin{} + _ pact.AdminPermissioned = materialsAdmin{} +) + +// materialsAdmin is the admin controller for materials. +type materialsAdmin struct{ temporalHooks } + +func (materialsAdmin) ID() string { return "jz.bm.materials" } +func (materialsAdmin) ModelName() string { return "Material" } +func (materialsAdmin) ConfigDir() string { return "controllers/materials" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (materialsAdmin) NewRecord() any { return &models.Material{} } + +// RequiredPermissions is checked before any schema or record is served. +func (materialsAdmin) RequiredPermissions() []string { + return []string{PermMaterials} +} + +// AdminFieldRelations binds the course picker and the tags. +// +// TODO: requires SummerCMS change. Materials carry their files as +// system_files attachments (field "files"), but cabana has no file upload +// form widget, so files are attached outside the admin form for now. +func (materialsAdmin) AdminFieldRelations() []cabana.FieldRelationContract { + return []cabana.FieldRelationContract{ + courseRelation("course_id"), + { + Field: "tags", + Kind: "belongsToMany", + NewRelated: func() any { return &models.Tag{} }, + NewPivot: func() any { return &models.MaterialTag{} }, + ParentForeignKey: "material_id", + RelatedForeignKey: "tag_id", + LabelColumn: "name", + }, + } +} + +// MaterialsController returns the jz.bm.materials admin controller. +func MaterialsController() pact.AdminController { return materialsAdmin{} } diff --git a/controllers/materials/config_form.yaml b/controllers/materials/config_form.yaml new file mode 100644 index 0000000..cd1c4f1 --- /dev/null +++ b/controllers/materials/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.materials.form +form: ~/plugins/jz/bm/models/materials/fields.yaml +modelClass: Material +defaultRedirect: jz/bm/materials + +create: + redirect: jz/bm/materials/update/:id + redirectClose: jz/bm/materials + +update: + redirect: jz/bm/materials + redirectClose: jz/bm/materials diff --git a/controllers/materials/config_list.yaml b/controllers/materials/config_list.yaml new file mode 100644 index 0000000..98ebb25 --- /dev/null +++ b/controllers/materials/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/materials/columns.yaml +modelClass: Material +title: jz.bm::lang.materials.title +recordUrl: jz/bm/materials/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: created_at + direction: desc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/controllers/meetings.go b/controllers/meetings.go new file mode 100644 index 0000000..0732934 --- /dev/null +++ b/controllers/meetings.go @@ -0,0 +1,32 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = meetingsAdmin{} + _ pact.AdminRecordSource = meetingsAdmin{} + _ pact.AdminPermissioned = meetingsAdmin{} +) + +// meetingsAdmin is the admin controller for meetings (zjazdy). +type meetingsAdmin struct{ temporalHooks } + +func (meetingsAdmin) ID() string { return "jz.bm.meetings" } +func (meetingsAdmin) ModelName() string { return "Meeting" } +func (meetingsAdmin) ConfigDir() string { return "controllers/meetings" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (meetingsAdmin) NewRecord() any { return &models.Meeting{} } + +// RequiredPermissions is checked before any schema or record is served. +func (meetingsAdmin) RequiredPermissions() []string { + return []string{PermSchedule} +} + +// MeetingsController returns the jz.bm.meetings admin controller. +func MeetingsController() pact.AdminController { return meetingsAdmin{} } diff --git a/controllers/meetings/config_form.yaml b/controllers/meetings/config_form.yaml new file mode 100644 index 0000000..39b929b --- /dev/null +++ b/controllers/meetings/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.meetings.form +form: ~/plugins/jz/bm/models/meetings/fields.yaml +modelClass: Meeting +defaultRedirect: jz/bm/meetings + +create: + redirect: jz/bm/meetings/update/:id + redirectClose: jz/bm/meetings + +update: + redirect: jz/bm/meetings + redirectClose: jz/bm/meetings diff --git a/controllers/meetings/config_list.yaml b/controllers/meetings/config_list.yaml new file mode 100644 index 0000000..49d31c6 --- /dev/null +++ b/controllers/meetings/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/meetings/columns.yaml +modelClass: Meeting +title: jz.bm::lang.meetings.title +recordUrl: jz/bm/meetings/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: number + direction: asc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/controllers/readings.go b/controllers/readings.go new file mode 100644 index 0000000..7a81d92 --- /dev/null +++ b/controllers/readings.go @@ -0,0 +1,38 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = readingsAdmin{} + _ pact.AdminRecordSource = readingsAdmin{} + _ pact.AdminPermissioned = readingsAdmin{} +) + +// readingsAdmin is the admin controller for readings (lektury). +type readingsAdmin struct{} + +func (readingsAdmin) ID() string { return "jz.bm.readings" } +func (readingsAdmin) ModelName() string { return "Reading" } +func (readingsAdmin) ConfigDir() string { return "controllers/readings" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (readingsAdmin) NewRecord() any { return &models.Reading{} } + +// RequiredPermissions is checked before any schema or record is served. +func (readingsAdmin) RequiredPermissions() []string { + return []string{PermCourses} +} + +// AdminFieldRelations binds the course picker. +func (readingsAdmin) AdminFieldRelations() []cabana.FieldRelationContract { + return []cabana.FieldRelationContract{courseRelation("course_id")} +} + +// ReadingsController returns the jz.bm.readings admin controller. +func ReadingsController() pact.AdminController { return readingsAdmin{} } diff --git a/controllers/readings/config_form.yaml b/controllers/readings/config_form.yaml new file mode 100644 index 0000000..be12ea9 --- /dev/null +++ b/controllers/readings/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.readings.form +form: ~/plugins/jz/bm/models/readings/fields.yaml +modelClass: Reading +defaultRedirect: jz/bm/readings + +create: + redirect: jz/bm/readings/update/:id + redirectClose: jz/bm/readings + +update: + redirect: jz/bm/readings + redirectClose: jz/bm/readings diff --git a/controllers/readings/config_list.yaml b/controllers/readings/config_list.yaml new file mode 100644 index 0000000..abcca66 --- /dev/null +++ b/controllers/readings/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/readings/columns.yaml +modelClass: Reading +title: jz.bm::lang.readings.title +recordUrl: jz/bm/readings/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: sort_order + direction: asc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/controllers/tags.go b/controllers/tags.go new file mode 100644 index 0000000..90288d1 --- /dev/null +++ b/controllers/tags.go @@ -0,0 +1,32 @@ +// Code generated by summer make. DO NOT EDIT. + +package controllers + +import ( + "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/models" +) + +var ( + _ pact.AdminController = tagsAdmin{} + _ pact.AdminRecordSource = tagsAdmin{} + _ pact.AdminPermissioned = tagsAdmin{} +) + +// tagsAdmin is the admin controller for material tags. +type tagsAdmin struct{} + +func (tagsAdmin) ID() string { return "jz.bm.tags" } +func (tagsAdmin) ModelName() string { return "Tag" } +func (tagsAdmin) ConfigDir() string { return "controllers/tags" } + +// NewRecord returns the model the generic admin handlers query and fill. +func (tagsAdmin) NewRecord() any { return &models.Tag{} } + +// RequiredPermissions is checked before any schema or record is served. +func (tagsAdmin) RequiredPermissions() []string { + return []string{PermMaterials} +} + +// TagsController returns the jz.bm.tags admin controller. +func TagsController() pact.AdminController { return tagsAdmin{} } diff --git a/controllers/tags/config_form.yaml b/controllers/tags/config_form.yaml new file mode 100644 index 0000000..dd13ba9 --- /dev/null +++ b/controllers/tags/config_form.yaml @@ -0,0 +1,12 @@ +name: jz.bm::lang.tags.form +form: ~/plugins/jz/bm/models/tags/fields.yaml +modelClass: Tag +defaultRedirect: jz/bm/tags + +create: + redirect: jz/bm/tags/update/:id + redirectClose: jz/bm/tags + +update: + redirect: jz/bm/tags + redirectClose: jz/bm/tags diff --git a/controllers/tags/config_list.yaml b/controllers/tags/config_list.yaml new file mode 100644 index 0000000..dcbeddc --- /dev/null +++ b/controllers/tags/config_list.yaml @@ -0,0 +1,15 @@ +list: ~/plugins/jz/bm/models/tags/columns.yaml +modelClass: Tag +title: jz.bm::lang.tags.title +recordUrl: jz/bm/tags/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 50 +perPageOptions: [20, 50, 100] +showCheckboxes: true +defaultSort: + column: name + direction: asc +toolbar: + buttons: [create, delete] + search: + prompt: backend::lang.list.search_prompt diff --git a/lang/en/lang.yaml b/lang/en/lang.yaml index 0967ef4..baadf2e 100644 --- a/lang/en/lang.yaml +++ b/lang/en/lang.yaml @@ -1 +1,97 @@ -{} +plugin: + name: BM studies + description: Courses, timetable, deadlines, readings and materials of the BM master's programme. +permissions: + access_courses: Manage courses and readings + access_schedule: Manage meetings, class sessions and deadlines + access_materials: Manage materials and tags +navigation: + courses: Courses + readings: Readings + meetings: Meetings + sessions: Class sessions + deadlines: Deadlines + materials: Materials + tags: Tags +common: + title: Title + description: Description + notes: Notes + course: Course + source: Source + url: URL + sort_order: Sort order + created_at: Created + none: — none — + date_comment: A date in the YYYY-MM-DD form, such as 2026-10-03. + datetime_comment: Date and time in Polish time, such as 2026-10-03 08:00. +forms: + wyklad: Lecture + cwiczenia: Tutorial + konwersatorium: Conversatorium + seminarium: Seminar + inne: Other +courses: + title: Courses + form: Course + name: Name + form_type: Form + code: Code + lecturer: Lecturer + semester: Semester + ects: ECTS + assessment: Assessment + assessment_comment: Such as egzamin or zaliczenie na ocenę. + elective_group: Elective group + elective_group_comment: Courses with the same value are alternatives to choose from, such as specjalnosc. + syllabus_url: Syllabus URL +meetings: + title: Meetings + form: Meeting + number: Meeting number + starts_on: From + ends_on: To +sessions: + title: Class sessions + form: Class session + meeting: Meeting + starts_at: Starts + ends_at: Ends + room: Room + kind: Kind + is_online: Online +deadlines: + title: Deadlines + form: Deadline + due_at: Due + kind: Kind +deadline_kinds: + zaliczenie: Credit + egzamin: Exam + praca: Paper + organizacyjne: Administrative + inne: Other +readings: + title: Readings + form: Reading + author: Author + is_required: Required +materials: + title: Materials + form: Material + type: Type + body: Body (Markdown) + body_comment: A note in Markdown. + published_at: Published + published_at_comment: Leave empty for a draft that students do not see. Such as 2026-10-03 08:00. + tags: Tags +material_types: + pdf: PDF + notatka: Note + link: Link + inne: Other +tags: + title: Tags + form: Tag + name: Name + slug: Slug diff --git a/lang/pl/lang.yaml b/lang/pl/lang.yaml new file mode 100644 index 0000000..7b9cff1 --- /dev/null +++ b/lang/pl/lang.yaml @@ -0,0 +1,97 @@ +plugin: + name: Studia BM + description: Przedmioty, plan zajęć, terminarz, lektury i materiały studiów magisterskich BM. +permissions: + access_courses: Zarządzanie przedmiotami i lekturami + access_schedule: Zarządzanie zjazdami, zajęciami i terminarzem + access_materials: Zarządzanie materiałami i tagami +navigation: + courses: Przedmioty + readings: Lektury + meetings: Zjazdy + sessions: Zajęcia + deadlines: Terminarz + materials: Materiały + tags: Tagi +common: + title: Tytuł + description: Opis + notes: Uwagi + course: Przedmiot + source: Źródło + url: Adres URL + sort_order: Kolejność + created_at: Utworzono + none: — brak — + date_comment: Data w formacie RRRR-MM-DD, np. 2026-10-03. + datetime_comment: Data i godzina czasu polskiego, np. 2026-10-03 08:00. +forms: + wyklad: Wykład + cwiczenia: Ćwiczenia + konwersatorium: Konwersatorium + seminarium: Seminarium + inne: Inne +courses: + title: Przedmioty + form: Przedmiot + name: Nazwa + form_type: Forma zajęć + code: Kod + lecturer: Prowadzący + semester: Semestr + ects: ECTS + assessment: Forma zaliczenia + assessment_comment: Np. egzamin, zaliczenie na ocenę. + elective_group: Grupa do wyboru + elective_group_comment: Przedmioty o tej samej wartości są alternatywami do wyboru, np. specjalnosc. + syllabus_url: Sylabus (URL) +meetings: + title: Zjazdy + form: Zjazd + number: Numer zjazdu + starts_on: Od + ends_on: Do +sessions: + title: Zajęcia + form: Zajęcia + meeting: Zjazd + starts_at: Początek + ends_at: Koniec + room: Sala + kind: Rodzaj + is_online: Zdalnie +deadlines: + title: Terminarz + form: Termin + due_at: Termin + kind: Rodzaj +deadline_kinds: + zaliczenie: Zaliczenie + egzamin: Egzamin + praca: Praca + organizacyjne: Organizacyjne + inne: Inne +readings: + title: Lektury + form: Lektura + author: Autor + is_required: Obowiązkowa +materials: + title: Materiały + form: Materiał + type: Typ + body: Treść (Markdown) + body_comment: Notatka w Markdownie. + published_at: Opublikowano + published_at_comment: Puste pole to szkic, niewidoczny dla studentów. Np. 2026-10-03 08:00. + tags: Tagi +material_types: + pdf: PDF + notatka: Notatka + link: Link + inne: Inne +tags: + title: Tagi + form: Tag + name: Nazwa + slug: Slug diff --git a/models/class_sessions/columns.yaml b/models/class_sessions/columns.yaml new file mode 100644 index 0000000..c0151ff --- /dev/null +++ b/models/class_sessions/columns.yaml @@ -0,0 +1,16 @@ +columns: + starts_at: + label: jz.bm::lang.sessions.starts_at + ends_at: + label: jz.bm::lang.sessions.ends_at + course: + label: jz.bm::lang.common.course + relation: course + select: name + searchable: true + room: + label: jz.bm::lang.sessions.room + searchable: true + is_online: + label: jz.bm::lang.sessions.is_online + type: switch diff --git a/models/class_sessions/fields.yaml b/models/class_sessions/fields.yaml new file mode 100644 index 0000000..3497285 --- /dev/null +++ b/models/class_sessions/fields.yaml @@ -0,0 +1,41 @@ +fields: + course: + label: jz.bm::lang.common.course + type: relation + nameFrom: name + span: left + required: true + meeting: + label: jz.bm::lang.sessions.meeting + type: relation + nameFrom: number + span: right + emptyOption: jz.bm::lang.common.none + starts_at: + label: jz.bm::lang.sessions.starts_at + type: text + span: left + required: true + comment: jz.bm::lang.common.datetime_comment + ends_at: + label: jz.bm::lang.sessions.ends_at + type: text + span: right + required: true + comment: jz.bm::lang.common.datetime_comment + room: + label: jz.bm::lang.sessions.room + type: text + span: left + kind: + label: jz.bm::lang.sessions.kind + type: text + span: right + is_online: + label: jz.bm::lang.sessions.is_online + type: switch + span: left + notes: + label: jz.bm::lang.common.notes + type: textarea + size: small diff --git a/models/courses/columns.yaml b/models/courses/columns.yaml new file mode 100644 index 0000000..8a1f655 --- /dev/null +++ b/models/courses/columns.yaml @@ -0,0 +1,17 @@ +columns: + name: + label: jz.bm::lang.courses.name + searchable: true + form: + label: jz.bm::lang.courses.form_type + semester: + label: jz.bm::lang.courses.semester + lecturer: + label: jz.bm::lang.courses.lecturer + searchable: true + ects: + label: jz.bm::lang.courses.ects + elective_group: + label: jz.bm::lang.courses.elective_group + sort_order: + label: jz.bm::lang.common.sort_order diff --git a/models/courses/fields.yaml b/models/courses/fields.yaml new file mode 100644 index 0000000..bc2c7a8 --- /dev/null +++ b/models/courses/fields.yaml @@ -0,0 +1,58 @@ +fields: + name: + label: jz.bm::lang.courses.name + type: text + span: left + required: true + form: + label: jz.bm::lang.courses.form_type + type: dropdown + span: right + required: true + options: + wyklad: jz.bm::lang.forms.wyklad + cwiczenia: jz.bm::lang.forms.cwiczenia + konwersatorium: jz.bm::lang.forms.konwersatorium + seminarium: jz.bm::lang.forms.seminarium + inne: jz.bm::lang.forms.inne + semester: + label: jz.bm::lang.courses.semester + type: number + span: left + required: true + default: 1 + code: + label: jz.bm::lang.courses.code + type: text + span: right + lecturer: + label: jz.bm::lang.courses.lecturer + type: text + span: left + ects: + label: jz.bm::lang.courses.ects + type: number + span: right + assessment: + label: jz.bm::lang.courses.assessment + type: text + span: left + comment: jz.bm::lang.courses.assessment_comment + elective_group: + label: jz.bm::lang.courses.elective_group + type: text + span: right + comment: jz.bm::lang.courses.elective_group_comment + sort_order: + label: jz.bm::lang.common.sort_order + type: number + span: left + default: 0 + syllabus_url: + label: jz.bm::lang.courses.syllabus_url + type: text + span: right + description: + label: jz.bm::lang.common.description + type: textarea + size: large diff --git a/models/deadlines/columns.yaml b/models/deadlines/columns.yaml new file mode 100644 index 0000000..010168c --- /dev/null +++ b/models/deadlines/columns.yaml @@ -0,0 +1,12 @@ +columns: + due_at: + label: jz.bm::lang.deadlines.due_at + title: + label: jz.bm::lang.common.title + searchable: true + kind: + label: jz.bm::lang.deadlines.kind + course: + label: jz.bm::lang.common.course + relation: course + select: name diff --git a/models/deadlines/fields.yaml b/models/deadlines/fields.yaml new file mode 100644 index 0000000..c817b9a --- /dev/null +++ b/models/deadlines/fields.yaml @@ -0,0 +1,33 @@ +fields: + title: + label: jz.bm::lang.common.title + type: text + span: left + required: true + due_at: + label: jz.bm::lang.deadlines.due_at + type: text + span: right + required: true + comment: jz.bm::lang.common.datetime_comment + kind: + label: jz.bm::lang.deadlines.kind + type: dropdown + span: left + required: true + options: + zaliczenie: jz.bm::lang.deadline_kinds.zaliczenie + egzamin: jz.bm::lang.deadline_kinds.egzamin + praca: jz.bm::lang.deadline_kinds.praca + organizacyjne: jz.bm::lang.deadline_kinds.organizacyjne + inne: jz.bm::lang.deadline_kinds.inne + course: + label: jz.bm::lang.common.course + type: relation + nameFrom: name + span: right + emptyOption: jz.bm::lang.common.none + description: + label: jz.bm::lang.common.description + type: textarea + size: small diff --git a/models/materials/columns.yaml b/models/materials/columns.yaml new file mode 100644 index 0000000..70b74b8 --- /dev/null +++ b/models/materials/columns.yaml @@ -0,0 +1,15 @@ +columns: + title: + label: jz.bm::lang.common.title + searchable: true + type: + label: jz.bm::lang.materials.type + course: + label: jz.bm::lang.common.course + relation: course + select: name + published_at: + label: jz.bm::lang.materials.published_at + created_at: + label: jz.bm::lang.common.created_at + type: datetime diff --git a/models/materials/fields.yaml b/models/materials/fields.yaml new file mode 100644 index 0000000..54e9ba8 --- /dev/null +++ b/models/materials/fields.yaml @@ -0,0 +1,46 @@ +# Attached files (system_files, field "files") have no form widget yet; see +# the TODO on materialsAdmin.AdminFieldRelations. +fields: + title: + label: jz.bm::lang.common.title + type: text + span: left + required: true + type: + label: jz.bm::lang.materials.type + type: dropdown + span: right + required: true + options: + pdf: jz.bm::lang.material_types.pdf + notatka: jz.bm::lang.material_types.notatka + link: jz.bm::lang.material_types.link + inne: jz.bm::lang.material_types.inne + course: + label: jz.bm::lang.common.course + type: relation + nameFrom: name + span: left + emptyOption: jz.bm::lang.common.none + published_at: + label: jz.bm::lang.materials.published_at + type: text + span: right + comment: jz.bm::lang.materials.published_at_comment + source: + label: jz.bm::lang.common.source + type: text + span: left + url: + label: jz.bm::lang.common.url + type: text + span: right + tags: + label: jz.bm::lang.materials.tags + type: relation + nameFrom: name + body: + label: jz.bm::lang.materials.body + type: textarea + size: giant + comment: jz.bm::lang.materials.body_comment diff --git a/models/meetings/columns.yaml b/models/meetings/columns.yaml new file mode 100644 index 0000000..59b6d70 --- /dev/null +++ b/models/meetings/columns.yaml @@ -0,0 +1,11 @@ +columns: + number: + label: jz.bm::lang.meetings.number + starts_on: + label: jz.bm::lang.meetings.starts_on + ends_on: + label: jz.bm::lang.meetings.ends_on + notes: + label: jz.bm::lang.common.notes + searchable: true + sortable: false diff --git a/models/meetings/fields.yaml b/models/meetings/fields.yaml new file mode 100644 index 0000000..1e85ca9 --- /dev/null +++ b/models/meetings/fields.yaml @@ -0,0 +1,22 @@ +fields: + number: + label: jz.bm::lang.meetings.number + type: number + span: left + required: true + starts_on: + label: jz.bm::lang.meetings.starts_on + type: text + span: left + required: true + comment: jz.bm::lang.common.date_comment + ends_on: + label: jz.bm::lang.meetings.ends_on + type: text + span: right + required: true + comment: jz.bm::lang.common.date_comment + notes: + label: jz.bm::lang.common.notes + type: textarea + size: small diff --git a/models/readings/columns.yaml b/models/readings/columns.yaml new file mode 100644 index 0000000..42d4782 --- /dev/null +++ b/models/readings/columns.yaml @@ -0,0 +1,17 @@ +columns: + title: + label: jz.bm::lang.common.title + searchable: true + author: + label: jz.bm::lang.readings.author + searchable: true + course: + label: jz.bm::lang.common.course + relation: course + select: name + searchable: true + is_required: + label: jz.bm::lang.readings.is_required + type: switch + sort_order: + label: jz.bm::lang.common.sort_order diff --git a/models/readings/fields.yaml b/models/readings/fields.yaml new file mode 100644 index 0000000..729e7c2 --- /dev/null +++ b/models/readings/fields.yaml @@ -0,0 +1,33 @@ +fields: + course: + label: jz.bm::lang.common.course + type: relation + nameFrom: name + span: left + required: true + is_required: + label: jz.bm::lang.readings.is_required + type: switch + span: right + title: + label: jz.bm::lang.common.title + type: text + span: left + required: true + author: + label: jz.bm::lang.readings.author + type: text + span: right + source: + label: jz.bm::lang.common.source + type: text + span: left + url: + label: jz.bm::lang.common.url + type: text + span: right + sort_order: + label: jz.bm::lang.common.sort_order + type: number + span: left + default: 0 diff --git a/models/tags/columns.yaml b/models/tags/columns.yaml new file mode 100644 index 0000000..c929a38 --- /dev/null +++ b/models/tags/columns.yaml @@ -0,0 +1,7 @@ +columns: + name: + label: jz.bm::lang.tags.name + searchable: true + slug: + label: jz.bm::lang.tags.slug + searchable: true diff --git a/models/tags/fields.yaml b/models/tags/fields.yaml new file mode 100644 index 0000000..48a5255 --- /dev/null +++ b/models/tags/fields.yaml @@ -0,0 +1,11 @@ +fields: + name: + label: jz.bm::lang.tags.name + type: text + span: left + required: true + slug: + label: jz.bm::lang.tags.slug + type: text + span: right + required: true diff --git a/plugin.go b/plugin.go index e5dbdd8..15d06d9 100644 --- a/plugin.go +++ b/plugin.go @@ -8,6 +8,7 @@ import ( "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" + "git.golem15.com/jakub/sm-bm-plugin/controllers" "github.com/go-gormigrate/gormigrate/v2" ) @@ -20,6 +21,9 @@ var ( _ pact.HasCommands = (*Plugin)(nil) _ pact.HasJobs = (*Plugin)(nil) _ pact.HasAdminControllers = (*Plugin)(nil) + _ pact.AdminAssets = (*Plugin)(nil) + _ pact.HasPermissions = (*Plugin)(nil) + _ pact.HasNavigation = (*Plugin)(nil) ) //go:embed config @@ -31,14 +35,32 @@ var langFS embed.FS //go:embed views/mail var mailFS embed.FS -// Plugin is the jz.bm plugin. -type Plugin struct{} +//go:embed controllers/*/*.yaml models/*/*.yaml +var adminFS embed.FS -func (p *Plugin) ID() string { return "jz.bm" } -func (p *Plugin) Requires() []string { return nil } +// Plugin is the jz.bm plugin: the study dashboard backend of the BM +// (Bezpieczeństwo międzynarodowe) master's programme. +type Plugin struct { + app *backpack.App +} -func (p *Plugin) Register(*backpack.App) error { return nil } -func (p *Plugin) Boot(*backpack.App) error { return nil } +func (p *Plugin) ID() string { return "jz.bm" } + +// Requires golem15.user: its users table holds the students, and its +// jwt.auth middleware guards the API. +func (p *Plugin) Requires() []string { return []string{"golem15.user"} } + +func (p *Plugin) Register(app *backpack.App) error { + p.app = app + return nil +} + +// Boot keeps the application, so route handlers and commands can reach its +// services, such as the database, when they run. +func (p *Plugin) Boot(app *backpack.App) error { + p.app = app + return nil +} func (p *Plugin) ConfigFS() fs.FS { return configFS } func (p *Plugin) LangFS() fs.FS { return langFS } @@ -49,12 +71,49 @@ func (p *Plugin) MailLayouts() map[string]string { return nil } func (p *Plugin) Models() []any { return generatedModels() } func (p *Plugin) Migrations() []*gormigrate.Migration { return generatedMigrations() } -func (p *Plugin) Commands() []bonfire.Command { return generatedCommands() } func (p *Plugin) Jobs() []pact.Job { return generatedJobs() } func (p *Plugin) AdminControllers() []pact.AdminController { return generatedAdminControllers() } +// Commands returns the generated commands. +func (p *Plugin) Commands() []bonfire.Command { return generatedCommands() } + +// AdminFS is the admin YAML the controllers read. +func (p *Plugin) AdminFS() fs.FS { return adminFS } + +// Permissions replaces registerPermissions(). +func (p *Plugin) Permissions() []pact.Permission { + tab := "jz.bm::lang.plugin.name" + return []pact.Permission{ + {Code: controllers.PermCourses, Tab: tab, Label: "jz.bm::lang.permissions.access_courses"}, + {Code: controllers.PermSchedule, Tab: tab, Label: "jz.bm::lang.permissions.access_schedule"}, + {Code: controllers.PermMaterials, Tab: tab, Label: "jz.bm::lang.permissions.access_materials"}, + } +} + +// Navigation replaces registerNavigation(): one "Studia BM" group with a +// side item per screen. +func (p *Plugin) Navigation() []pact.NavigationItem { + return []pact.NavigationItem{{ + Code: "bm", + Label: "jz.bm::lang.plugin.name", + Icon: "icon-graduation-cap", + Permissions: []string{controllers.PermCourses, controllers.PermSchedule, controllers.PermMaterials}, + Order: 300, + Controller: "jz.bm.courses", + SideMenu: []pact.NavigationItem{ + {Code: "courses", Label: "jz.bm::lang.navigation.courses", Icon: "icon-book", Permissions: []string{controllers.PermCourses}, Controller: "jz.bm.courses"}, + {Code: "readings", Label: "jz.bm::lang.navigation.readings", Icon: "icon-bookmark", Permissions: []string{controllers.PermCourses}, Controller: "jz.bm.readings"}, + {Code: "meetings", Label: "jz.bm::lang.navigation.meetings", Icon: "icon-calendar", Permissions: []string{controllers.PermSchedule}, Controller: "jz.bm.meetings"}, + {Code: "sessions", Label: "jz.bm::lang.navigation.sessions", Icon: "icon-clock-o", Permissions: []string{controllers.PermSchedule}, Controller: "jz.bm.class_sessions"}, + {Code: "deadlines", Label: "jz.bm::lang.navigation.deadlines", Icon: "icon-flag", Permissions: []string{controllers.PermSchedule}, Controller: "jz.bm.deadlines"}, + {Code: "materials", Label: "jz.bm::lang.navigation.materials", Icon: "icon-file-text-o", Permissions: []string{controllers.PermMaterials}, Controller: "jz.bm.materials"}, + {Code: "tags", Label: "jz.bm::lang.navigation.tags", Icon: "icon-tags", Permissions: []string{controllers.PermMaterials}, Controller: "jz.bm.tags"}, + }, + }} +} + func init() { party.Register(&Plugin{}) } diff --git a/registry.gen.go b/registry.gen.go index 164452c..794d376 100644 --- a/registry.gen.go +++ b/registry.gen.go @@ -5,15 +5,39 @@ package bm import ( "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/pact" + "git.golem15.com/jakub/sm-bm-plugin/controllers" + "git.golem15.com/jakub/sm-bm-plugin/models" + "git.golem15.com/jakub/sm-bm-plugin/updates" "github.com/go-gormigrate/gormigrate/v2" ) func generatedModels() []any { - return nil + return []any{ + &models.ClassSession{}, + &models.Course{}, + &models.Deadline{}, + &models.Material{}, + &models.MaterialTag{}, + &models.Meeting{}, + &models.Reading{}, + &models.ReadingProgress{}, + &models.Tag{}, + } } func generatedMigrations() []*gormigrate.Migration { - return nil + return []*gormigrate.Migration{ + updates.CreateMeetings(), + updates.CreateCourses(), + updates.CreateClassSessions(), + updates.CreateDeadlines(), + updates.CreateReadings(), + updates.CreateReadingProgress(), + updates.CreateTags(), + updates.CreateMaterials(), + updates.CreateMaterialTag(), + updates.AddElectiveGroupToCourses(), + } } func generatedCommands() []bonfire.Command { @@ -25,5 +49,13 @@ func generatedJobs() []pact.Job { } func generatedAdminControllers() []pact.AdminController { - return nil + return []pact.AdminController{ + controllers.ClassSessionsController(), + controllers.CoursesController(), + controllers.DeadlinesController(), + controllers.MaterialsController(), + controllers.MeetingsController(), + controllers.ReadingsController(), + controllers.TagsController(), + } } diff --git a/updates/updates_test.go b/updates/updates_test.go new file mode 100644 index 0000000..134d715 --- /dev/null +++ b/updates/updates_test.go @@ -0,0 +1,43 @@ +package updates_test + +import ( + "os" + "regexp" + "slices" + "strings" + "testing" + + bm "git.golem15.com/jakub/sm-bm-plugin" +) + +var migrationFile = regexp.MustCompile(`^(\d{14})_[a-z0-9_]+\.go$`) + +// The set must run in ID order (foreign keys point at earlier tables), and +// every migration file is named after its ID. +func TestMigrationOrder(t *testing.T) { + var ids []string + for _, m := range (&bm.Plugin{}).Migrations() { + if m.Migrate == nil || m.Rollback == nil { + t.Errorf("migration %s has no Migrate or Rollback", m.ID) + } + ids = append(ids, m.ID) + } + if !slices.IsSorted(ids) { + t.Errorf("migration IDs are not ascending: %v", ids) + } + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + var files []string + for _, e := range entries { + name := e.Name() + if e.IsDir() || !migrationFile.MatchString(name) { + continue + } + files = append(files, strings.TrimSuffix(name, ".go")) + } + if !slices.Equal(files, ids) { + t.Errorf("migration files %v, want one per ID %v", files, ids) + } +}