feat(15-02): add Categories, Tags, and journal settings admin

Register the remaining YAML admin screens and the settings singleton
so a host can boot Posts, Categories, Tags, and Code journal. Posts
update and delete refuse other authors without access_other_posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 18:42:29 +02:00
parent dea0934950
commit afd0a2cafc
17 changed files with 388 additions and 3 deletions

View File

@@ -12,6 +12,8 @@ import (
func AdminControllers(app func() *backpack.App) []pact.AdminController {
return []pact.AdminController{
postsAdminController{app: app},
categoriesAdminController{app: app},
tagsAdminController{app: app},
}
}
@@ -25,5 +27,15 @@ func init() {
var _ pact.FilterOptions = postsAdminController{}
var _ pact.FormBeforeCreate = postsAdminController{}
var _ pact.FormBeforeUpdate = postsAdminController{}
var _ pact.FormBeforeDelete = postsAdminController{}
var _ cabana.FieldRelationProvider = postsAdminController{}
var _ pact.AdminController = categoriesAdminController{}
var _ pact.AdminPermissioned = categoriesAdminController{}
var _ pact.AdminRecordSource = categoriesAdminController{}
var _ cabana.FieldRelationProvider = categoriesAdminController{}
var _ pact.AdminController = tagsAdminController{}
var _ pact.AdminPermissioned = tagsAdminController{}
var _ pact.AdminRecordSource = tagsAdminController{}
}

48
controllers/categories.go Normal file
View File

@@ -0,0 +1,48 @@
package controllers
import (
"git.golem15.com/golem15/sm-journal-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/pact"
)
const (
// CategoriesControllerID is the cabana controller id of the Categories screen.
CategoriesControllerID = "golem15.journal.categories"
// PermissionAccessCategories is Categories::$requiredPermissions.
PermissionAccessCategories = "golem15.journal.access_categories"
)
type categoriesAdminController struct {
app func() *backpack.App
}
func (categoriesAdminController) ID() string { return CategoriesControllerID }
func (categoriesAdminController) ModelName() string { return `Golem15\Journal\Models\Category` }
func (categoriesAdminController) ConfigDir() string { return "controllers/categories" }
func (categoriesAdminController) RequiredPermissions() []string {
return []string{PermissionAccessCategories}
}
func (categoriesAdminController) NewRecord() any { return &models.Category{} }
func (categoriesAdminController) AdminFieldRelations() []cabana.FieldRelationContract {
return []cabana.FieldRelationContract{{
Field: "parent",
Kind: "belongsTo",
NewRelated: func() any { return &models.Category{} },
ForeignKey: "parent_id",
LabelColumn: "name",
}}
}
var (
_ pact.AdminController = categoriesAdminController{}
_ pact.AdminPermissioned = categoriesAdminController{}
_ pact.AdminRecordSource = categoriesAdminController{}
_ cabana.FieldRelationProvider = categoriesAdminController{}
)

View File

@@ -0,0 +1,19 @@
name: golem15.journal::lang.journal.create_category
form: ~/plugins/golem15/journal/models/category/fields.yaml
modelClass: Golem15\Journal\Models\Category
defaultRedirect: golem15/journal/categories
create:
redirect: golem15/journal/categories/update/:id
redirectClose: golem15/journal/categories
update:
redirect: golem15/journal/categories
redirectClose: golem15/journal/categories
messages:
create: golem15.journal::lang.journal.create_category
update: backend::lang.messages.form.update
saved: backend::lang.messages.form.saved
deleteConfirm: golem15.journal::lang.category.delete_confirm
deleted: backend::lang.messages.form.deleted

View File

@@ -0,0 +1,19 @@
list: ~/plugins/golem15/journal/models/category/columns.yaml
modelClass: Golem15\Journal\Models\Category
title: golem15.journal::lang.category.label_plural
recordUrl: golem15/journal/categories/update/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage: 20
showCheckboxes: true
showSetup: true
toolbar:
buttons: [create, delete]
search:
prompt: backend::lang.list.search_prompt
messages:
create: golem15.journal::lang.journal.create_category
empty: backend::lang.list.no_records
deleteConfirm: golem15.journal::lang.category.delete_confirm
deleted: backend::lang.messages.list.deleted

View File

@@ -174,9 +174,28 @@ func (c postsAdminController) FormBeforeUpdate(ctx context.Context, model any) e
if !ok || post == nil {
return errors.New("posts admin: form model is not a post")
}
if err := refuseOtherAuthor(ctx, post); err != nil {
return err
}
return preparePostWrite(ctx, post)
}
func (c postsAdminController) FormBeforeDelete(ctx context.Context, model any) error {
post, ok := model.(*models.Post)
if !ok || post == nil {
return errors.New("posts admin: form model is not a post")
}
return refuseOtherAuthor(ctx, post)
}
func refuseOtherAuthor(ctx context.Context, post *models.Post) error {
principal, _ := bouncer.User(ctx)
if post.CanEdit(principal) {
return nil
}
return &cabana.ForbiddenError{Message: "golem15.journal::lang.journal.access_other_posts"}
}
func preparePostWrite(ctx context.Context, post *models.Post) error {
if err := refuseUnpublishedWrite(ctx, post); err != nil {
return err

36
controllers/tags.go Normal file
View File

@@ -0,0 +1,36 @@
package controllers
import (
"git.golem15.com/golem15/sm-journal-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
)
const (
// TagsControllerID is the cabana controller id of the Tags screen.
TagsControllerID = "golem15.journal.tags"
// PermissionAccessTags is Tags::$requiredPermissions.
PermissionAccessTags = "golem15.journal.access_tags"
)
type tagsAdminController struct {
app func() *backpack.App
}
func (tagsAdminController) ID() string { return TagsControllerID }
func (tagsAdminController) ModelName() string { return `Golem15\Journal\Models\Tag` }
func (tagsAdminController) ConfigDir() string { return "controllers/tags" }
func (tagsAdminController) RequiredPermissions() []string {
return []string{PermissionAccessTags}
}
func (tagsAdminController) NewRecord() any { return &models.Tag{} }
var (
_ pact.AdminController = tagsAdminController{}
_ pact.AdminPermissioned = tagsAdminController{}
_ pact.AdminRecordSource = tagsAdminController{}
)

View File

@@ -0,0 +1,19 @@
name: golem15.journal::lang.journal.create_tag
form: ~/plugins/golem15/journal/models/tag/fields.yaml
modelClass: Golem15\Journal\Models\Tag
defaultRedirect: golem15/journal/tags
create:
redirect: golem15/journal/tags/update/:id
redirectClose: golem15/journal/tags
update:
redirect: golem15/journal/tags
redirectClose: golem15/journal/tags
messages:
create: golem15.journal::lang.journal.create_tag
update: backend::lang.messages.form.update
saved: backend::lang.messages.form.saved
deleteConfirm: golem15.journal::lang.tag.delete_confirm
deleted: backend::lang.messages.form.deleted

View File

@@ -0,0 +1,19 @@
list: ~/plugins/golem15/journal/models/tag/columns.yaml
modelClass: Golem15\Journal\Models\Tag
title: golem15.journal::lang.tag.label_plural
recordUrl: golem15/journal/tags/update/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage: 20
showCheckboxes: true
showSetup: true
toolbar:
buttons: [create, delete]
search:
prompt: backend::lang.list.search_prompt
messages:
create: golem15.journal::lang.journal.create_tag
empty: backend::lang.list.no_records
deleteConfirm: golem15.journal::lang.tag.delete_confirm
deleted: backend::lang.messages.list.deleted