feat(11.1-05): add the walkthrough's admin controller, publish command and published_at migration
- make:admin-controller, make:command and make:migration output, finished: the Posts controller serves models.Post behind acme.blog.access_posts, WinterCMS-style form and list YAML embedded through pact.AdminAssets, blog:publish sets published_at by slug with a bound parameter - the posts route lists published posts only, newest first - Docker tests migrate an ICU pl-PL database, roll back published_at, serve the route and run blog:publish with a published and an opened database - the page gains the admin controller, console command and added-column sections
This commit is contained in:
34
docs/examples/blog/controllers/posts.go
Normal file
34
docs/examples/blog/controllers/posts.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Code generated by summer make. DO NOT EDIT.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/summercms/docs/examples/blog/models"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
var (
|
||||
_ pact.AdminController = postsAdmin{}
|
||||
_ pact.AdminRecordSource = postsAdmin{}
|
||||
_ pact.AdminPermissioned = postsAdmin{}
|
||||
)
|
||||
|
||||
// postsAdmin is the Go form of the Posts backend controller with the List
|
||||
// and Form behaviours.
|
||||
type postsAdmin struct{}
|
||||
|
||||
func (postsAdmin) ID() string { return "acme.blog.posts" }
|
||||
func (postsAdmin) ModelName() string { return "Post" }
|
||||
func (postsAdmin) ConfigDir() string { return "controllers/posts" }
|
||||
|
||||
// NewRecord returns the model the generic admin handlers query and fill.
|
||||
func (postsAdmin) NewRecord() any { return &models.Post{} }
|
||||
|
||||
// RequiredPermissions replaces $requiredPermissions: an administrator needs
|
||||
// this permission before any schema or record is served.
|
||||
func (postsAdmin) RequiredPermissions() []string {
|
||||
return []string{"acme.blog.access_posts"}
|
||||
}
|
||||
|
||||
// PostsController returns the acme.blog.posts admin controller.
|
||||
func PostsController() pact.AdminController { return postsAdmin{} }
|
||||
12
docs/examples/blog/controllers/posts/config_form.yaml
Normal file
12
docs/examples/blog/controllers/posts/config_form.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
name: acme.blog::lang.posts.post
|
||||
form: ~/plugins/acme/blog/models/posts/fields.yaml
|
||||
modelClass: Post
|
||||
defaultRedirect: acme/blog/posts
|
||||
|
||||
create:
|
||||
redirect: acme/blog/posts/update/:id
|
||||
redirectClose: acme/blog/posts
|
||||
|
||||
update:
|
||||
redirect: acme/blog/posts
|
||||
redirectClose: acme/blog/posts
|
||||
13
docs/examples/blog/controllers/posts/config_list.yaml
Normal file
13
docs/examples/blog/controllers/posts/config_list.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
list: ~/plugins/acme/blog/models/posts/columns.yaml
|
||||
modelClass: Post
|
||||
title: acme.blog::lang.posts.title
|
||||
recordUrl: acme/blog/posts/update/:id
|
||||
recordsPerPage: 20
|
||||
showCheckboxes: true
|
||||
defaultSort:
|
||||
column: published_at
|
||||
direction: desc
|
||||
toolbar:
|
||||
buttons: [create, delete]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
99
docs/examples/blog/controllers/posts_test.go
Normal file
99
docs/examples/blog/controllers/posts_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package controllers_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/docs/examples/blog"
|
||||
"git.golem15.com/golem15/summercms/docs/examples/blog/controllers"
|
||||
"git.golem15.com/golem15/summercms/docs/examples/blog/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
func TestPostsControllerDeclaration(t *testing.T) {
|
||||
ctl := controllers.PostsController()
|
||||
if ctl.ID() != "acme.blog.posts" {
|
||||
t.Errorf("ID = %q, want acme.blog.posts", ctl.ID())
|
||||
}
|
||||
if ctl.ModelName() != "Post" {
|
||||
t.Errorf("ModelName = %q, want Post", ctl.ModelName())
|
||||
}
|
||||
if ctl.ConfigDir() != "controllers/posts" {
|
||||
t.Errorf("ConfigDir = %q, want controllers/posts", ctl.ConfigDir())
|
||||
}
|
||||
perm, ok := ctl.(pact.AdminPermissioned)
|
||||
if !ok {
|
||||
t.Fatal("the controller does not declare a permission (pact.AdminPermissioned)")
|
||||
}
|
||||
if got := perm.RequiredPermissions(); !slices.Equal(got, []string{"acme.blog.access_posts"}) {
|
||||
t.Errorf("RequiredPermissions = %v, want [acme.blog.access_posts]", got)
|
||||
}
|
||||
src, ok := ctl.(pact.AdminRecordSource)
|
||||
if !ok {
|
||||
t.Fatal("the controller does not implement pact.AdminRecordSource")
|
||||
}
|
||||
if _, ok := src.NewRecord().(*models.Post); !ok {
|
||||
t.Errorf("NewRecord is %T, want *models.Post", src.NewRecord())
|
||||
}
|
||||
|
||||
plugin := &blog.Plugin{}
|
||||
fsys := plugin.AdminFS()
|
||||
form, err := cabana.CompileForm("acme.blog", ctl, fsys)
|
||||
if err != nil {
|
||||
t.Fatalf("CompileForm: %v", err)
|
||||
}
|
||||
var fields []string
|
||||
for _, f := range form.Fields {
|
||||
fields = append(fields, f.Name)
|
||||
}
|
||||
if want := []string{"title", "slug", "body"}; !slices.Equal(fields, want) {
|
||||
t.Errorf("form fields = %v, want %v", fields, want)
|
||||
}
|
||||
list, err := cabana.CompileList("acme.blog", ctl, fsys)
|
||||
if err != nil {
|
||||
t.Fatalf("CompileList: %v", err)
|
||||
}
|
||||
if list.ModelClass != "Post" || list.DefaultSort == nil {
|
||||
t.Errorf("list = %+v, want modelClass Post with a default sort", list)
|
||||
}
|
||||
for _, name := range []string{"controllers/posts/config_form.yaml", "controllers/posts/config_list.yaml", "models/posts/fields.yaml", "models/posts/columns.yaml"} {
|
||||
if _, err := fs.Stat(fsys, name); err != nil {
|
||||
t.Errorf("AdminFS is missing %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// cabana compiles every admin controller at start-up without a database.
|
||||
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Env: "testing", Environ: []string{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.Set("admin.jwt.secret", "test-only-secret-with-at-least-32-bytes"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
routes, err := cabana.Activate(backpack.New(cfg), []party.Plugin{plugin})
|
||||
if err != nil {
|
||||
t.Fatalf("cabana.Activate: %v", err)
|
||||
}
|
||||
if routes == nil {
|
||||
t.Fatal("cabana.Activate mounted no admin routes")
|
||||
}
|
||||
|
||||
editor := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: map[string]bool{"acme.blog.access_posts": true}}
|
||||
visitor := &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{}}
|
||||
if !cabana.Allows(editor, perm.RequiredPermissions()) {
|
||||
t.Error("an administrator with acme.blog.access_posts is refused")
|
||||
}
|
||||
if cabana.Allows(visitor, perm.RequiredPermissions()) {
|
||||
t.Error("an administrator without acme.blog.access_posts is allowed")
|
||||
}
|
||||
declared := slices.ContainsFunc(plugin.Permissions(), func(p pact.Permission) bool { return p.Code == "acme.blog.access_posts" })
|
||||
if !declared {
|
||||
t.Error("the plugin does not declare acme.blog.access_posts in Permissions")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user