feat: organisation logo (attachOne logo) and payload helpers

- Organisation gains the public attachOne logo (OrganisationLogoField),
  editable as an image upload on the Organisations form; avatar unchanged
- OrganisationLogoURL and OrganisationLogoFields give applications the
  has_logo, logo and logo_url keys for their organisation payloads
- no migration: attachments live in system_files
This commit is contained in:
Jakub Zych
2026-10-23 17:03:13 +02:00
parent ee5c096d0d
commit f20a8d3b98
8 changed files with 124 additions and 9 deletions

View File

@@ -0,0 +1,90 @@
package classes
import (
"context"
"errors"
"strconv"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
"gocloud.dev/blob"
"gorm.io/gorm"
)
// OrganisationLogoSize is the edge in pixels of the logo thumbnail
// OrganisationLogoFields answers.
const OrganisationLogoSize = 256
// organisationLogo loads the organisation's logo attachment, or nil.
func organisationLogo(ctx context.Context, app *backpack.App, org *models.Organisation) (*attach.File, error) {
if app == nil || org == nil || org.ID == 0 {
return nil, nil
}
gdb, ok := app.Lookup[*gorm.DB]()
if !ok || gdb == nil {
return nil, nil
}
var f attach.File
err := gdb.WithContext(ctx).
Where("attachment_type = ? AND attachment_id = ? AND field = ?", org.MorphName(), strconv.FormatUint(uint64(org.ID), 10), models.OrganisationLogoField).
Take(&f).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &f, nil
}
// OrganisationLogoURL returns the URL of the organisation's logo at size
// pixels, the way AvatarURL serves a user's picture: the thumbnail (size by
// size, mode auto) of the logo attachment, or the attachment's file path
// when the thumbnail fails (logged). It answers false when the organisation
// has no logo or no file bucket is published. The URL may be relative; a
// caller that needs an absolute URL resolves it. Only a failing database
// lookup is an error.
func OrganisationLogoURL(ctx context.Context, app *backpack.App, org *models.Organisation, size int) (string, bool, error) {
f, err := organisationLogo(ctx, app, org)
if err != nil || f == nil {
return "", false, err
}
bucket, ok := app.Lookup[*blob.Bucket]()
if !ok || bucket == nil {
return "", false, nil
}
url, err := f.Thumb(ctx, bucket, size, size, "auto")
if err != nil {
pluginLog.Error("golem15.user: organisation logo thumb", "err", err)
return "/" + attach.BlobKey(f.DiskName), true, nil
}
return url, true, nil
}
// OrganisationLogoFields answers the logo keys an application adds to its
// own organisation payloads, named like the user payload's avatar keys:
// has_logo (bool), logo (the attachment's disk name, or nil) and logo_url
// (the OrganisationLogoSize thumbnail URL, or nil). An organisation without
// a logo answers false, nil and nil. Only a failing database lookup is an
// error.
func OrganisationLogoFields(ctx context.Context, app *backpack.App, org *models.Organisation) (map[string]any, error) {
out := map[string]any{"has_logo": false, "logo": nil, "logo_url": nil}
f, err := organisationLogo(ctx, app, org)
if err != nil {
return nil, err
}
if f == nil {
return out, nil
}
out["has_logo"] = true
out["logo"] = f.DiskName
url, ok, err := OrganisationLogoURL(ctx, app, org, OrganisationLogoSize)
if err != nil {
return nil, err
}
if ok && url != "" {
out["logo_url"] = url
}
return out, nil
}