is an advert the
+// server injects into its own XML; the parser has to shrug it off.
+const hamqthEU1EU = `
+
+
+
+eu1eu
+Igor
+Minsk-5
+Belarus
+27
+29
+16
+KO33SV
+Igor Vladimirovich Getman
+A/ya 143
+Minsk-5
+220005
+Belarus
+27
+WAARB-LE
+?
+?
+?
+Y
+eu1eu@mail.ru
+53.88999938964844
+27.59000015258789
+EU
+-2
+https://www.hamqth.com/images/default/ts-930_qith_old_radios.jpg
+
+`
+
+func TestHamQTHParsesEveryUsefulField(t *testing.T) {
+ var root hamqthRoot
+ if err := xml.Unmarshal([]byte(hamqthEU1EU), &root); err != nil {
+ t.Fatalf("parse: %v", err)
+ }
+ s := root.Search
+
+ for _, c := range []struct{ name, got, want string }{
+ {"callsign", s.Callsign, "eu1eu"},
+ {"email", s.Email, "eu1eu@mail.ru"},
+ {"grid", s.Grid, "KO33SV"},
+ {"qth", s.QTH, "Minsk-5"},
+ {"street", s.AdrStreet1, "A/ya 143"},
+ {"city", s.AdrCity, "Minsk-5"},
+ {"country", s.AdrCountry, "Belarus"},
+ {"continent", s.Continent, "EU"},
+ {"dxcc", s.DXCC, "27"},
+ {"cq", s.CQ, "16"},
+ {"itu", s.ITU, "29"},
+ } {
+ if c.got != c.want {
+ t.Errorf("%s = %q, want %q", c.name, c.got, c.want)
+ }
+ }
+}
+
+// The fields the parser was blind to. Each is data OpsLog has somewhere to put.
+func TestHamQTHParsesTheFieldsThatWereMissing(t *testing.T) {
+ var root hamqthRoot
+ if err := xml.Unmarshal([]byte(hamqthEU1EU), &root); err != nil {
+ t.Fatalf("parse: %v", err)
+ }
+ s := root.Search
+
+ // The postal name, the fallback when a record carries no nick and no name.
+ if s.AdrName != "Igor Vladimirovich Getman" {
+ t.Errorf("adr_name = %q", s.AdrName)
+ }
+ if s.LastName != "" {
+ t.Errorf("this record has no
; got %q", s.LastName)
+ }
+ // The profile picture. Result.ImageURL existed and said "QRZ only" — HamQTH
+ // sends one too, under a different element name.
+ if s.Picture == "" {
+ t.Error("picture not parsed")
+ }
+ if s.AdrZip != "220005" {
+ t.Errorf("adr_zip = %q", s.AdrZip)
+ }
+}
+
+// End to end: what the provider hands back must carry the email, the full name
+// and the picture.
+func TestHamQTHResultCarriesTheLot(t *testing.T) {
+ r, err := parseHamQTHSearch([]byte(hamqthEU1EU))
+ if err != nil {
+ t.Fatalf("parse: %v", err)
+ }
+ if r.Email != "eu1eu@mail.ru" {
+ t.Errorf("Email = %q — this is what was reported missing", r.Email)
+ }
+ // The nick, not the postal name: a log wants the name the operator goes by
+ // on the air. adr_name is only the fallback when there is nothing else.
+ if r.Name != "Igor" {
+ t.Errorf("Name = %q, want the on-air nick", r.Name)
+ }
+ if r.ImageURL == "" {
+ t.Error("ImageURL empty — HamQTH sent a picture")
+ }
+ if r.Grid != "KO33SV" {
+ t.Errorf("Grid = %q", r.Grid)
+ }
+ if r.Lat < 53.8 || r.Lat > 53.9 {
+ t.Errorf("Lat = %v", r.Lat)
+ }
+ if r.DXCC != 27 || r.CQZ != 16 || r.ITUZ != 29 {
+ t.Errorf("dxcc/cq/itu = %d/%d/%d", r.DXCC, r.CQZ, r.ITUZ)
+ }
+ if r.Address != "A/ya 143" {
+ t.Errorf("Address = %q", r.Address)
+ }
+}
diff --git a/internal/lookup/lookup.go b/internal/lookup/lookup.go
index fac3817..0b88229 100644
--- a/internal/lookup/lookup.go
+++ b/internal/lookup/lookup.go
@@ -37,7 +37,13 @@ type Result struct {
Continent string `json:"cont,omitempty"`
Email string `json:"email,omitempty"`
QSLVia string `json:"qsl_via,omitempty"`
- ImageURL string `json:"image_url,omitempty"` // profile picture URL (QRZ only for now)
+ // Web is the operator's own site. The QSO table has had a `web` column all
+ // along and nothing ever filled it, because no provider mapping read the
+ // field.
+ Web string `json:"web,omitempty"`
+ // Zip is the postal code. HamQTH and QRZ both send one.
+ Zip string `json:"zip,omitempty"`
+ ImageURL string `json:"image_url,omitempty"` // profile picture URL
Source string `json:"source"` // "qrz", "hamqth", or "cache"
FetchedAt time.Time `json:"fetched_at"`
}
@@ -446,12 +452,13 @@ func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
row := c.db.QueryRowContext(ctx, `
SELECT callsign, name, qth, address, state, cnty, country, grid,
lat, lon, dxcc, cqz, ituz, cont, email, qsl_via, image_url,
- source, fetched_at
+ web, zip, source, fetched_at
FROM callsign_cache WHERE callsign = ?`, callsign)
var (
r Result
name, qth, addr, state, cnty sql.NullString
country, grid, cont, email, qslVia, image sql.NullString
+ web, zip sql.NullString
src string
dxcc, cqz, ituz sql.NullInt64
lat, lon sql.NullFloat64
@@ -459,7 +466,7 @@ func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
)
if err := row.Scan(&r.Callsign, &name, &qth, &addr, &state, &cnty,
&country, &grid, &lat, &lon,
- &dxcc, &cqz, &ituz, &cont, &email, &qslVia, &image,
+ &dxcc, &cqz, &ituz, &cont, &email, &qslVia, &image, &web, &zip,
&src, &fetched); err != nil {
return Result{}, false
}
@@ -481,6 +488,8 @@ func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
r.Lon = lon.Float64
r.Continent = cont.String
r.Email = email.String
+ r.Web = web.String
+ r.Zip = zip.String
r.QSLVia = qslVia.String
r.ImageURL = image.String
r.DXCC = int(dxcc.Int64)
@@ -497,7 +506,7 @@ func (c *Cache) Put(ctx context.Context, r Result) error {
updateCols := []string{
"name", "qth", "address", "state", "cnty",
"country", "grid", "lat", "lon",
- "dxcc", "cqz", "ituz", "cont", "email", "qsl_via", "image_url",
+ "dxcc", "cqz", "ituz", "cont", "email", "qsl_via", "image_url", "web", "zip",
"source", "fetched_at",
}
// The lookup cache always lives in the local SQLite database, so SQLite
@@ -510,8 +519,8 @@ func (c *Cache) Put(ctx context.Context, r Result) error {
INSERT INTO callsign_cache(callsign, name, qth, address, state, cnty,
country, grid, lat, lon,
dxcc, cqz, ituz, cont, email, qsl_via, image_url,
- source, fetched_at)
- VALUES(?,?,?,?,?,?, ?,?,?,?, ?,?,?,?,?,?,?, ?,?)
+ web, zip, source, fetched_at)
+ VALUES(?,?,?,?,?,?, ?,?,?,?, ?,?,?,?,?,?,?, ?,?, ?,?)
ON CONFLICT(callsign) DO UPDATE SET ` + strings.Join(sets, ", ")
_, err := c.db.ExecContext(ctx, q,
r.Callsign, nullable(r.Name), nullable(r.QTH), nullable(r.Address),
@@ -520,7 +529,7 @@ func (c *Cache) Put(ctx context.Context, r Result) error {
nullableFloat(r.Lat), nullableFloat(r.Lon),
nullableInt(r.DXCC), nullableInt(r.CQZ), nullableInt(r.ITUZ),
nullable(r.Continent), nullable(r.Email), nullable(r.QSLVia),
- nullable(r.ImageURL),
+ nullable(r.ImageURL), nullable(r.Web), nullable(r.Zip),
r.Source, db.NowISO(),
)
return err