feat(awards): implement the QRZ.com and Custom confirmation sources
The award editor offered five confirmation sources and Def's own doc comment named five, but confirmed() had cases for three. "qrzcom" and "custom" fell through the switch, so ticking either marked nothing as confirmed — the exact failure the GrantCodes comment in this struct warns about: a checkbox that does nothing is worse than no checkbox, because it is trusted. QRZ.com reads qrzcom_qso_download_status, not the upload one: uploading a QSO is us telling QRZ about it, which confirms nothing. Custom names a field. Rather than a checkbox per external source, the Def gains ConfirmField + ConfirmValue: any QSO field or ADIF extras key, and optionally the comma-separated values that count. That one shape covers the three cases asked for — the OpsLog card marker (APP_OPSLOG_QSL_RCVD), an arbitrary ADIF tag, and a tag stamped by an imported club list — because all three end up as a field on the QSO. An empty ConfirmValue means any non-empty content confirms: the OpsLog marker stores the date the card arrived, not a Y/N flag. A custom source naming NO field confirms nothing, deliberately — the opposite default would silently mark a whole logbook confirmed.
This commit is contained in:
@@ -4253,8 +4253,8 @@ func (a *App) GetAwardStats(code string) (AwardStatsResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bi, hasBand := bandIdx[strings.ToLower(strings.TrimSpace(q.Band))]
|
bi, hasBand := bandIdx[strings.ToLower(strings.TrimSpace(q.Band))]
|
||||||
isConf := award.Confirmed(q, def.Confirm)
|
isConf := award.Confirmed(q, *def, def.Confirm)
|
||||||
isVal := award.Confirmed(q, def.Validate)
|
isVal := award.Confirmed(q, *def, def.Validate)
|
||||||
cat := strings.ToUpper(award.EmissionOf(q.Mode))
|
cat := strings.ToUpper(award.EmissionOf(q.Mode))
|
||||||
|
|
||||||
record := func(c string) {
|
record := func(c string) {
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
"Worked before: an operator's portable callsigns now count as the same station. Typing RK3DWA finds the RK3DWA/3, /P and /MM contacts too, and the other way round — before, the history only appeared if you typed the exact form. Can be turned off in Settings → General; contest dupe checking is unaffected and stays exact.",
|
"Worked before: an operator's portable callsigns now count as the same station. Typing RK3DWA finds the RK3DWA/3, /P and /MM contacts too, and the other way round — before, the history only appeared if you typed the exact form. Can be turned off in Settings → General; contest dupe checking is unaffected and stays exact.",
|
||||||
"QSO editor: the OpsLog card now appears with the other confirmations. It has its own Sent / Received row in the QSL Info table, next to QSL, LoTW, eQSL and the rest, and the \"QSL received\" tick moved there from Contact's details — with its PSE QSL / TNX indicator, which is unchanged. Sent stays read-only: OpsLog stamps it when the card actually goes out.",
|
"QSO editor: the OpsLog card now appears with the other confirmations. It has its own Sent / Received row in the QSL Info table, next to QSL, LoTW, eQSL and the rest, and the \"QSL received\" tick moved there from Contact's details — with its PSE QSL / TNX indicator, which is unchanged. Sent stays read-only: OpsLog stamps it when the card actually goes out.",
|
||||||
"QSL card: the {qso.pse_tnx} stamp now prints \"TNX QSL\" instead of just \"TNX\", to read as the counterpart of \"PSE QSL\".",
|
"QSL card: the {qso.pse_tnx} stamp now prints \"TNX QSL\" instead of just \"TNX\", to read as the counterpart of \"PSE QSL\".",
|
||||||
|
"Awards: the QRZ.com and Custom confirmation sources actually work now. Both were offered in the award editor but neither was implemented, so ticking one marked no QSO as confirmed — silently. QRZ.com reads the download status (them confirming back, not our upload). Custom reads any QSO field or ADIF tag you name, with an optional list of values that count: point it at APP_OPSLOG_QSL_RCVD for a card received through OpsLog, or at a tag stamped by a club list you imported. Leave the value empty and any non-empty content confirms. A Custom source naming no field confirms nothing.",
|
||||||
"Window: closing OpsLog while it is minimised no longer loses its size and position. Windows parks a minimised window at -32000,-32000 with a stub size, and that was what got saved — so the next launch found geometry no screen could hold, discarded it, and reopened at the default place. The stored geometry is now kept instead; it repairs itself the first time you close a window that is on screen."
|
"Window: closing OpsLog while it is minimised no longer loses its size and position. Windows parks a minimised window at -32000,-32000 with a stub size, and that was what got saved — so the next launch found geometry no screen could hold, discarded it, and reopened at the default place. The stored geometry is now kept instead; it repairs itself the first time you close a window that is on screen."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Déjà contacté : les indicatifs portables d'un opérateur comptent désormais comme la même station. Taper RK3DWA retrouve aussi les QSO en RK3DWA/3, /P et /MM, et inversement — avant, l'historique n'apparaissait que si tu tapais la forme exacte. Désactivable dans Réglages → Général ; le contrôle de doublon en concours n'est pas touché et reste strict.",
|
"Déjà contacté : les indicatifs portables d'un opérateur comptent désormais comme la même station. Taper RK3DWA retrouve aussi les QSO en RK3DWA/3, /P et /MM, et inversement — avant, l'historique n'apparaissait que si tu tapais la forme exacte. Désactivable dans Réglages → Général ; le contrôle de doublon en concours n'est pas touché et reste strict.",
|
||||||
"Éditeur de QSO : la carte OpsLog rejoint les autres confirmations. Elle a sa propre ligne Envoyée / Reçue dans le tableau de l'onglet QSL Info, à côté de QSL, LoTW, eQSL et les autres, et la case « QSL reçue » y a été déplacée depuis Détails du contact — avec son indicateur PSE QSL / TNX, inchangé. « Envoyée » reste en lecture seule : OpsLog l'inscrit quand la carte part réellement.",
|
"Éditeur de QSO : la carte OpsLog rejoint les autres confirmations. Elle a sa propre ligne Envoyée / Reçue dans le tableau de l'onglet QSL Info, à côté de QSL, LoTW, eQSL et les autres, et la case « QSL reçue » y a été déplacée depuis Détails du contact — avec son indicateur PSE QSL / TNX, inchangé. « Envoyée » reste en lecture seule : OpsLog l'inscrit quand la carte part réellement.",
|
||||||
"Carte QSL : le tampon {qso.pse_tnx} imprime désormais « TNX QSL » et non plus « TNX » seul, pour répondre à « PSE QSL ».",
|
"Carte QSL : le tampon {qso.pse_tnx} imprime désormais « TNX QSL » et non plus « TNX » seul, pour répondre à « PSE QSL ».",
|
||||||
|
"Diplômes : les sources de confirmation QRZ.com et Custom fonctionnent enfin. Les deux étaient proposées dans l'éditeur de diplômes sans être implémentées : cocher l'une ou l'autre ne confirmait aucun QSO, en silence. QRZ.com lit le statut de téléchargement (leur confirmation en retour, pas notre envoi). Custom lit n'importe quel champ de QSO ou balise ADIF que tu désignes, avec une liste facultative de valeurs qui comptent : pointe-la sur APP_OPSLOG_QSL_RCVD pour une carte reçue via OpsLog, ou sur une balise inscrite à l'import d'une liste de club. Laisse la valeur vide et tout contenu non vide confirme. Une source Custom sans champ ne confirme rien.",
|
||||||
"Fenêtre : fermer OpsLog alors qu'il est réduit ne fait plus perdre sa taille et sa position. Windows range une fenêtre réduite en -32000,-32000 avec une taille factice, et c'est cela qui était enregistré — au lancement suivant, OpsLog trouvait une géométrie qu'aucun écran ne peut contenir, la rejetait et rouvrait à l'emplacement par défaut. La géométrie déjà enregistrée est désormais conservée ; cela se répare tout seul à la première fermeture d'une fenêtre visible à l'écran."
|
"Fenêtre : fermer OpsLog alors qu'il est réduit ne fait plus perdre sa taille et sa position. Windows range une fenêtre réduite en -32000,-32000 avec une taille factice, et c'est cela qui était enregistré — au lancement suivant, OpsLog trouvait une géométrie qu'aucun écran ne peut contenir, la rejetait et rouvrait à l'emplacement par défaut. La géométrie déjà enregistrée est désormais conservée ; cela se répare tout seul à la première fermeture d'une fenêtre visible à l'écran."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export type AwardDef = {
|
|||||||
or_rules?: AwardOrRule[];
|
or_rules?: AwardOrRule[];
|
||||||
dxcc_filter: number[] | null; valid_bands?: string[]; valid_modes?: string[]; emission?: string[];
|
dxcc_filter: number[] | null; valid_bands?: string[]; valid_modes?: string[]; emission?: string[];
|
||||||
confirm: string[] | null; validate?: string[] | null; grant_codes?: string; export_credit_granted?: boolean;
|
confirm: string[] | null; validate?: string[] | null; grant_codes?: string; export_credit_granted?: boolean;
|
||||||
|
// The "custom" confirmation source: the field it reads and, optionally, the
|
||||||
|
// value(s) that count. Empty value = any non-empty value confirms.
|
||||||
|
confirm_field?: string; confirm_value?: string;
|
||||||
total: number; builtin?: boolean; version?: number;
|
total: number; builtin?: boolean; version?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -622,6 +625,30 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* "Custom" is only meaningful once it names a field, so the
|
||||||
|
inputs appear as soon as it is ticked in either column —
|
||||||
|
and a custom source with no field confirms nothing. */}
|
||||||
|
{((cur.confirm ?? []).includes('custom') || (cur.validate ?? []).includes('custom')) && (
|
||||||
|
<div className="space-y-2 border-t border-border/60 pt-3">
|
||||||
|
<Label className="text-xs font-semibold">{t('awed.customLabel')}</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[11px] text-muted-foreground">{t('awed.customField')}</Label>
|
||||||
|
<Input className="h-8 font-mono text-xs" placeholder="APP_OPSLOG_QSL_RCVD"
|
||||||
|
value={(cur as any).confirm_field ?? ''}
|
||||||
|
onChange={(e) => patch({ confirm_field: e.target.value.trim().toUpperCase() } as any)} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[11px] text-muted-foreground">{t('awed.customValue')}</Label>
|
||||||
|
<Input className="h-8 font-mono text-xs" placeholder="Y,V"
|
||||||
|
value={(cur as any).confirm_value ?? ''}
|
||||||
|
onChange={(e) => patch({ confirm_value: e.target.value } as any)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-relaxed">{t('awed.customHint')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* "Grant codes" and "export credit_granted" used to live here. No
|
{/* "Grant codes" and "export credit_granted" used to live here. No
|
||||||
ADIF export has ever written CREDIT_GRANTED, so both controls
|
ADIF export has ever written CREDIT_GRANTED, so both controls
|
||||||
did nothing at all. The stored values are kept (see award.Def);
|
did nothing at all. The stored values are kept (see award.Def);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -325,6 +325,8 @@ export namespace award {
|
|||||||
emission?: string[];
|
emission?: string[];
|
||||||
confirm: string[];
|
confirm: string[];
|
||||||
validate?: string[];
|
validate?: string[];
|
||||||
|
confirm_field?: string;
|
||||||
|
confirm_value?: string;
|
||||||
grant_codes?: string;
|
grant_codes?: string;
|
||||||
export_credit_granted?: boolean;
|
export_credit_granted?: boolean;
|
||||||
total: number;
|
total: number;
|
||||||
@@ -367,6 +369,8 @@ export namespace award {
|
|||||||
this.emission = source["emission"];
|
this.emission = source["emission"];
|
||||||
this.confirm = source["confirm"];
|
this.confirm = source["confirm"];
|
||||||
this.validate = source["validate"];
|
this.validate = source["validate"];
|
||||||
|
this.confirm_field = source["confirm_field"];
|
||||||
|
this.confirm_value = source["confirm_value"];
|
||||||
this.grant_codes = source["grant_codes"];
|
this.grant_codes = source["grant_codes"];
|
||||||
this.export_credit_granted = source["export_credit_granted"];
|
this.export_credit_granted = source["export_credit_granted"];
|
||||||
this.total = source["total"];
|
this.total = source["total"];
|
||||||
|
|||||||
+73
-5
@@ -111,6 +111,18 @@ type Def struct {
|
|||||||
// --- Confirmation ---
|
// --- Confirmation ---
|
||||||
Confirm []string `json:"confirm"` // worked-confirmed: lotw|qsl|eqsl|qrzcom|custom
|
Confirm []string `json:"confirm"` // worked-confirmed: lotw|qsl|eqsl|qrzcom|custom
|
||||||
Validate []string `json:"validate,omitempty"` // validated/granted sources
|
Validate []string `json:"validate,omitempty"` // validated/granted sources
|
||||||
|
// The "custom" source, for confirmations OpsLog has no dedicated column for:
|
||||||
|
// ConfirmField names a QSO field or an ADIF extras key, ConfirmValue the
|
||||||
|
// value(s) that count (comma-separated, case-insensitive).
|
||||||
|
//
|
||||||
|
// An EMPTY ConfirmValue means "any non-empty value confirms" — which is what
|
||||||
|
// the OpsLog card marker needs: APP_OPSLOG_QSL_RCVD stores the timestamp of
|
||||||
|
// the day the card arrived, not a Y/N flag. The same shape serves a key
|
||||||
|
// stamped by an outside source (a club's CSV imported into an extras field),
|
||||||
|
// so one mechanism covers every "confirmed somewhere else" case instead of a
|
||||||
|
// new checkbox per site.
|
||||||
|
ConfirmField string `json:"confirm_field,omitempty"`
|
||||||
|
ConfirmValue string `json:"confirm_value,omitempty"`
|
||||||
// NOT IMPLEMENTED. Kept so the values operators already typed are not lost, but
|
// NOT IMPLEMENTED. Kept so the values operators already typed are not lost, but
|
||||||
// nothing reads them: no ADIF export has ever written CREDIT_GRANTED. Their
|
// nothing reads them: no ADIF export has ever written CREDIT_GRANTED. Their
|
||||||
// controls have been removed from the editor — a checkbox that quietly does
|
// controls have been removed from the editor — a checkbox that quietly does
|
||||||
@@ -523,8 +535,8 @@ func Compute(defs []Def, qsos []qso.QSO, refMetas map[string][]RefMeta, nameOf N
|
|||||||
}
|
}
|
||||||
band := strings.ToLower(strings.TrimSpace(q.Band))
|
band := strings.ToLower(strings.TrimSpace(q.Band))
|
||||||
modeClass := ModeClass(q.Mode)
|
modeClass := ModeClass(q.Mode)
|
||||||
isConf := confirmed(q, d.Confirm)
|
isConf := confirmed(q, d.Confirm, d)
|
||||||
isVal := confirmed(q, d.Validate)
|
isVal := confirmed(q, d.Validate, d)
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
a := agg[i][ref]
|
a := agg[i][ref]
|
||||||
if a == nil {
|
if a == nil {
|
||||||
@@ -685,8 +697,9 @@ func manualRefs(q *qso.QSO, code string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Confirmed reports whether a QSO satisfies any of the given confirmation
|
// Confirmed reports whether a QSO satisfies any of the given confirmation
|
||||||
// sources (lotw|qsl|eqsl). Exported for the statistics view.
|
// sources (lotw|qsl|eqsl|qrzcom|custom). Exported for the statistics view.
|
||||||
func Confirmed(q *qso.QSO, sources []string) bool { return confirmed(q, sources) }
|
// The Def is needed for the "custom" source, which reads the field IT names.
|
||||||
|
func Confirmed(q *qso.QSO, d Def, sources []string) bool { return confirmed(q, sources, &d) }
|
||||||
|
|
||||||
// InScope reports whether a QSO falls within an award's scope (DXCC entity,
|
// InScope reports whether a QSO falls within an award's scope (DXCC entity,
|
||||||
// bands, modes, emission, dates) — independent of whether a reference was
|
// bands, modes, emission, dates) — independent of whether a reference was
|
||||||
@@ -1374,7 +1387,7 @@ func dxccAllowed(dxcc *int, filter []int) bool {
|
|||||||
|
|
||||||
// confirmed reports whether the QSO satisfies any accepted confirmation source.
|
// confirmed reports whether the QSO satisfies any accepted confirmation source.
|
||||||
// ADIF *_QSL_RCVD values Y (confirmed) and V (verified) both count.
|
// ADIF *_QSL_RCVD values Y (confirmed) and V (verified) both count.
|
||||||
func confirmed(q *qso.QSO, sources []string) bool {
|
func confirmed(q *qso.QSO, sources []string, d *Def) bool {
|
||||||
for _, s := range sources {
|
for _, s := range sources {
|
||||||
switch s {
|
switch s {
|
||||||
case "lotw":
|
case "lotw":
|
||||||
@@ -1389,11 +1402,66 @@ func confirmed(q *qso.QSO, sources []string) bool {
|
|||||||
if isYes(q.EQSLRcvd) {
|
if isYes(q.EQSLRcvd) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
case "qrzcom":
|
||||||
|
// The DOWNLOAD status, not the upload one: uploading a QSO to QRZ is
|
||||||
|
// us telling them, not them confirming it back.
|
||||||
|
if isYes(q.QRZComDownloadStatus) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case "custom":
|
||||||
|
if customConfirmed(q, d) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// customConfirmed answers the operator-defined confirmation source: the field
|
||||||
|
// named by ConfirmField, optionally required to hold one of ConfirmValue's
|
||||||
|
// comma-separated values.
|
||||||
|
//
|
||||||
|
// A source that names no field confirms NOTHING. That is deliberate: ticking
|
||||||
|
// "custom" without saying what it means used to mark every QSO as unconfirmed
|
||||||
|
// anyway, and silently marking them all CONFIRMED instead would be far worse.
|
||||||
|
func customConfirmed(q *qso.QSO, d *Def) bool {
|
||||||
|
if d == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
field := strings.TrimSpace(d.ConfirmField)
|
||||||
|
if field == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
got := strings.TrimSpace(customFieldValue(q, field))
|
||||||
|
if got == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
want := strings.TrimSpace(d.ConfirmValue)
|
||||||
|
if want == "" {
|
||||||
|
return true // any value at all counts — see ConfirmField's doc
|
||||||
|
}
|
||||||
|
for _, w := range strings.Split(want, ",") {
|
||||||
|
if w = strings.TrimSpace(w); w != "" && strings.EqualFold(w, got) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// customFieldValue resolves the field a custom confirmation names: one of the
|
||||||
|
// engine's known QSO fields first, then the ADIF extras — which is where the
|
||||||
|
// OpsLog card marker (APP_OPSLOG_QSL_RCVD) and anything stamped by an outside
|
||||||
|
// import actually live.
|
||||||
|
func customFieldValue(q *qso.QSO, field string) string {
|
||||||
|
if v := strings.TrimSpace(fieldRaw(field, q)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if q.Extras == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return q.Extras[strings.ToUpper(strings.TrimSpace(field))]
|
||||||
|
}
|
||||||
|
|
||||||
func isYes(v string) bool {
|
func isYes(v string) bool {
|
||||||
switch strings.ToUpper(strings.TrimSpace(v)) {
|
switch strings.ToUpper(strings.TrimSpace(v)) {
|
||||||
case "Y", "V":
|
case "Y", "V":
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package award
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hamlog/internal/qso"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Every source the editor offers must actually do something. "qrzcom" and
|
||||||
|
// "custom" were listed in the UI and in Def's doc comment but had no case in the
|
||||||
|
// switch, so ticking either marked nothing as confirmed — a checkbox that is
|
||||||
|
// trusted and silently inert.
|
||||||
|
func TestConfirmedSources(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
q qso.QSO
|
||||||
|
d Def
|
||||||
|
sources []string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"lotw", qso.QSO{LOTWRcvd: "Y"}, Def{}, []string{"lotw"}, true},
|
||||||
|
{"qsl", qso.QSO{QSLRcvd: "Y"}, Def{}, []string{"qsl"}, true},
|
||||||
|
{"eqsl", qso.QSO{EQSLRcvd: "Y"}, Def{}, []string{"eqsl"}, true},
|
||||||
|
|
||||||
|
// QRZ confirms on the DOWNLOAD status. The upload one is us telling QRZ
|
||||||
|
// about the QSO, which is not a confirmation of anything.
|
||||||
|
{"qrz download", qso.QSO{QRZComDownloadStatus: "Y"}, Def{}, []string{"qrzcom"}, true},
|
||||||
|
{"qrz upload only", qso.QSO{QRZComUploadStatus: "Y"}, Def{}, []string{"qrzcom"}, false},
|
||||||
|
|
||||||
|
// Custom, no value required: any non-empty value counts. This is the
|
||||||
|
// OpsLog card case — the marker holds a timestamp, not a flag.
|
||||||
|
{"custom any value",
|
||||||
|
qso.QSO{Extras: map[string]string{"APP_OPSLOG_QSL_RCVD": "2026-08-09T01:00:00Z"}},
|
||||||
|
Def{ConfirmField: "APP_OPSLOG_QSL_RCVD"}, []string{"custom"}, true},
|
||||||
|
{"custom field empty",
|
||||||
|
qso.QSO{Extras: map[string]string{"APP_OPSLOG_QSL_RCVD": ""}},
|
||||||
|
Def{ConfirmField: "APP_OPSLOG_QSL_RCVD"}, []string{"custom"}, false},
|
||||||
|
|
||||||
|
// Custom with an explicit value list, matched case-insensitively.
|
||||||
|
{"custom value match",
|
||||||
|
qso.QSO{Extras: map[string]string{"APP_CLUB_CONF": "v"}},
|
||||||
|
Def{ConfirmField: "APP_CLUB_CONF", ConfirmValue: "Y,V"}, []string{"custom"}, true},
|
||||||
|
{"custom value mismatch",
|
||||||
|
qso.QSO{Extras: map[string]string{"APP_CLUB_CONF": "N"}},
|
||||||
|
Def{ConfirmField: "APP_CLUB_CONF", ConfirmValue: "Y,V"}, []string{"custom"}, false},
|
||||||
|
|
||||||
|
// A custom source naming no field must confirm NOTHING — never everything.
|
||||||
|
{"custom without a field",
|
||||||
|
qso.QSO{Extras: map[string]string{"APP_OPSLOG_QSL_RCVD": "x"}},
|
||||||
|
Def{}, []string{"custom"}, false},
|
||||||
|
|
||||||
|
// Known QSO fields resolve too, not just extras.
|
||||||
|
{"custom on a known field", qso.QSO{State: "TX"},
|
||||||
|
Def{ConfirmField: "state", ConfirmValue: "TX"}, []string{"custom"}, true},
|
||||||
|
|
||||||
|
// Any source in the list is enough.
|
||||||
|
{"first source misses, second hits", qso.QSO{EQSLRcvd: "Y"}, Def{},
|
||||||
|
[]string{"lotw", "eqsl"}, true},
|
||||||
|
{"no source set", qso.QSO{LOTWRcvd: "Y"}, Def{}, nil, false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := Confirmed(&c.q, c.d, c.sources); got != c.want {
|
||||||
|
t.Errorf("%s: Confirmed = %v, want %v", c.name, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user