feat: tags, unpublish and image upload for the author

- GET /_bm/api/v1/tags: the jz_bm_tags dictionary, to material managers
- POST /materials/{id}/unpublish: back to drafts, no revision bump, the
  search document resynced
- POST /materials/{id}/images: one JPEG/PNG/GIF/WebP per request through
  storeMaterialUpload, refusals with reason type, size or count
- storeMaterialUpload/fileRefusalFor: attach.ErrNotImage is a type
  refusal, not a 500; storeMaterialFile keeps its texts
- the author's routes get their own per-user buckets (the inline
  throttle form shares one counter per user)
- README: the author routes and revision on the student detail
This commit is contained in:
Jakub Zych
2026-11-09 01:05:54 +01:00
parent 7bf18de987
commit 40e4a2438b
9 changed files with 499 additions and 27 deletions

View File

@@ -1,8 +1,12 @@
package api
import (
"errors"
"fmt"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
)
const fullEditBody = `{"revision":3,"title":"Notatka","type":"notatka","body":"Tekst","source":null,"url":null,"meeting_id":null,"tags":["lektura"]}`
@@ -144,3 +148,37 @@ func TestNormalizeBody(t *testing.T) {
}
}
}
// TestFileRefusalReasons checks how attach.Store errors become refusals of
// the material file routes: size for ErrTooLarge, type for ErrFileType,
// ErrMIMEType and the image-mode ErrNotImage (no longer a 500), and no
// refusal for other errors.
func TestFileRefusalReasons(t *testing.T) {
cases := []struct {
err error
reason string
}{
{attach.ErrTooLarge, "size"},
{attach.ErrFileType, "type"},
{attach.ErrMIMEType, "type"},
{attach.ErrNotImage, "type"},
{fmt.Errorf("store: %w", attach.ErrNotImage), "type"},
}
for _, tc := range cases {
why, ok := fileRefusalFor(tc.err, "a.png", imageLimits)
if !ok || why.Reason != tc.reason || !strings.HasPrefix(why.Text, "a.png: ") {
t.Errorf("fileRefusalFor(%v) = %+v %v, want reason %s", tc.err, why, ok, tc.reason)
}
}
if why, _ := fileRefusalFor(attach.ErrTooLarge, "a.png", imageLimits); why.Text != "a.png: the file is larger than 50 MB." {
t.Errorf("size text = %q", why.Text)
}
for _, err := range []error{nil, errors.New("disk full")} {
if why, ok := fileRefusalFor(err, "a.png", imageLimits); ok {
t.Errorf("fileRefusalFor(%v) = %+v, want no refusal", err, why)
}
}
if !imageLimits.Image || imageLimits.MaxBytes != MaterialFileMaxBytes {
t.Errorf("imageLimits = %+v", imageLimits)
}
}