Files
sm-user-plugin/controllers/organisations_admin_controller.go
Jakub Zych 58595fef27 feat: Organisations admin screen with the avatar and the members manager
- controller golem15.user.organisations behind golem15.users.access_users
- list and form (name, slug preset from the name, description, avatar)
- members relation manager: link sets and unlink clears users.organisation_id
- deleting an organisation releases its members first
- Organisation gains Fillable, Rules, MorphName, AttachRelations, BeforeValidate
  and the Members relation; models.Slugify
- navigation side item organisations
- TestAdminOrganisations, TestAdminOrganisationMembers, TestSlugify
2026-10-05 13:58:54 +02:00

118 lines
4.3 KiB
Go

package controllers
import (
"context"
"errors"
"regexp"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/cabana"
"gorm.io/gorm"
)
// organisationSlugPattern is the PHP alpha_dash rule on the slug, kept to
// ASCII. lagoon.Validate has no alpha_dash token, so the Form before-hooks
// check it.
var organisationSlugPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// membersRelation is the organisation's hasMany relation to its users.
const membersRelation = "members"
// organisationsAdminController is the Winter Organisations backend
// controller: the list, the form with the avatar, and the members relation
// manager. The screen and every write need golem15.users.access_users, as in
// PHP.
//
// The members manager sets and clears users.organisation_id and nothing
// else. It never writes users_groups.
type organisationsAdminController struct {
app appAccess
}
func (organisationsAdminController) ID() string { return "golem15.user.organisations" }
func (organisationsAdminController) ModelName() string { return `Golem15\User\Models\Organisation` }
func (organisationsAdminController) ConfigDir() string { return "controllers/organisations" }
func (organisationsAdminController) RequiredPermissions() []string {
return []string{PermissionAccessUsers}
}
func (organisationsAdminController) NewRecord() any { return &models.Organisation{} }
// FormBeforeCreate and FormBeforeUpdate check the slug's characters.
func (c organisationsAdminController) FormBeforeCreate(ctx context.Context, model any) error {
return c.requireSlugFormat(ctx, model)
}
func (c organisationsAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
return c.requireSlugFormat(ctx, model)
}
// requireSlugFormat answers 422 on slug when it holds anything but letters,
// digits, underscore and hyphen. An empty slug is left to the required rule.
func (c organisationsAdminController) requireSlugFormat(ctx context.Context, model any) error {
organisation, ok := model.(*models.Organisation)
if !ok || organisation == nil {
return errors.New("organisations admin: form model is not an organisation")
}
if organisation.Slug == "" || organisationSlugPattern.MatchString(organisation.Slug) {
return nil
}
message := "The slug may only contain letters, numbers, dashes and underscores."
if tr := c.app.translator(); tr != nil {
const key = "golem15.user::lang.organisation.slug_invalid"
if text := tr.Get(ctx, key, nil); text != "" && text != key {
message = text
}
}
return &cabana.ValidationError{Details: map[string]any{"slug": []string{message}}}
}
// AdminRelationContracts declares the members relation manager: the users
// whose organisation_id is the organisation. Linking sets the column and
// unlinking clears it; the column is nullable, so a user is never deleted.
func (organisationsAdminController) AdminRelationContracts() []cabana.RelationContract {
return []cabana.RelationContract{{
Name: membersRelation,
Kind: cabana.RelationHasMany,
NewRelated: func() any { return &models.User{} },
ForeignKey: "organisation_id",
Columns: map[string]string{
"name": "name",
"email": "email",
},
}}
}
// RelationExtendManageQuery fails closed: any relation name other than
// members has no candidates.
func (organisationsAdminController) RelationExtendManageQuery(_ context.Context, relation string, db *gorm.DB) *gorm.DB {
if db == nil {
return db
}
if relation != membersRelation {
return db.Where("1 = 0")
}
return db
}
// FormBeforeDelete releases the members of the organisation in the delete's
// transaction: organisation_id and organisation_role are cleared, on
// deactivated users too, so the delete does not fail on the users foreign key
// and no user is deleted.
func (organisationsAdminController) FormBeforeDelete(ctx context.Context, model any) error {
organisation, ok := model.(*models.Organisation)
if !ok || organisation == nil {
return errors.New("organisations admin: form model is not an organisation")
}
tx, ok := cabana.TxFromContext(ctx)
if !ok {
return errors.New("organisations admin: a delete needs its transaction")
}
return tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Unscoped().
Model(&models.User{}).Where("organisation_id = ?", organisation.ID).
Updates(map[string]any{"organisation_id": nil, "organisation_role": nil}).Error
}