feat(12.1-02): password and form-only fields, rules per operation and preset
- pact.FormVirtualFields lists form fields that are not model columns: never bound, filled or projected; their values reach the Form hooks through cabana.VirtualFieldsFromContext when the field's context allows the operation - type: password is a masked field that must be listed as virtual - pact.FormRules supplies the rule set per operation and replaces the model's Rules() for admin saves; a rule on a virtual field sees the submitted value - preset on a text field follows another text field on the create form - SPA: PasswordField, preset handling in FormView, empty password left out of an update - README, docs, OpenAPI document, TS types and dist updated
This commit is contained in:
115
modules/cabana/example_form_seams_test.go
Normal file
115
modules/cabana/example_form_seams_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package cabana_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// Member is the model behind the acme.roster members controller. Password
|
||||
// holds a hash; the form never reads or writes the column itself.
|
||||
type Member struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Slug string `gorm:"column:slug"`
|
||||
Password string `gorm:"column:password" json:"-"`
|
||||
}
|
||||
|
||||
func (Member) TableName() string { return "acme_roster_members" }
|
||||
|
||||
// Fillable lists the columns the admin form may write.
|
||||
func (Member) Fillable() []string { return []string{"name", "slug"} }
|
||||
|
||||
// Rules are the model's own rules, used wherever the controller sets none.
|
||||
func (Member) Rules() map[string]string {
|
||||
return map[string]string{"name": "required", "password": "required|between:8,255|confirmed"}
|
||||
}
|
||||
|
||||
// MembersController is an admin controller whose form has fields that are
|
||||
// not columns and rules of its own.
|
||||
type MembersController struct{}
|
||||
|
||||
var (
|
||||
_ pact.AdminController = MembersController{}
|
||||
_ pact.AdminRecordSource = MembersController{}
|
||||
_ pact.FormVirtualFields = MembersController{}
|
||||
_ pact.FormRules = MembersController{}
|
||||
_ pact.FormBeforeCreate = MembersController{}
|
||||
_ pact.FormBeforeUpdate = MembersController{}
|
||||
)
|
||||
|
||||
func (MembersController) ID() string { return "acme.roster.members" }
|
||||
func (MembersController) ModelName() string { return "Member" }
|
||||
func (MembersController) ConfigDir() string { return "controllers/members" }
|
||||
func (MembersController) NewRecord() any { return &Member{} }
|
||||
|
||||
// FormVirtualFields names the fields of fields.yaml that are not columns of
|
||||
// the form. cabana never fills or returns them.
|
||||
func (MembersController) FormVirtualFields() []string {
|
||||
return []string{"password", "password_confirmation", "notify"}
|
||||
}
|
||||
|
||||
// FormRules replaces the model's rules for admin saves: a create needs a
|
||||
// password, an update takes one only when the administrator types it.
|
||||
func (MembersController) FormRules(_ context.Context, op string) map[string]string {
|
||||
rules := map[string]string{"name": "required"}
|
||||
if op == "create" {
|
||||
rules["password"] = "required|between:8,255|confirmed"
|
||||
} else {
|
||||
rules["password"] = "nullable|between:8,255|confirmed"
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// FormBeforeCreate reads the submitted virtual values, which have passed the
|
||||
// rules by now, and stores what the model needs.
|
||||
func (MembersController) FormBeforeCreate(ctx context.Context, model any) error {
|
||||
values, _ := cabana.VirtualFieldsFromContext(ctx)
|
||||
member := model.(*Member)
|
||||
if plain, ok := values["password"].(string); ok && plain != "" {
|
||||
member.Password = hashPassword(plain)
|
||||
}
|
||||
if notify, _ := values["notify"].(bool); notify {
|
||||
// Queue the welcome message here.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormBeforeUpdate changes the password only when one was submitted.
|
||||
func (MembersController) FormBeforeUpdate(ctx context.Context, model any) error {
|
||||
values, _ := cabana.VirtualFieldsFromContext(ctx)
|
||||
if plain, ok := values["password"].(string); ok && plain != "" {
|
||||
model.(*Member).Password = hashPassword(plain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashPassword stands in for the application's password hasher.
|
||||
func hashPassword(plain string) string {
|
||||
sum := sha256.Sum256([]byte(plain))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// Example_formSeams shows what the controller declares. Outside a save there
|
||||
// are no submitted values, so the hook stores nothing.
|
||||
func Example_formSeams() {
|
||||
ctl := MembersController{}
|
||||
ctx := context.Background()
|
||||
fmt.Println(ctl.FormVirtualFields())
|
||||
fmt.Println("create:", ctl.FormRules(ctx, "create")["password"])
|
||||
fmt.Println("update:", ctl.FormRules(ctx, "update")["password"])
|
||||
|
||||
member := &Member{Name: "Ada"}
|
||||
_, inSave := cabana.VirtualFieldsFromContext(ctx)
|
||||
err := ctl.FormBeforeCreate(ctx, member)
|
||||
fmt.Println(inSave, err, member.Password == "")
|
||||
// Output:
|
||||
// [password password_confirmation notify]
|
||||
// create: required|between:8,255|confirmed
|
||||
// update: nullable|between:8,255|confirmed
|
||||
// false <nil> true
|
||||
}
|
||||
Reference in New Issue
Block a user