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
This commit is contained in:
117
controllers/organisations_admin_controller.go
Normal file
117
controllers/organisations_admin_controller.go
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user