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 true }