feat(17-03): register the journal-post menu item type
- Plugin implements pact.HasMenuItemTypes with a lazy app accessor - journal-post: references by title, resolve fills the layered pattern by ordered replacement (id, slug, category, year, month, day) and builds the ordered per-locale alternates with translated slugs (x-default without enabled locales) - menuitem.journal_post labels in en and pl - TestMenuItemTypesSmoke: host override, item override, sort_order alternates
This commit is contained in:
439
classes/menu_item_types.go
Normal file
439
classes/menu_item_types.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package classes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-journal-plugin/models"
|
||||
translatemodels "git.golem15.com/golem15/sm-translate-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// The Journal menu item type keys, the PHP keys of Plugin.php
|
||||
// extendWinterPagesPlugin (D-04).
|
||||
const (
|
||||
MenuTypePost = "journal-post"
|
||||
)
|
||||
|
||||
// The phrase keys of the menu item type labels.
|
||||
const (
|
||||
menuLabelPost = "golem15.journal::lang.menuitem.journal_post"
|
||||
)
|
||||
|
||||
// The default URL patterns of the menu item types. A host overrides them
|
||||
// through the registry's menu_items.patterns config, an item through its own
|
||||
// URL pattern.
|
||||
const (
|
||||
DefaultPostPattern = "/blog/:slug"
|
||||
)
|
||||
|
||||
// xDefaultLocale is the single alternate's locale when translate has no
|
||||
// enabled locale (PHP buildAlternateLinks).
|
||||
const xDefaultLocale = "x-default"
|
||||
|
||||
// postPlaceholders are the placeholders a post URL pattern understands, in
|
||||
// the order PHP getMenuTypeInfo lists them.
|
||||
var postPlaceholders = []string{":slug", ":category", ":id", ":year", ":month", ":day"}
|
||||
|
||||
// MenuItemTypes returns the Journal menu item types, the typed successors
|
||||
// of the PHP pages.menuitem handlers of Post and Category (headless branch):
|
||||
//
|
||||
// - journal-post: one post picked by id (References lists every post by
|
||||
// title); it resolves to the post's URL without a published filter.
|
||||
//
|
||||
// URLs are built from the pattern the registry layered (MenuItemResolve
|
||||
// Pattern) by ordered literal replacement of the placeholders, as PHP
|
||||
// buildUrlFromPattern did: posts fill :id, :slug, :category (the slug of the
|
||||
// post's first category by name) and, for a published post with a publish
|
||||
// date, :year, :month and :day (UTC). Alternates hold one URL per enabled
|
||||
// translate locale in sort_order (ties by id): the unprefixed locale
|
||||
// (MenuItemResolve.UnprefixedLocale, else translate's default locale) has no
|
||||
// prefix, every other locale the prefix /code, and slugs are translated per
|
||||
// locale with the default value as fallback. Without an enabled locale the
|
||||
// only alternate is x-default. The item URL is the first alternate's URL, as
|
||||
// PHP took reset($alternateLinks).
|
||||
//
|
||||
// app is asked for the booted application at request time; its *gorm.DB is
|
||||
// the database the types read.
|
||||
func MenuItemTypes(app func() *backpack.App) []pact.MenuItemType {
|
||||
m := menuTypes{app: app}
|
||||
return []pact.MenuItemType{
|
||||
{
|
||||
Key: MenuTypePost,
|
||||
Label: menuLabelPost,
|
||||
DefaultPattern: DefaultPostPattern,
|
||||
Placeholders: append([]string(nil), postPlaceholders...),
|
||||
References: m.postReferences,
|
||||
Resolve: m.resolvePost,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// menuTypes carries the lazy app accessor the type closures share.
|
||||
type menuTypes struct {
|
||||
app func() *backpack.App
|
||||
}
|
||||
|
||||
func (m menuTypes) db(ctx context.Context) (*gorm.DB, error) {
|
||||
if m.app == nil {
|
||||
return nil, errors.New("golem15.journal: menu item types: app is not available")
|
||||
}
|
||||
app := m.app()
|
||||
if app == nil {
|
||||
return nil, errors.New("golem15.journal: menu item types: app is not booted")
|
||||
}
|
||||
db, ok := app.Lookup[*gorm.DB]()
|
||||
if !ok || db == nil {
|
||||
return nil, errors.New("golem15.journal: menu item types: database is not configured")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return db.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
// postReferences lists every post ordered by title (PHP getMenuTypeInfo),
|
||||
// ties by id.
|
||||
func (m menuTypes) postReferences(ctx context.Context) ([]pact.MenuItemReference, error) {
|
||||
db, err := m.db(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var posts []models.Post
|
||||
if err := db.Select("id", "title").Order("title ASC, id ASC").Find(&posts).Error; err != nil {
|
||||
return nil, fmt.Errorf("golem15.journal: menu item post references: %w", err)
|
||||
}
|
||||
refs := make([]pact.MenuItemReference, 0, len(posts))
|
||||
for _, p := range posts {
|
||||
refs = append(refs, pact.MenuItemReference{Value: strconv.FormatUint(uint64(p.ID), 10), Label: p.Title})
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
// resolvePost resolves one journal-post item. An unknown or missing
|
||||
// reference resolves to nothing, as PHP returned null.
|
||||
func (m menuTypes) resolvePost(ctx context.Context, in pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) {
|
||||
id, ok := menuReferenceID(in.Reference)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
db, err := m.db(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var posts []models.Post
|
||||
if err := db.Where("id = ?", id).Limit(1).Find(&posts).Error; err != nil {
|
||||
return nil, fmt.Errorf("golem15.journal: menu item post %d: %w", id, err)
|
||||
}
|
||||
if len(posts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
items, err := resolvePostItems(ctx, db, posts, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
// resolvePostItems resolves posts to one item each, in the given order.
|
||||
func resolvePostItems(ctx context.Context, db *gorm.DB, posts []models.Post, in pact.MenuItemResolve) ([]pact.ResolvedMenuItem, error) {
|
||||
locales, err := loadMenuLocales(ctx, db, in.UnprefixedLocale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uint, 0, len(posts))
|
||||
for _, p := range posts {
|
||||
ids = append(ids, p.ID)
|
||||
}
|
||||
postTr, err := loadMenuTranslations(ctx, db, models.Post{}.MorphName(), ids, locales)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firsts, err := firstPostCategories(ctx, db, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
catIDs := []uint{}
|
||||
for _, c := range firsts {
|
||||
catIDs = append(catIDs, c.ID)
|
||||
}
|
||||
catTr, err := loadMenuTranslations(ctx, db, models.Category{}.MorphName(), catIDs, locales)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]pact.ResolvedMenuItem, 0, len(posts))
|
||||
for i := range posts {
|
||||
p := &posts[i]
|
||||
cat, hasCat := firsts[p.ID]
|
||||
params := func(locale string) [][2]string {
|
||||
slug := p.Slug
|
||||
catSlug := ""
|
||||
if hasCat {
|
||||
catSlug = cat.Slug
|
||||
}
|
||||
if locale != "" {
|
||||
slug = postTr.value(locale, p.ID, "slug", slug)
|
||||
if hasCat {
|
||||
catSlug = catTr.value(locale, cat.ID, "slug", catSlug)
|
||||
}
|
||||
}
|
||||
return postURLParams(p, slug, catSlug)
|
||||
}
|
||||
title := func(locale string) string {
|
||||
if locale == "" {
|
||||
return p.Title
|
||||
}
|
||||
return postTr.value(locale, p.ID, "title", p.Title)
|
||||
}
|
||||
out = append(out, resolvedMenuItem(in.Pattern, locales, p.Title, p.UpdatedAt, params, title))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// postURLParams are a post's placeholder values in PHP
|
||||
// getHeadlessUrlParams order: id, slug, category, then year, month and day
|
||||
// for a published post with a publish date.
|
||||
func postURLParams(p *models.Post, slug, categorySlug string) [][2]string {
|
||||
params := [][2]string{
|
||||
{"id", strconv.FormatUint(uint64(p.ID), 10)},
|
||||
{"slug", slug},
|
||||
{"category", categorySlug},
|
||||
}
|
||||
if p.Published && p.PublishedAt != nil {
|
||||
at := p.PublishedAt.UTC()
|
||||
params = append(params,
|
||||
[2]string{"year", fmt.Sprintf("%04d", at.Year())},
|
||||
[2]string{"month", fmt.Sprintf("%02d", int(at.Month()))},
|
||||
[2]string{"day", fmt.Sprintf("%02d", at.Day())},
|
||||
)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// firstPostCategories maps each post id to its first category by name (the
|
||||
// PHP categories relation's order), ties by id.
|
||||
func firstPostCategories(ctx context.Context, db *gorm.DB, postIDs []uint) (map[uint]models.Category, error) {
|
||||
out := map[uint]models.Category{}
|
||||
if len(postIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
var rows []struct {
|
||||
PostID uint
|
||||
models.Category
|
||||
}
|
||||
err := db.WithContext(ctx).
|
||||
Table("golem15_journal_categories AS c").
|
||||
Select("pc.post_id AS post_id, c.*").
|
||||
Joins("JOIN golem15_journal_posts_categories AS pc ON pc.category_id = c.id").
|
||||
Where("pc.post_id IN ?", postIDs).
|
||||
Order("c.name ASC, c.id ASC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("golem15.journal: menu item post categories: %w", err)
|
||||
}
|
||||
for _, r := range rows {
|
||||
if _, seen := out[r.PostID]; !seen {
|
||||
out[r.PostID] = r.Category
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// resolvedMenuItem builds one resolved item: its alternates from pattern
|
||||
// per locale, its URL the first alternate's URL (else the pattern filled
|
||||
// with the default values), its title and mtime.
|
||||
func resolvedMenuItem(pattern string, locales menuLocales, title string, mtime *time.Time, params func(locale string) [][2]string, titleIn func(locale string) string) pact.ResolvedMenuItem {
|
||||
alternates := menuAlternates(pattern, locales, params, titleIn)
|
||||
url := ""
|
||||
if len(alternates) > 0 {
|
||||
url = alternates[0].URL
|
||||
}
|
||||
if url == "" {
|
||||
url = fillMenuPattern(pattern, params(""))
|
||||
}
|
||||
item := pact.ResolvedMenuItem{
|
||||
Title: title,
|
||||
URL: url,
|
||||
Alternates: alternates,
|
||||
}
|
||||
if mtime != nil {
|
||||
t := *mtime
|
||||
item.MTime = &t
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// menuAlternates is PHP buildAlternateLinks: x-default alone without an
|
||||
// enabled locale, else one URL per enabled locale in order, prefixed with
|
||||
// /code except for the unprefixed locale.
|
||||
func menuAlternates(pattern string, locales menuLocales, params func(locale string) [][2]string, title func(locale string) string) []pact.MenuItemAlternate {
|
||||
if len(locales.codes) == 0 {
|
||||
return []pact.MenuItemAlternate{{
|
||||
Locale: xDefaultLocale,
|
||||
URL: fillMenuPattern(pattern, params("")),
|
||||
Title: title(""),
|
||||
}}
|
||||
}
|
||||
out := make([]pact.MenuItemAlternate, 0, len(locales.codes))
|
||||
for _, code := range locales.codes {
|
||||
prefix := ""
|
||||
if code != locales.unprefixed {
|
||||
prefix = "/" + code
|
||||
}
|
||||
out = append(out, pact.MenuItemAlternate{
|
||||
Locale: code,
|
||||
URL: prefix + fillMenuPattern(pattern, params(code)),
|
||||
Title: title(code),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fillMenuPattern replaces each ":key" in pattern with its value, in params
|
||||
// order, as PHP buildUrlFromPattern's str_replace loop did.
|
||||
func fillMenuPattern(pattern string, params [][2]string) string {
|
||||
url := pattern
|
||||
for _, kv := range params {
|
||||
url = strings.ReplaceAll(url, ":"+kv[0], kv[1])
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// menuReferenceID parses an item reference as a record id.
|
||||
func menuReferenceID(ref string) (uint, bool) {
|
||||
n, err := strconv.ParseUint(strings.TrimSpace(ref), 10, 32)
|
||||
if err != nil || n == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return uint(n), true
|
||||
}
|
||||
|
||||
// menuLocales is the enabled translate locales in sort_order (ties by id),
|
||||
// the default locale whose values live on the host columns, and the locale
|
||||
// whose URLs carry no prefix.
|
||||
type menuLocales struct {
|
||||
codes []string
|
||||
defaultCode string
|
||||
unprefixed string
|
||||
}
|
||||
|
||||
// loadMenuLocales reads the enabled locales. unprefixed names the
|
||||
// unprefixed locale; "" means translate's default locale.
|
||||
func loadMenuLocales(ctx context.Context, db *gorm.DB, unprefixed string) (menuLocales, error) {
|
||||
var rows []translatemodels.Locale
|
||||
err := db.WithContext(ctx).
|
||||
Where("is_enabled = ?", true).
|
||||
Order("sort_order ASC, id ASC").
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return menuLocales{}, fmt.Errorf("golem15.journal: menu item locales: %w", err)
|
||||
}
|
||||
out := menuLocales{unprefixed: strings.TrimSpace(unprefixed)}
|
||||
for _, l := range rows {
|
||||
out.codes = append(out.codes, l.Code)
|
||||
if l.IsDefault && out.defaultCode == "" {
|
||||
out.defaultCode = l.Code
|
||||
}
|
||||
}
|
||||
if out.unprefixed == "" {
|
||||
out.unprefixed = out.defaultCode
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// menuTranslations holds the translated attributes of a set of records:
|
||||
// locale, then record id, then field.
|
||||
type menuTranslations struct {
|
||||
defaultCode string
|
||||
data map[string]map[uint]map[string]any
|
||||
}
|
||||
|
||||
// value is the record's field in locale with PHP's fallback: the default
|
||||
// locale and a missing or falsy translation read the host value.
|
||||
func (t menuTranslations) value(locale string, id uint, field, host string) string {
|
||||
if locale == "" || locale == t.defaultCode {
|
||||
return host
|
||||
}
|
||||
v, ok := t.data[locale][id][field]
|
||||
if !ok || !phpTruthy(v) {
|
||||
return host
|
||||
}
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
// loadMenuTranslations reads the translate attribute rows of the records
|
||||
// ids of morph in every non-default enabled locale with one query.
|
||||
func loadMenuTranslations(ctx context.Context, db *gorm.DB, morph string, ids []uint, locales menuLocales) (menuTranslations, error) {
|
||||
out := menuTranslations{defaultCode: locales.defaultCode, data: map[string]map[uint]map[string]any{}}
|
||||
var codes []string
|
||||
for _, c := range locales.codes {
|
||||
if c != locales.defaultCode {
|
||||
codes = append(codes, c)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 || len(codes) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
keys := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
keys = append(keys, strconv.FormatUint(uint64(id), 10))
|
||||
}
|
||||
var rows []translatemodels.Attribute
|
||||
err := db.WithContext(ctx).
|
||||
Where("model_type = ? AND locale IN ? AND model_id IN ?", morph, codes, keys).
|
||||
Order("id ASC").
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("golem15.journal: menu item translations: %w", err)
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.ModelID == nil || r.AttributeData == nil || strings.TrimSpace(*r.AttributeData) == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(*r.ModelID), 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var fields map[string]any
|
||||
if err := json.Unmarshal([]byte(*r.AttributeData), &fields); err != nil {
|
||||
continue
|
||||
}
|
||||
if out.data[r.Locale] == nil {
|
||||
out.data[r.Locale] = map[uint]map[string]any{}
|
||||
}
|
||||
if _, seen := out.data[r.Locale][uint(id)]; !seen {
|
||||
out.data[r.Locale][uint(id)] = fields
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// phpTruthy is PHP's boolean cast of a decoded JSON value, the test PHP
|
||||
// hasTranslation applied before it used a translation.
|
||||
func phpTruthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case string:
|
||||
return t != "" && t != "0"
|
||||
case bool:
|
||||
return t
|
||||
case float64:
|
||||
return t != 0
|
||||
case []any:
|
||||
return len(t) > 0
|
||||
case map[string]any:
|
||||
return len(t) > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -131,6 +131,8 @@ tag:
|
||||
delete_confirm: Delete this tag?
|
||||
delete_success: Successfully deleted those tags.
|
||||
return_to_tags: Return to the journal tag list
|
||||
menuitem:
|
||||
journal_post: Journal post
|
||||
scopes:
|
||||
journal: Journal
|
||||
journal_description: Create, edit, publish and delete posts as you, within your Journal permissions.
|
||||
|
||||
@@ -83,6 +83,8 @@ category:
|
||||
delete_success: Kategorie zostały pomyślnie usunięte.
|
||||
return_to_categories: Wróć do listy kategorii
|
||||
reorder: Zmień kolejność kategorii
|
||||
menuitem:
|
||||
journal_post: Wpis (Journal)
|
||||
scopes:
|
||||
journal: Journal
|
||||
journal_description: Twórz, edytuj, publikuj i usuwaj posty jako Ty, w ramach swoich uprawnień Journal.
|
||||
|
||||
22
menu_item_types.go
Normal file
22
menu_item_types.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package journal
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-journal-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// MenuItemTypes registers the Journal menu item types under their PHP keys
|
||||
// (classes.MenuItemTypes) with a lazy accessor of the booted app, so menus
|
||||
// and sitemaps can address Journal posts and categories through the
|
||||
// framework's menu item type registry.
|
||||
func (p *Plugin) MenuItemTypes() []pact.MenuItemType {
|
||||
return classes.MenuItemTypes(func() *backpack.App {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.app
|
||||
})
|
||||
}
|
||||
|
||||
var _ pact.HasMenuItemTypes = (*Plugin)(nil)
|
||||
150
menu_item_types_smoke_test.go
Normal file
150
menu_item_types_smoke_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package journal_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-journal-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-journal-plugin/models"
|
||||
translate "git.golem15.com/golem15/sm-translate-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
"git.golem15.com/golem15/summercms/modules/signpost"
|
||||
"gocloud.dev/blob/memblob"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// bootMenuItemTypes boots translate and Journal on a fresh database with
|
||||
// extra config files (name to YAML) and returns the app, the database and
|
||||
// the signpost registry.
|
||||
func bootMenuItemTypes(t *testing.T, files map[string]string) (*backpack.App, *gorm.DB, *signpost.Registry) {
|
||||
t.Helper()
|
||||
sqlDB := dedicatedDB(t, "journal_menu_"+strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||
gdb, err := lagoon.Use(t.Context(), sqlDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
files["app.yaml"] = "name: journal-menu\nlocale: en\nfallback_locale: en\nurl: https://app.example.test\n"
|
||||
for name, body := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cfg, err := compass.Open(compass.Options{
|
||||
Dir: dir,
|
||||
Environ: []string{"SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=" + adminJWTSecret},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bucket := memblob.OpenBucket(nil)
|
||||
t.Cleanup(func() { _ = bucket.Close() })
|
||||
if err := attach.Publish(app, bucket); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugins, err := party.Activate(app, []string{"golem15.translate", "golem15.journal"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := lagoon.Migrate(gdb, plugins); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reg, ok := app.Lookup[*signpost.Registry]()
|
||||
if !ok || reg == nil {
|
||||
t.Fatal("signpost registry is not published")
|
||||
}
|
||||
return app, gdb, reg
|
||||
}
|
||||
|
||||
func menuPost(t *testing.T, gdb *gorm.DB, title, slug string, published bool, at *time.Time) models.Post {
|
||||
t.Helper()
|
||||
p := models.Post{Title: title, Slug: slug, Content: "x", Published: published, PublishedAt: at}
|
||||
if err := gdb.Create(&p).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func altString(alts []pact.MenuItemAlternate) string {
|
||||
parts := make([]string, 0, len(alts))
|
||||
for _, a := range alts {
|
||||
parts = append(parts, a.Locale+"="+a.URL)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// TestMenuItemTypesSmoke boots Journal with translate, checks the
|
||||
// registered types and resolves them through the signpost registry with host
|
||||
// pattern overrides and ordered alternates.
|
||||
func TestMenuItemTypesSmoke(t *testing.T) {
|
||||
requirePostgres(t)
|
||||
ctx := context.Background()
|
||||
_, gdb, reg := bootMenuItemTypes(t, map[string]string{
|
||||
"menu_items.yaml": "patterns:\n journal-post: /news/:slug\n",
|
||||
})
|
||||
|
||||
typ, ok := reg.Type(classes.MenuTypePost)
|
||||
if !ok {
|
||||
t.Fatalf("%s is not registered", classes.MenuTypePost)
|
||||
}
|
||||
if typ.DefaultPattern != classes.DefaultPostPattern || typ.References == nil {
|
||||
t.Fatalf("journal-post type = %+v", typ)
|
||||
}
|
||||
|
||||
// pl sorts before the default en, so the first alternate (the item
|
||||
// URL, PHP reset()) is the prefixed pl URL.
|
||||
if err := gdb.Exec(`UPDATE golem15_translate_locales SET sort_order = CASE code WHEN 'pl' THEN 1 WHEN 'en' THEN 2 ELSE sort_order END`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
at := time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC)
|
||||
post := menuPost(t, gdb, "Hello", "hello", true, &at)
|
||||
if err := translate.SetTranslated(ctx, gdb, &post, "slug", "pl", "witaj"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
menuPost(t, gdb, "Another", "another", false, nil)
|
||||
|
||||
refs, err := signpost.ReferenceOptions(ctx, reg, classes.MenuTypePost)
|
||||
if err != nil || !refs.Applies || len(refs.Options) != 2 || refs.Options[0].Label != "Another" || refs.Options[1].Label != "Hello" {
|
||||
t.Fatalf("post references = %+v err=%v", refs, err)
|
||||
}
|
||||
|
||||
got, err := reg.Resolve(ctx, classes.MenuTypePost, strconv.FormatUint(uint64(post.ID), 10), "", false, "")
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("resolve journal-post: %+v %v", got, err)
|
||||
}
|
||||
if want := "pl=/pl/news/witaj en=/news/hello"; altString(got.Alternates) != want {
|
||||
t.Fatalf("alternates = %q, want %q", altString(got.Alternates), want)
|
||||
}
|
||||
if got.URL != "/pl/news/witaj" || got.Title != "Hello" || got.MTime == nil {
|
||||
t.Fatalf("resolved = %+v", got)
|
||||
}
|
||||
|
||||
// The item override wins over the host override; the unprefixed locale
|
||||
// comes from the caller.
|
||||
got, err = reg.Resolve(ctx, classes.MenuTypePost, strconv.FormatUint(uint64(post.ID), 10), "/:year/:month/:day/:slug", false, "pl")
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("resolve with item pattern: %+v %v", got, err)
|
||||
}
|
||||
if want := "pl=/2026/03/04/witaj en=/en/2026/03/04/hello"; altString(got.Alternates) != want {
|
||||
t.Fatalf("alternates = %q, want %q", altString(got.Alternates), want)
|
||||
}
|
||||
|
||||
// An unknown reference resolves to nothing.
|
||||
if got, err := reg.Resolve(ctx, classes.MenuTypePost, "999999", "", false, ""); err != nil || got != nil {
|
||||
t.Fatalf("unknown post = %+v %v", got, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user