fix: report the old translated content_html on API post events

syncTranslations adds content_html to a translation that sends content, after
snapshotPost read the old values, so the event saw content_html as newly
submitted and reported Old: nil for a translation that existed.
submittedTranslations now lists content_html whenever a non-blank content is
sent, so the snapshot and the event cover the same fields.
This commit is contained in:
2026-10-27 15:36:11 +01:00
parent 62c75b1fc2
commit 7a03640b88
2 changed files with 35 additions and 2 deletions

View File

@@ -2,9 +2,11 @@ package api
import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
"git.golem15.com/golem15/sm-journal-plugin/models"
@@ -121,7 +123,11 @@ func postSavedEvent(ctx context.Context, db *gorm.DB, before *postSnapshot, post
}
// submittedTranslations lists, per locale, the translated fields a request
// body submits with a value, in translatedEventFields order.
// body submits with a value, in translatedEventFields order. content_html is
// listed whenever a non-blank content is, as the write derives it from the
// content (classes.FormatTranslation): the snapshot before the write and the
// event after it then cover the same fields, whether or not the write has
// already added content_html to the body.
func submittedTranslations(body map[string]any) map[string][]string {
out := map[string][]string{}
m, ok := body["translations"].(map[string]any)
@@ -134,7 +140,8 @@ func submittedTranslations(body map[string]any) map[string][]string {
continue
}
for _, field := range translatedEventFields {
if v, has := attrs[field]; has && v != nil {
v, has := attrs[field]
if (has && v != nil) || (field == "content_html" && derivesContentHTML(attrs)) {
out[locale] = append(out[locale], field)
}
}
@@ -142,6 +149,13 @@ func submittedTranslations(body map[string]any) map[string][]string {
return out
}
// derivesContentHTML reports whether a translation's attributes carry a
// content the write turns into content_html (a non-blank value).
func derivesContentHTML(attrs map[string]any) bool {
content, has := attrs["content"]
return has && content != nil && strings.TrimSpace(fmt.Sprint(content)) != ""
}
// derefValue is a pointer column's value, nil for a nil pointer.
func derefValue[T any](p *T) any {
if p == nil {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"testing"
@@ -239,6 +240,24 @@ func TestAPIRecordEvents(t *testing.T) {
t.Fatalf("PUT described URL = %q", got[0].url)
}
// A translated content derives its content_html: the event reports the
// stored content_html as Old, not nil (WR-05).
env.expect(t, http.StatusOK, http.MethodPut, "/_journal/api/v1/posts/"+id, token, map[string]any{
"translations": map[string]any{"pl": map[string]any{"content": "Stara treść"}},
})
l.reset()
env.expect(t, http.StatusOK, http.MethodPut, "/_journal/api/v1/posts/"+id, token, map[string]any{
"translations": map[string]any{"pl": map[string]any{"content": "Nowa treść"}},
})
got = l.savedEvents()
if len(got) != 1 {
t.Fatalf("PUT content events = %+v, want one RecordSaved", got)
}
html, ok := findChange(got[0].ev.Changes, "content_html", "pl")
if old, _ := html.Old.(string); !ok || !strings.Contains(old, "Stara") || !strings.Contains(fmt.Sprint(html.New), "Nowa") {
t.Fatalf("pl content_html change = %+v (found %v); changes %+v", html, ok, got[0].ev.Changes)
}
// A write that fails after the row is saved rolls the row back and
// fires nothing (WR-04).
l.reset()