feat: G15Office job, widget settings and submissions admin, embed.js and the settings importer
- SyncG15Office files each submission as a G15Office task with its screenshot through a fetchguard client limited to the configured host; a River job with 3 attempts, PHP's messages on failure, nothing sent when unconfigured - the replay of PHP's recorded G15Office exchange (classes/testdata) asserts the task JSON, headers and the screenshot part - settings screen (cabana field types only), read-only submissions list, manage_settings permission and navigation - embed.js served byte for byte at /plugins/golem15/feedback/assets/js/embed.js - feedback:import-settings copies the WinterCMS settings row once
This commit is contained in:
180
console/import_settings.go
Normal file
180
console/import_settings.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Package console holds the golem15.feedback operator commands.
|
||||
package console
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-feedback-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ErrSettingsCustomised refuses an import over settings an operator has
|
||||
// already changed (the row was saved after it was created).
|
||||
var ErrSettingsCustomised = errors.New("golem15_feedback_settings was already changed; nothing was imported (pass --force to overwrite it)")
|
||||
|
||||
// importNotice is a clean stop: there is nothing to import.
|
||||
type importNotice struct{ msg string }
|
||||
|
||||
func (n *importNotice) Error() string { return n.msg }
|
||||
|
||||
// IsNothingToImport reports an import that stopped because the source
|
||||
// holds nothing (no system_settings table or no golem15_feedback_settings
|
||||
// row).
|
||||
func IsNothingToImport(err error) bool {
|
||||
var n *importNotice
|
||||
return errors.As(err, &n)
|
||||
}
|
||||
|
||||
// ImportSettings is feedback:import-settings: a one-time copy of the
|
||||
// WinterCMS widget settings (the system_settings row
|
||||
// golem15_feedback_settings) into the golem15_feedback_settings singleton,
|
||||
// including the widget key, so embed snippets keep working. It writes only
|
||||
// while the singleton still holds what it was created with (or does not
|
||||
// exist yet); --force overwrites changed settings.
|
||||
func ImportSettings(app *backpack.App) bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "feedback:import-settings",
|
||||
Description: "Import the WinterCMS golem15_feedback_settings row into golem15_feedback_settings, once.",
|
||||
Flags: []bonfire.Flag{{
|
||||
Name: "force",
|
||||
Description: "Overwrite settings that were already changed",
|
||||
Bare: true,
|
||||
}},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
if app == nil {
|
||||
return errors.New("feedback:import-settings: app is nil")
|
||||
}
|
||||
db, ok := app.Lookup[*gorm.DB]()
|
||||
if !ok || db == nil {
|
||||
return errors.New("feedback:import-settings: database is not configured")
|
||||
}
|
||||
force, _ := in.Flag("force")
|
||||
err := Import(ctx, db, force == "true")
|
||||
var notice *importNotice
|
||||
switch {
|
||||
case errors.As(err, ¬ice):
|
||||
out.Info(notice.msg)
|
||||
return nil
|
||||
case err != nil:
|
||||
out.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
out.Success("Imported the feedback widget settings.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Import copies the system_settings row into the singleton. Keys the row
|
||||
// does not hold keep their current (default) values.
|
||||
func Import(ctx context.Context, db *gorm.DB, force bool) error {
|
||||
raw, err := settingsValue(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var values map[string]any
|
||||
dec := json.NewDecoder(strings.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&values); err != nil {
|
||||
return fmt.Errorf("feedback:import-settings: the settings are not a JSON object: %w", err)
|
||||
}
|
||||
return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
s, err := models.Instance(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !force && !s.UpdatedAt.Equal(s.CreatedAt) {
|
||||
return ErrSettingsCustomised
|
||||
}
|
||||
apply(s, values)
|
||||
s.UpdatedAt = time.Now()
|
||||
return tx.WithContext(ctx).Save(s).Error
|
||||
})
|
||||
}
|
||||
|
||||
func settingsValue(ctx context.Context, db *gorm.DB) (string, error) {
|
||||
if !db.WithContext(ctx).Migrator().HasTable("system_settings") {
|
||||
return "", &importNotice{msg: "No system_settings table: nothing to import."}
|
||||
}
|
||||
var values []string
|
||||
if err := db.WithContext(ctx).Raw(`SELECT value FROM system_settings WHERE item = ?`, models.SettingsCode).Scan(&values).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return "", &importNotice{msg: "No " + models.SettingsCode + " row in system_settings: nothing to import."}
|
||||
}
|
||||
return values[0], nil
|
||||
}
|
||||
|
||||
func apply(s *models.Settings, v map[string]any) {
|
||||
if x, ok := v["enabled"]; ok {
|
||||
s.Enabled = truthy(x)
|
||||
}
|
||||
if x, ok := v["allow_hide"]; ok {
|
||||
s.AllowHide = truthy(x)
|
||||
}
|
||||
if key := str(v["widget_key"]); key != nil && strings.TrimSpace(*key) != "" {
|
||||
s.WidgetKey = *key
|
||||
}
|
||||
for name, field := range map[string]**string{
|
||||
"position": &s.Position,
|
||||
"allowed_origins": &s.AllowedOrigins,
|
||||
"color_primary": &s.ColorPrimary,
|
||||
"color_accent": &s.ColorAccent,
|
||||
"label_title_pl": &s.LabelTitlePL,
|
||||
"label_title_en": &s.LabelTitleEN,
|
||||
"label_placeholder_pl": &s.LabelPlaceholderPL,
|
||||
"label_placeholder_en": &s.LabelPlaceholderEN,
|
||||
"label_success_pl": &s.LabelSuccessPL,
|
||||
"label_success_en": &s.LabelSuccessEN,
|
||||
"g15office_task_status": &s.G15OfficeTaskStatus,
|
||||
"g15office_task_priority": &s.G15OfficeTaskPriority,
|
||||
} {
|
||||
if x, ok := v[name]; ok {
|
||||
*field = str(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// str is a stored scalar as a string; null stays nil.
|
||||
func str(v any) *string {
|
||||
var s string
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case string:
|
||||
s = t
|
||||
case json.Number:
|
||||
s = t.String()
|
||||
case bool:
|
||||
// PHP's string of a boolean.
|
||||
if t {
|
||||
s = "1"
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// truthy is PHP's (bool) of a stored switch value.
|
||||
func truthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case bool:
|
||||
return t
|
||||
case string:
|
||||
return t != "" && t != "0"
|
||||
case json.Number:
|
||||
f, err := t.Float64()
|
||||
return err != nil || f != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user