- golem15.seo.redirects list and form (UI-SPEC C8): chain rows marked, status, match, origin and chain filters, the navigation item lands here - a field-less extension on the controller refuses a loop with redirects.loop on target, answers the chain notice and records history - the status dropdown posts text, read through RedirectStatus
193 lines
6.6 KiB
Go
193 lines
6.6 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// RedirectsControllerID is the redirects screen (UI-SPEC C8).
|
|
const RedirectsControllerID = "golem15.seo.redirects"
|
|
|
|
// redirectsAdminController lists and edits redirects (D-16 to D-19). The
|
|
// loop refusal, the chain notice and history come from the plugin's own
|
|
// field-less form extension on this controller, which runs inside the
|
|
// save transaction once the row is written.
|
|
type redirectsAdminController struct {
|
|
app func() *backpack.App
|
|
}
|
|
|
|
func (redirectsAdminController) ID() string { return RedirectsControllerID }
|
|
func (redirectsAdminController) ModelName() string { return `Golem15\SEO\Models\Redirect` }
|
|
func (redirectsAdminController) ConfigDir() string { return "controllers/redirects" }
|
|
func (redirectsAdminController) RequiredPermissions() []string {
|
|
return []string{PermissionManage}
|
|
}
|
|
|
|
// NewRecord is a redirect with the form's defaults, so a save that omits
|
|
// an option keeps its default.
|
|
func (redirectsAdminController) NewRecord() any {
|
|
r := models.NewRedirect()
|
|
return &r
|
|
}
|
|
|
|
// FormBeforeCreate normalises the redirect and refuses an invalid source,
|
|
// target or regex, and a source and match another redirect has.
|
|
func (c redirectsAdminController) FormBeforeCreate(ctx context.Context, model any) error {
|
|
return c.check(ctx, model)
|
|
}
|
|
|
|
// FormBeforeUpdate checks an update like a create.
|
|
func (c redirectsAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
|
|
return c.check(ctx, model)
|
|
}
|
|
|
|
func (c redirectsAdminController) check(ctx context.Context, model any) error {
|
|
r, ok := model.(*models.Redirect)
|
|
if !ok || r == nil {
|
|
return nil
|
|
}
|
|
r.Normalize()
|
|
db, err := database(ctx, c.app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return problemsError(ctx, translator(c.app), r.Problems(db))
|
|
}
|
|
|
|
// FormAfterDelete records the deleted redirect in history inside the
|
|
// delete's transaction.
|
|
func (c redirectsAdminController) FormAfterDelete(ctx context.Context, model any) error {
|
|
r, ok := model.(*models.Redirect)
|
|
if !ok || r == nil {
|
|
return nil
|
|
}
|
|
svc, err := classes.NewService(appOf(c.app))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
db, err := database(ctx, c.app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return svc.Record(ctx, db, classes.Change{
|
|
Kind: models.SubjectRedirect,
|
|
SubjectID: strconv.FormatUint(uint64(r.ID), 10),
|
|
What: svc.Phrase("golem15.seo::lang.history.what_redirect", map[string]string{"url": r.Source}),
|
|
Before: classes.RedirectValues(*r),
|
|
})
|
|
}
|
|
|
|
// ListRowStates marks a chain (UI-SPEC C8): a redirect whose target is
|
|
// another redirect's source, labelled redirects.state_chain.
|
|
func (redirectsAdminController) ListRowStates(ctx context.Context, db *gorm.DB, records []any) ([][]pact.RowState, error) {
|
|
var targets []string
|
|
for _, record := range records {
|
|
if r, ok := record.(*models.Redirect); ok && r != nil && r.Target != "" {
|
|
targets = append(targets, r.Target)
|
|
}
|
|
}
|
|
sources := map[string]bool{}
|
|
if len(targets) > 0 {
|
|
var rows []string
|
|
if err := db.WithContext(ctx).Model(&models.Redirect{}).Where("source IN ?", targets).Pluck("source", &rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, s := range rows {
|
|
sources[s] = true
|
|
}
|
|
}
|
|
out := make([][]pact.RowState, len(records))
|
|
for i, record := range records {
|
|
if r, ok := record.(*models.Redirect); ok && r != nil && r.Target != "" && sources[r.Target] {
|
|
out[i] = []pact.RowState{pact.RowStateNegative}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// matchLabels and originLabels are the phrase keys of the match kinds and
|
|
// origins.
|
|
var (
|
|
matchLabels = map[string]string{
|
|
models.RedirectExact: "golem15.seo::lang.redirects.match_exact",
|
|
models.RedirectPattern: "golem15.seo::lang.redirects.match_pattern",
|
|
models.RedirectRegex: "golem15.seo::lang.redirects.match_regex",
|
|
}
|
|
originLabels = map[string]string{
|
|
models.OriginManual: "golem15.seo::lang.redirects.origin_manual",
|
|
models.OriginSlug: "golem15.seo::lang.redirects.origin_slug",
|
|
models.OriginImport: "golem15.seo::lang.redirects.origin_import",
|
|
models.OriginMCP: "golem15.seo::lang.redirects.origin_mcp",
|
|
models.OriginNotFound: "golem15.seo::lang.redirects.origin_notfound",
|
|
}
|
|
)
|
|
|
|
// FilterOptions serves the choices of the status, match, origin and chain
|
|
// filters.
|
|
func (redirectsAdminController) FilterOptions(scope string) []pact.Option {
|
|
switch scope {
|
|
case models.FilterRedirectStatus:
|
|
return []pact.Option{
|
|
{Value: "301", Label: "golem15.seo::lang.redirects.status_301"},
|
|
{Value: "302", Label: "golem15.seo::lang.redirects.status_302"},
|
|
{Value: "410", Label: "golem15.seo::lang.redirects.status_410"},
|
|
}
|
|
case models.FilterRedirectMatch:
|
|
out := make([]pact.Option, 0, len(matchLabels))
|
|
for _, m := range []string{models.RedirectExact, models.RedirectPattern, models.RedirectRegex} {
|
|
out = append(out, pact.Option{Value: m, Label: matchLabels[m]})
|
|
}
|
|
return out
|
|
case models.FilterRedirectOrigin:
|
|
out := make([]pact.Option, 0, len(models.RedirectOrigins))
|
|
for _, o := range models.RedirectOrigins {
|
|
out = append(out, pact.Option{Value: o, Label: originLabels[o]})
|
|
}
|
|
return out
|
|
case models.FilterRedirectChain:
|
|
return []pact.Option{
|
|
{Value: models.ChainOnly, Label: "golem15.seo::lang.redirects.filter_chain_only"},
|
|
{Value: models.ChainHide, Label: "golem15.seo::lang.redirects.filter_chain_hide"},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListExtendRecords shows the match and the origin of the list's rows as
|
|
// their labels in the request locale. The rows are list values only and
|
|
// never saved; the columns still sort by the stored values.
|
|
func (c redirectsAdminController) ListExtendRecords(ctx context.Context, _ *gorm.DB, records []any) error {
|
|
tr := translator(c.app)
|
|
for _, record := range records {
|
|
r, ok := record.(*models.Redirect)
|
|
if !ok || r == nil {
|
|
continue
|
|
}
|
|
if key, ok := matchLabels[r.Match]; ok {
|
|
r.Match = Phrase(ctx, tr, key, nil)
|
|
}
|
|
if key, ok := originLabels[r.Origin]; ok {
|
|
r.Origin = Phrase(ctx, tr, key, nil)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
_ pact.AdminController = redirectsAdminController{}
|
|
_ pact.AdminPermissioned = redirectsAdminController{}
|
|
_ pact.AdminRecordSource = redirectsAdminController{}
|
|
_ pact.FormBeforeCreate = redirectsAdminController{}
|
|
_ pact.FormBeforeUpdate = redirectsAdminController{}
|
|
_ pact.FormAfterDelete = redirectsAdminController{}
|
|
_ pact.ListRowStates = redirectsAdminController{}
|
|
_ pact.FilterOptions = redirectsAdminController{}
|
|
_ pact.ListExtendRecords = redirectsAdminController{}
|
|
)
|