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.
This commit is contained in:
35
controllers/admin_test.go
Normal file
35
controllers/admin_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
47
controllers/class_sessions.go
Normal file
47
controllers/class_sessions.go
Normal file
@@ -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{} }
|
||||
12
controllers/class_sessions/config_form.yaml
Normal file
12
controllers/class_sessions/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/class_sessions/config_list.yaml
Normal file
15
controllers/class_sessions/config_list.yaml
Normal file
@@ -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
|
||||
38
controllers/courses.go
Normal file
38
controllers/courses.go
Normal file
@@ -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{} }
|
||||
12
controllers/courses/config_form.yaml
Normal file
12
controllers/courses/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/courses/config_list.yaml
Normal file
15
controllers/courses/config_list.yaml
Normal file
@@ -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
|
||||
38
controllers/deadlines.go
Normal file
38
controllers/deadlines.go
Normal file
@@ -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{} }
|
||||
12
controllers/deadlines/config_form.yaml
Normal file
12
controllers/deadlines/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/deadlines/config_list.yaml
Normal file
15
controllers/deadlines/config_list.yaml
Normal file
@@ -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
|
||||
55
controllers/hooks.go
Normal file
55
controllers/hooks.go
Normal file
@@ -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",
|
||||
}
|
||||
}
|
||||
53
controllers/materials.go
Normal file
53
controllers/materials.go
Normal file
@@ -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{} }
|
||||
12
controllers/materials/config_form.yaml
Normal file
12
controllers/materials/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/materials/config_list.yaml
Normal file
15
controllers/materials/config_list.yaml
Normal file
@@ -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
|
||||
32
controllers/meetings.go
Normal file
32
controllers/meetings.go
Normal file
@@ -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{} }
|
||||
12
controllers/meetings/config_form.yaml
Normal file
12
controllers/meetings/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/meetings/config_list.yaml
Normal file
15
controllers/meetings/config_list.yaml
Normal file
@@ -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
|
||||
38
controllers/readings.go
Normal file
38
controllers/readings.go
Normal file
@@ -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{} }
|
||||
12
controllers/readings/config_form.yaml
Normal file
12
controllers/readings/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/readings/config_list.yaml
Normal file
15
controllers/readings/config_list.yaml
Normal file
@@ -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
|
||||
32
controllers/tags.go
Normal file
32
controllers/tags.go
Normal file
@@ -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{} }
|
||||
12
controllers/tags/config_form.yaml
Normal file
12
controllers/tags/config_form.yaml
Normal file
@@ -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
|
||||
15
controllers/tags/config_list.yaml
Normal file
15
controllers/tags/config_list.yaml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
97
lang/pl/lang.yaml
Normal file
97
lang/pl/lang.yaml
Normal file
@@ -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
|
||||
16
models/class_sessions/columns.yaml
Normal file
16
models/class_sessions/columns.yaml
Normal file
@@ -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
|
||||
41
models/class_sessions/fields.yaml
Normal file
41
models/class_sessions/fields.yaml
Normal file
@@ -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
|
||||
17
models/courses/columns.yaml
Normal file
17
models/courses/columns.yaml
Normal file
@@ -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
|
||||
58
models/courses/fields.yaml
Normal file
58
models/courses/fields.yaml
Normal file
@@ -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
|
||||
12
models/deadlines/columns.yaml
Normal file
12
models/deadlines/columns.yaml
Normal file
@@ -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
|
||||
33
models/deadlines/fields.yaml
Normal file
33
models/deadlines/fields.yaml
Normal file
@@ -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
|
||||
15
models/materials/columns.yaml
Normal file
15
models/materials/columns.yaml
Normal file
@@ -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
|
||||
46
models/materials/fields.yaml
Normal file
46
models/materials/fields.yaml
Normal file
@@ -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
|
||||
11
models/meetings/columns.yaml
Normal file
11
models/meetings/columns.yaml
Normal file
@@ -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
|
||||
22
models/meetings/fields.yaml
Normal file
22
models/meetings/fields.yaml
Normal file
@@ -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
|
||||
17
models/readings/columns.yaml
Normal file
17
models/readings/columns.yaml
Normal file
@@ -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
|
||||
33
models/readings/fields.yaml
Normal file
33
models/readings/fields.yaml
Normal file
@@ -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
|
||||
7
models/tags/columns.yaml
Normal file
7
models/tags/columns.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
columns:
|
||||
name:
|
||||
label: jz.bm::lang.tags.name
|
||||
searchable: true
|
||||
slug:
|
||||
label: jz.bm::lang.tags.slug
|
||||
searchable: true
|
||||
11
models/tags/fields.yaml
Normal file
11
models/tags/fields.yaml
Normal file
@@ -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
|
||||
73
plugin.go
73
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{})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
43
updates/updates_test.go
Normal file
43
updates/updates_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user