feat: versioning in awards definition
This commit is contained in:
@@ -126,6 +126,7 @@ const (
|
|||||||
keyAwardRefsUpdated = "awards.refs.updated." // + CODE → last list-update timestamp
|
keyAwardRefsUpdated = "awards.refs.updated." // + CODE → last list-update timestamp
|
||||||
keyAwardRefsSeeded = "awards.refs.seeded" // built-in reference-list seed version
|
keyAwardRefsSeeded = "awards.refs.seeded" // built-in reference-list seed version
|
||||||
keyAwardDefsFixed = "awards.defs.fixed" // built-in award def correction version
|
keyAwardDefsFixed = "awards.defs.fixed" // built-in award def correction version
|
||||||
|
keyAwardEditsSeeded = "awards.defs.editflag" // one-shot: back-fill the user-edited flag
|
||||||
|
|
||||||
keyClublogCtyEnabled = "clublog.cty_exceptions" // "1" → apply ClubLog exceptions
|
keyClublogCtyEnabled = "clublog.cty_exceptions" // "1" → apply ClubLog exceptions
|
||||||
|
|
||||||
@@ -2155,9 +2156,11 @@ func (a *App) awardDefs() []award.Def {
|
|||||||
if strings.TrimSpace(s) != "" {
|
if strings.TrimSpace(s) != "" {
|
||||||
var defs []award.Def
|
var defs []award.Def
|
||||||
if json.Unmarshal([]byte(s), &defs) == nil && len(defs) > 0 {
|
if json.Unmarshal([]byte(s), &defs) == nil && len(defs) > 0 {
|
||||||
// Upgrade legacy defs (pre-rich-model) in memory on every load.
|
// Upgrade legacy defs (pre-rich-model) in memory on every load, and
|
||||||
|
// reconcile with the catalog so a newly shipped (or newly fixed) award
|
||||||
|
// is visible even before migrateAwardDefs has written it back.
|
||||||
migrated, _ := award.Migrate(defs)
|
migrated, _ := award.Migrate(defs)
|
||||||
migrated, _ = mergeCatalog(migrated)
|
migrated, _, _ = mergeCatalog(migrated)
|
||||||
return migrated
|
return migrated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2165,25 +2168,88 @@ func (a *App) awardDefs() []award.Def {
|
|||||||
return award.Defaults()
|
return award.Defaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeCatalog adds the catalog awards that are missing from the stored
|
// mergeCatalog reconciles the stored award definitions with the catalog OpsLog
|
||||||
// definitions. This is how an award SHIPPED in a new release (FFMA, say) reaches
|
// ships. Two distinct jobs:
|
||||||
// an operator who already has awards saved: without it awardDefs() would keep
|
//
|
||||||
// returning the stored copy and the new award would simply never appear. Add-only
|
// - ADD an award the catalog has and storage doesn't (FFMA in a new release).
|
||||||
// — an award already there keeps the operator's edits, Valid=false included.
|
// Without this, awardDefs() would keep returning the stored copy for ever and
|
||||||
func mergeCatalog(defs []award.Def) ([]award.Def, bool) {
|
// a newly shipped award would simply never appear.
|
||||||
have := make(map[string]struct{}, len(defs))
|
// - UPDATE a shipped award whose catalog Version is higher than the stored one
|
||||||
for _, d := range defs {
|
// — that is how a FIX to an award we already distributed (a corrected OR
|
||||||
have[strings.ToUpper(strings.TrimSpace(d.Code))] = struct{}{}
|
// chain, a fixed reference list) actually reaches its users. Distributing an
|
||||||
|
// award you can never afterwards correct is only half a mechanism.
|
||||||
|
//
|
||||||
|
// An award the operator has edited (UserEdited) is NEVER updated: their work
|
||||||
|
// outranks ours. It keeps its stored version, so if they later reset it the
|
||||||
|
// update applies then.
|
||||||
|
//
|
||||||
|
// Returns the reconciled defs and the codes whose DEFINITION was replaced — the
|
||||||
|
// caller re-seeds those references from the catalog.
|
||||||
|
func mergeCatalog(defs []award.Def) ([]award.Def, []string, bool) {
|
||||||
|
idx := make(map[string]int, len(defs))
|
||||||
|
for i, d := range defs {
|
||||||
|
idx[strings.ToUpper(strings.TrimSpace(d.Code))] = i
|
||||||
}
|
}
|
||||||
added := false
|
var updated []string
|
||||||
for _, d := range award.Defaults() {
|
changed := false
|
||||||
if _, ok := have[strings.ToUpper(strings.TrimSpace(d.Code))]; ok {
|
for _, cd := range award.Defaults() {
|
||||||
|
code := strings.ToUpper(strings.TrimSpace(cd.Code))
|
||||||
|
i, ok := idx[code]
|
||||||
|
if !ok {
|
||||||
|
defs = append(defs, cd)
|
||||||
|
changed = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
defs = append(defs, d)
|
stored := defs[i]
|
||||||
added = true
|
if stored.UserEdited || cd.Version <= stored.Version {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return defs, added
|
// Keep the operator's own switch: an award they deliberately disabled must
|
||||||
|
// not come back enabled just because we shipped a fix. That is a preference,
|
||||||
|
// not a definition.
|
||||||
|
cd.Valid = stored.Valid
|
||||||
|
defs[i] = cd
|
||||||
|
updated = append(updated, code)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
return defs, updated, changed
|
||||||
|
}
|
||||||
|
|
||||||
|
func defByCode(defs []award.Def, code string) award.Def {
|
||||||
|
for _, d := range defs {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(d.Code), code) {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return award.Def{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reseedRefsFromCatalog replaces an award's reference list with the one the
|
||||||
|
// catalog ships. Only called for an award whose definition we just updated and
|
||||||
|
// that the operator has NOT edited, so nothing of theirs is overwritten. Awards
|
||||||
|
// whose list is generated (DXCC, French departments) or fetched online
|
||||||
|
// (POTA/SOTA/WWFF) ship none — those are left alone.
|
||||||
|
func (a *App) reseedRefsFromCatalog(code string) {
|
||||||
|
if a.awardRefs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, ok := award.CatalogRefs(code)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var refs []awardref.Ref
|
||||||
|
if err := json.Unmarshal(raw, &refs); err != nil || len(refs) == 0 {
|
||||||
|
if err != nil {
|
||||||
|
applog.Printf("awards: %s catalog references are malformed: %v", code, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := a.awardRefs.ReplaceAll(a.ctx, code, refs)
|
||||||
|
if err != nil {
|
||||||
|
applog.Printf("awards: re-seeding %s from the catalog failed: %v", code, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applog.Printf("awards: %s references re-seeded from the catalog — %d", code, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAwardDefs returns the (editable) award definitions.
|
// GetAwardDefs returns the (editable) award definitions.
|
||||||
@@ -2211,11 +2277,47 @@ func (a *App) migrateAwardDefs() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
migrated, changed := award.Migrate(defs)
|
migrated, changed := award.Migrate(defs)
|
||||||
// Awards added to the catalog since this operator last saved (a new release
|
|
||||||
// ships one) must be written back, or the editor would keep showing the old set.
|
// Back-fill the user-edited flag, once. The flag is what stops a catalog update
|
||||||
if merged, added := mergeCatalog(migrated); added {
|
// from overwriting the operator's own work — but it only starts being recorded
|
||||||
|
// from the release that introduced it, so an award customised BEFORE that would
|
||||||
|
// look pristine and get silently clobbered by the first update we ship.
|
||||||
|
//
|
||||||
|
// No award version has ever been bumped yet, so today any shipped award whose
|
||||||
|
// definition differs from the catalog can only have been changed by its owner.
|
||||||
|
// That makes the back-fill exact, and it is only ever right to run it once —
|
||||||
|
// after the first version bump, "differs from the catalog" would just mean
|
||||||
|
// "older revision".
|
||||||
|
if v, _ := a.settings.GetGlobal(a.ctx, keyAwardEditsSeeded); v != "1" {
|
||||||
|
cat := map[string]award.Def{}
|
||||||
|
for _, d := range award.Defaults() {
|
||||||
|
cat[strings.ToUpper(strings.TrimSpace(d.Code))] = d
|
||||||
|
}
|
||||||
|
for i := range migrated {
|
||||||
|
c, ok := cat[strings.ToUpper(strings.TrimSpace(migrated[i].Code))]
|
||||||
|
if ok && !migrated[i].UserEdited && !c.SameContent(migrated[i]) {
|
||||||
|
migrated[i].UserEdited = true
|
||||||
|
changed = true
|
||||||
|
applog.Printf("awards: %s differs from the catalog — marked as yours, catalog updates will leave it alone", migrated[i].Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.setSettingGlobal(keyAwardEditsSeeded, "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile with the catalog: awards ADDED since this operator last saved, and
|
||||||
|
// shipped awards whose definition we have since FIXED (a higher Version). Both
|
||||||
|
// must be written back, or the editor would keep showing the old set.
|
||||||
|
merged, updated, mc := mergeCatalog(migrated)
|
||||||
|
if mc {
|
||||||
migrated, changed = merged, true
|
migrated, changed = merged, true
|
||||||
}
|
}
|
||||||
|
// A replaced definition gets the catalog's reference list back too — a fix is
|
||||||
|
// usually BOTH (a new OR chain and the references its regexes rely on), and a
|
||||||
|
// half-applied update is worse than none.
|
||||||
|
for _, code := range updated {
|
||||||
|
applog.Printf("awards: %s updated from the catalog (v%d)", code, defByCode(migrated, code).Version)
|
||||||
|
a.reseedRefsFromCatalog(code)
|
||||||
|
}
|
||||||
// Version-gated correction of the built-in awards' Validate sources, which
|
// Version-gated correction of the built-in awards' Validate sources, which
|
||||||
// an earlier version wrongly set equal to Confirm (so VALIDATED == CONFIRMED
|
// an earlier version wrongly set equal to Confirm (so VALIDATED == CONFIRMED
|
||||||
// even for paper-QSL-only entities). Re-apply the canonical Confirm/Validate
|
// even for paper-QSL-only entities). Re-apply the canonical Confirm/Validate
|
||||||
@@ -2244,11 +2346,35 @@ func (a *App) migrateAwardDefs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markUserEdited flags every award whose definition differs from what was stored
|
||||||
|
// before this save. The flag is what makes a catalog update safe: it tells us
|
||||||
|
// which awards carry the operator's own work, so we never overwrite one with a
|
||||||
|
// newer shipped version. It is sticky — once edited, an award stays theirs until
|
||||||
|
// they reset it — and it is set here, on the save path, rather than trusted from
|
||||||
|
// the frontend, which has no reason to reason about it.
|
||||||
|
func markUserEdited(next, prev []award.Def) {
|
||||||
|
before := make(map[string]award.Def, len(prev))
|
||||||
|
for _, d := range prev {
|
||||||
|
before[strings.ToUpper(strings.TrimSpace(d.Code))] = d
|
||||||
|
}
|
||||||
|
for i := range next {
|
||||||
|
old, ok := before[strings.ToUpper(strings.TrimSpace(next[i].Code))]
|
||||||
|
if !ok {
|
||||||
|
continue // brand-new award: not a catalog award, nothing to protect it from
|
||||||
|
}
|
||||||
|
if old.UserEdited || !old.SameContent(next[i]) {
|
||||||
|
next[i].UserEdited = true
|
||||||
|
}
|
||||||
|
next[i].Version = old.Version // the version says which SHIPPED revision this came from; a save never bumps it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SaveAwardDefs persists edited award definitions.
|
// SaveAwardDefs persists edited award definitions.
|
||||||
func (a *App) SaveAwardDefs(defs []award.Def) error {
|
func (a *App) SaveAwardDefs(defs []award.Def) error {
|
||||||
if a.settings == nil {
|
if a.settings == nil {
|
||||||
return fmt.Errorf("db not initialized")
|
return fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
|
markUserEdited(defs, a.awardDefs())
|
||||||
b, err := json.Marshal(defs)
|
b, err := json.Marshal(defs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2348,10 +2474,25 @@ func (a *App) mirrorAwardsToFolder(defs []award.Def) {
|
|||||||
|
|
||||||
// ResetAwardDefs restores the built-in defaults.
|
// ResetAwardDefs restores the built-in defaults.
|
||||||
func (a *App) ResetAwardDefs() ([]award.Def, error) {
|
func (a *App) ResetAwardDefs() ([]award.Def, error) {
|
||||||
|
if a.settings == nil {
|
||||||
|
return nil, fmt.Errorf("db not initialized")
|
||||||
|
}
|
||||||
d := award.Defaults()
|
d := award.Defaults()
|
||||||
if err := a.SaveAwardDefs(d); err != nil {
|
// Written directly, NOT through SaveAwardDefs: that path marks whatever it
|
||||||
|
// saves as user-edited, and a reset means the exact opposite — the operator is
|
||||||
|
// handing the awards back to the catalog, flags cleared, so future updates
|
||||||
|
// apply again. Their reference lists go back to the shipped ones too.
|
||||||
|
b, err := json.Marshal(d)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := a.settings.SetGlobal(a.ctx, keyAwardDefs, string(b)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, def := range d {
|
||||||
|
a.reseedRefsFromCatalog(def.Code)
|
||||||
|
}
|
||||||
|
go a.mirrorAwardsToFolder(d)
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3401,6 +3542,7 @@ func (a *App) SaveAwardReference(code string, ref awardref.Ref) error {
|
|||||||
if err := a.awardRefs.Upsert(a.ctx, code, ref); err != nil {
|
if err := a.awardRefs.Upsert(a.ctx, code, ref); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
a.markAwardEdited(code)
|
||||||
a.mirrorAwards()
|
a.mirrorAwards()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -3413,10 +3555,36 @@ func (a *App) DeleteAwardReference(code, refCode string) error {
|
|||||||
if err := a.awardRefs.Delete(a.ctx, code, refCode); err != nil {
|
if err := a.awardRefs.Delete(a.ctx, code, refCode); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
a.markAwardEdited(code)
|
||||||
a.mirrorAwards()
|
a.mirrorAwards()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markAwardEdited records that the operator has hand-modified an award's
|
||||||
|
// REFERENCE list, so a catalog update never overwrites it. Only the manual paths
|
||||||
|
// call this: the online refresh of POTA/SOTA/WWFF replaces the list too, but that
|
||||||
|
// is a data sync, not a customisation, and treating it as one would freeze those
|
||||||
|
// awards out of every future fix.
|
||||||
|
func (a *App) markAwardEdited(code string) {
|
||||||
|
if a.settings == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defs := a.awardDefs()
|
||||||
|
touched := false
|
||||||
|
for i := range defs {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(defs[i].Code), strings.TrimSpace(code)) && !defs[i].UserEdited {
|
||||||
|
defs[i].UserEdited = true
|
||||||
|
touched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !touched {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b, err := json.Marshal(defs); err == nil {
|
||||||
|
a.setSettingGlobal(keyAwardDefs, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ReplaceAwardReferences atomically replaces an award's whole reference list
|
// ReplaceAwardReferences atomically replaces an award's whole reference list
|
||||||
// (used by paste / CSV import and presets). Returns the new count.
|
// (used by paste / CSV import and presets). Returns the new count.
|
||||||
func (a *App) ReplaceAwardReferences(code string, refs []awardref.Ref) (int, error) {
|
func (a *App) ReplaceAwardReferences(code string, refs []awardref.Ref) (int, error) {
|
||||||
@@ -3430,6 +3598,7 @@ func (a *App) ReplaceAwardReferences(code string, refs []awardref.Ref) (int, err
|
|||||||
if a.settings != nil {
|
if a.settings != nil {
|
||||||
a.setSetting(keyAwardRefsUpdated+strings.ToUpper(code), time.Now().Format("2006-01-02 15:04"))
|
a.setSetting(keyAwardRefsUpdated+strings.ToUpper(code), time.Now().Format("2006-01-02 15:04"))
|
||||||
}
|
}
|
||||||
|
a.markAwardEdited(code)
|
||||||
a.mirrorAwards()
|
a.mirrorAwards()
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hamlog/internal/award"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The catalog is the channel through which a shipped award is both DELIVERED and
|
||||||
|
// CORRECTED. These tests pin the three rules that make that safe:
|
||||||
|
// - a new catalog award reaches an operator who already has awards stored;
|
||||||
|
// - a fixed one (higher Version) replaces the stored copy;
|
||||||
|
// - unless the operator has edited it, in which case their work wins.
|
||||||
|
|
||||||
|
func catalogDef(t *testing.T, code string) award.Def {
|
||||||
|
t.Helper()
|
||||||
|
for _, d := range award.Defaults() {
|
||||||
|
if d.Code == code {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("%s is not in the catalog", code)
|
||||||
|
return award.Def{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findDef(defs []award.Def, code string) (award.Def, bool) {
|
||||||
|
for _, d := range defs {
|
||||||
|
if d.Code == code {
|
||||||
|
return d, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return award.Def{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCatalogAddsMissingAward(t *testing.T) {
|
||||||
|
stored := []award.Def{{Code: "MYOWN", Name: "Mine", Valid: true}}
|
||||||
|
got, updated, changed := mergeCatalog(stored)
|
||||||
|
if !changed {
|
||||||
|
t.Fatal("merge reported no change, but every catalog award was missing")
|
||||||
|
}
|
||||||
|
if len(updated) != 0 {
|
||||||
|
t.Errorf("updated = %v, want none: an ADDED award is not an UPDATED one", updated)
|
||||||
|
}
|
||||||
|
if _, ok := findDef(got, "FFMA"); !ok {
|
||||||
|
t.Error("FFMA was not added — a newly shipped award never reaches an existing install")
|
||||||
|
}
|
||||||
|
if _, ok := findDef(got, "MYOWN"); !ok {
|
||||||
|
t.Error("the operator's own award was dropped by the merge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCatalogUpdatesOlderVersion(t *testing.T) {
|
||||||
|
// The stored copy is an older revision of a shipped award, with a broken rule.
|
||||||
|
old := catalogDef(t, "FFMA")
|
||||||
|
old.Version = catalogDef(t, "FFMA").Version - 1
|
||||||
|
old.Field = "wrong"
|
||||||
|
old.Valid = false // the operator disabled it — a preference, not a definition
|
||||||
|
|
||||||
|
got, updated, changed := mergeCatalog([]award.Def{old})
|
||||||
|
if !changed || len(updated) == 0 {
|
||||||
|
t.Fatal("a higher catalog version did not replace the stored definition — a shipped award could never be FIXED")
|
||||||
|
}
|
||||||
|
d, _ := findDef(got, "FFMA")
|
||||||
|
if d.Field != "grid4" {
|
||||||
|
t.Errorf("FFMA field = %q, want the catalog's %q", d.Field, "grid4")
|
||||||
|
}
|
||||||
|
if d.Valid {
|
||||||
|
t.Error("the update re-enabled an award the operator had switched off; that is their choice to make, not ours")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeCatalogSkipsUserEdited(t *testing.T) {
|
||||||
|
old := catalogDef(t, "FFMA")
|
||||||
|
old.Version = catalogDef(t, "FFMA").Version - 1
|
||||||
|
old.Field = "mine"
|
||||||
|
old.UserEdited = true
|
||||||
|
|
||||||
|
got, updated, _ := mergeCatalog([]award.Def{old})
|
||||||
|
if len(updated) != 0 {
|
||||||
|
t.Fatalf("updated = %v: an award the operator has edited must never be overwritten", updated)
|
||||||
|
}
|
||||||
|
if d, _ := findDef(got, "FFMA"); d.Field != "mine" {
|
||||||
|
t.Errorf("field = %q, want the operator's %q", d.Field, "mine")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkUserEditedOnlyOnRealChange(t *testing.T) {
|
||||||
|
prev := []award.Def{
|
||||||
|
{Code: "A", Name: "A", Field: "state", Valid: true, Version: 2},
|
||||||
|
{Code: "B", Name: "B", Field: "cqz", Valid: true, Version: 2},
|
||||||
|
}
|
||||||
|
next := []award.Def{
|
||||||
|
{Code: "A", Name: "A", Field: "state", Valid: true}, // untouched
|
||||||
|
{Code: "B", Name: "B", Field: "county", Valid: true}, // changed
|
||||||
|
{Code: "C", Name: "C", Field: "note", Valid: true}, // brand new
|
||||||
|
}
|
||||||
|
markUserEdited(next, prev)
|
||||||
|
|
||||||
|
if next[0].UserEdited {
|
||||||
|
t.Error("A was flagged as edited although nothing about it changed — every save would freeze every award out of future updates")
|
||||||
|
}
|
||||||
|
if !next[1].UserEdited {
|
||||||
|
t.Error("B changed field and was not flagged; a catalog update would overwrite the operator's work")
|
||||||
|
}
|
||||||
|
if next[2].UserEdited {
|
||||||
|
t.Error("C is a brand-new award; there is no shipped version to protect it from")
|
||||||
|
}
|
||||||
|
// A save must not pretend to be a new shipped revision.
|
||||||
|
if next[0].Version != 2 || next[1].Version != 2 {
|
||||||
|
t.Errorf("versions = %d/%d, want both 2: saving is not shipping", next[0].Version, next[1].Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,6 +274,8 @@ export namespace award {
|
|||||||
export_credit_granted?: boolean;
|
export_credit_granted?: boolean;
|
||||||
total: number;
|
total: number;
|
||||||
builtin: boolean;
|
builtin: boolean;
|
||||||
|
version?: number;
|
||||||
|
user_edited?: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Def(source);
|
return new Def(source);
|
||||||
@@ -314,6 +316,8 @@ export namespace award {
|
|||||||
this.export_credit_granted = source["export_credit_granted"];
|
this.export_credit_granted = source["export_credit_granted"];
|
||||||
this.total = source["total"];
|
this.total = source["total"];
|
||||||
this.builtin = source["builtin"];
|
this.builtin = source["builtin"];
|
||||||
|
this.version = source["version"];
|
||||||
|
this.user_edited = source["user_edited"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|||||||
@@ -99,6 +99,35 @@ type Def struct {
|
|||||||
|
|
||||||
Total int `json:"total"` // known denominator (0 = unknown / derive from list)
|
Total int `json:"total"` // known denominator (0 = unknown / derive from list)
|
||||||
Builtin bool `json:"builtin"` // shipped default (informational)
|
Builtin bool `json:"builtin"` // shipped default (informational)
|
||||||
|
|
||||||
|
// --- Catalog updates ---
|
||||||
|
// Version is the revision of a SHIPPED award. Bump it in the catalog JSON when
|
||||||
|
// you fix a definition (a better OR chain, a corrected reference list) and want
|
||||||
|
// that fix to reach operators who already run the award: on startup, a catalog
|
||||||
|
// award whose Version is higher than the stored one REPLACES it, definition and
|
||||||
|
// references. Awards created by the operator have no version and are never touched.
|
||||||
|
Version int `json:"version,omitempty"`
|
||||||
|
// UserEdited marks an award the operator has changed. A catalog update then
|
||||||
|
// SKIPS it: their work outranks ours. Set the moment the award (or its reference
|
||||||
|
// list) is saved to something other than what the catalog ships.
|
||||||
|
UserEdited bool `json:"user_edited,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SameContent reports whether two definitions describe the same award — ignoring
|
||||||
|
// the bookkeeping fields (version, the user-edited flag, the derived builtin bit),
|
||||||
|
// which say where a definition came from, not what it does. Used to decide whether
|
||||||
|
// a save actually changed anything.
|
||||||
|
func (d Def) SameContent(o Def) bool {
|
||||||
|
a, b := d, o
|
||||||
|
a.Version, b.Version = 0, 0
|
||||||
|
a.UserEdited, b.UserEdited = false, false
|
||||||
|
a.Builtin, b.Builtin = false, false
|
||||||
|
ja, err1 := json.Marshal(a)
|
||||||
|
jb, err2 := json.Marshal(b)
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return string(ja) == string(jb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrRule is one additional search OR'd with the award's primary matching rule.
|
// OrRule is one additional search OR'd with the award's primary matching rule.
|
||||||
|
|||||||
Reference in New Issue
Block a user