diff options
author | Thomas Voss <mail@thomasvoss.com> | 2025-06-09 02:51:27 +0200 |
---|---|---|
committer | Thomas Voss <mail@thomasvoss.com> | 2025-06-09 02:51:27 +0200 |
commit | 52db1d03e5067de4ba77c3a12465149d268eb3d1 (patch) | |
tree | 96286b94957e2546a789cbdcad2677b6f03b0973 | |
parent | 09defec0a015e7ddb118bd453ea2925a5cb9d5dc (diff) |
Begin migration towards SQLite
-rw-r--r-- | GNUmakefile | 5 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | main.go | 5 | ||||
-rw-r--r-- | src/dbx/db.go | 227 | ||||
-rw-r--r-- | src/dbx/mintages.go | 65 | ||||
-rw-r--r-- | src/dbx/sql/000-genesis.sql | 74 | ||||
-rw-r--r-- | src/dbx/users.go | 64 | ||||
-rw-r--r-- | src/http.go | 41 | ||||
-rw-r--r-- | src/i18n.go | 6 | ||||
-rw-r--r-- | src/mintage/mintage.go | 42 | ||||
-rw-r--r-- | src/mintage/parser.go | 168 | ||||
-rw-r--r-- | src/templates.go | 28 | ||||
-rw-r--r-- | src/templates/coins-mintages.html.tmpl | 7 |
14 files changed, 496 insertions, 242 deletions
diff --git a/GNUmakefile b/GNUmakefile index e3aa32b..696fd91 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,13 +1,14 @@ cssfiles := $(shell find static -name '*.css' -not -name '*.min.css') cssfiles := $(cssfiles:.css=.min.css) gofiles := $(shell find main.go src -name '*.go') +sqlfiles := $(shell find src/dbx/sql -name '*.sql') templates := $(shell find src/templates -name '*.tmpl') exttmpl := $(wildcard cmd/exttmpl/*.go) all: euro-cash.eu exttmpl -euro-cash.eu: $(cssfiles) $(templates) $(gofiles) +euro-cash.eu: $(cssfiles) $(templates) $(gofiles) $(sqlfiles) go build all-i18n: exttmpl @@ -29,4 +30,4 @@ clean: -or -name '*.tar.gz' \ \) -delete -.PHONY: all-i18n clean release +.PHONY: all-i18n clean release
\ No newline at end of file @@ -3,6 +3,8 @@ module git.thomasvoss.com/euro-cash.eu go 1.24 require ( + github.com/mattn/go-sqlite3 v1.14.28 + golang.org/x/crypto v0.39.0 golang.org/x/text v0.26.0 golang.org/x/tools v0.34.0 ) @@ -1,5 +1,9 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= @@ -11,6 +11,7 @@ import ( "time" "git.thomasvoss.com/euro-cash.eu/src" + "git.thomasvoss.com/euro-cash.eu/src/dbx" "git.thomasvoss.com/euro-cash.eu/src/email" ) @@ -29,11 +30,14 @@ func main() { "address to send error messages from") flag.StringVar(&email.Config.Password, "email-password", "", "password to authenticate the email client") + flag.StringVar(&dbx.DBName, "db-name", "eurocash.db", + "database name or ‘:memory:’ for an in-memory database") flag.Parse() if *debugp { go watch() } + dbx.Init() src.Run(*port) } @@ -55,6 +59,7 @@ func watch() { } if nstat.ModTime() != ostat.ModTime() { + dbx.DB.Close() if err := syscall.Exec(path, os.Args, os.Environ()); err != nil { log.Fatal(err) } diff --git a/src/dbx/db.go b/src/dbx/db.go new file mode 100644 index 0000000..9086265 --- /dev/null +++ b/src/dbx/db.go @@ -0,0 +1,227 @@ +package dbx + +import ( + "database/sql" + "embed" + "fmt" + "io/fs" + "log" + "path/filepath" + "reflect" + "sort" + "strings" + + "github.com/mattn/go-sqlite3" +) + +var ( + DB *sql.DB + DBName string + + //go:embed "sql/*.sql" + migrations embed.FS +) + +func Init() { + var err error + if DB, err = sql.Open("sqlite3", DBName); err != nil { + log.Fatal(err) + } + if err = DB.Ping(); err != nil { + log.Fatal(err) + } + + if err := applyMigrations("sql"); err != nil { + log.Fatal(err) + } + + /* TODO: Remove debug code */ + if err := CreateUser(User{ + Email: "mail@thomasvoss.com", + Username: "Thomas", + Password: "69", + AdminP: true, + }); err != nil { + log.Fatal(err) + } + if err := CreateUser(User{ + Email: "foo@BAR.baz", + Username: "Foobar", + Password: "420", + AdminP: false, + }); err != nil { + log.Fatal(err) + } + if _, err := GetMintages("ad"); err != nil { + log.Fatal(err) + } +} + +func applyMigrations(dir string) error { + var latest int + migratedp := true + + rows, err := DB.Query("SELECT latest FROM migration") + if err != nil { + e, ok := err.(sqlite3.Error) + /* IDK if there is a better way to do this… lol */ + if ok && e.Error() == "no such table: migration" { + migratedp = false + } else { + return err + } + } else { + defer rows.Close() + } + + if migratedp { + rows.Next() + if err := rows.Err(); err != nil { + return err + } + if err := rows.Scan(&latest); err != nil { + return err + } + } else { + latest = -1 + } + + files, err := fs.ReadDir(migrations, dir) + if err != nil { + return err + } + + scripts := []string{} + for _, f := range files { + scripts = append(scripts, f.Name()) + } + + sort.Strings(scripts) + for _, f := range scripts[latest+1:] { + qry, err := migrations.ReadFile(filepath.Join(dir, f)) + if err != nil { + return err + } + + tx, err := DB.Begin() + if err != nil { + return err + } + + if _, err := tx.Exec(string(qry)); err != nil { + tx.Rollback() + return fmt.Errorf("error in ‘%s’: %w", f, err) + } + + var n int + if _, err := fmt.Sscanf(f, "%d", &n); err != nil { + return err + } + _, err = tx.Exec("UPDATE migration SET latest = ? WHERE id = 1", n) + if err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return err + } + log.Printf("Applied database migration ‘%s’", f) + } + + return nil +} + +func scanToStructs[T any](rs *sql.Rows) ([]T, error) { + xs := []T{} + for rs.Next() { + x, err := scanToStruct[T](rs) + if err != nil { + return nil, err + } + xs = append(xs, x) + } + return xs, rs.Err() +} + +func scanToStruct[T any](rs *sql.Rows) (T, error) { + var t, zero T + + cols, err := rs.Columns() + if err != nil { + return zero, err + } + + v := reflect.ValueOf(&t).Elem() + tType := v.Type() + + rawValues := make([]any, len(cols)) + for i := range rawValues { + var zero any + rawValues[i] = &zero + } + + if err := rs.Scan(rawValues...); err != nil { + return zero, err + } + + /* col idx → [field idx, array idx] */ + arrayTargets := make(map[int][2]int) + colToField := make(map[string]int) + + for i := 0; i < tType.NumField(); i++ { + field := tType.Field(i) + tag := field.Tag.Get("db") + if tag == "" { + continue + } + + if strings.Contains(tag, ";") { + parts := strings.Split(tag, ";") + tag, dbcols := parts[0], parts[1:] + if tag != "array" { + /* TODO: This is bad… it should log something */ + return zero, fmt.Errorf("invalid `db:\"…\"` tag ‘%s’", tag) + } + fv := v.Field(i) + if fv.Kind() != reflect.Array { + return zero, fmt.Errorf("field ‘%s’ is not array", + field.Name) + } + if len(dbcols) != fv.Len() { + return zero, fmt.Errorf("field ‘%s’ array length mismatch", + field.Name) + } + for j, colName := range cols { + for k, dbColName := range dbcols { + if colName == dbColName { + arrayTargets[j] = [2]int{i, k} + } + } + } + } else { + colToField[tag] = i + } + } + + for i, col := range cols { + vp := rawValues[i].(*any) + if fieldIdx, ok := colToField[col]; ok { + assignValue(v.Field(fieldIdx), *vp) + } else if target, ok := arrayTargets[i]; ok { + assignValue(v.Field(target[0]).Index(target[1]), *vp) + } + } + + return t, nil +} + +func assignValue(fv reflect.Value, val any) { + if val == nil { + fv.Set(reflect.Zero(fv.Type())) + return + } + v := reflect.ValueOf(val) + if v.Type().ConvertibleTo(fv.Type()) { + fv.Set(v.Convert(fv.Type())) + } +} diff --git a/src/dbx/mintages.go b/src/dbx/mintages.go new file mode 100644 index 0000000..96cc871 --- /dev/null +++ b/src/dbx/mintages.go @@ -0,0 +1,65 @@ +package dbx + +type MintageData struct { + Standard []MSRow + Commemorative []MCRow +} + +type MSRow struct { + Type int `db:"type"` + Year int `db:"year"` + Mintmark string `db:"mintmark"` + Mintages [ndenoms]int `db:"array;€0,01;€0,02;€0,05;€0,10;€0,20;€0,50;€1,00;€2,00"` + Reference string `db:"reference"` +} + +type MCRow struct { + Type int `db:"type"` + Year int `db:"year"` + Name string `db:"name"` + Number int `db:"number"` + Mintmark string `db:"mintmark"` + Mintage int `db:"mintage"` + Reference string `db:"reference"` +} + +/* DO NOT REORDER! */ +const ( + TypeCirc = iota + TypeNifc + TypeProof +) + +/* DO NOT REORDER! */ +const ( + MintageUnknown = -iota - 1 + MintageInvalid +) + +const ndenoms = 8 + +func GetMintages(country string) (MintageData, error) { + var zero MintageData + + srows, err := DB.Query(`SELECT * FROM mintages_s WHERE country = ?`, country) + if err != nil { + return zero, err + } + defer srows.Close() + xs, err := scanToStructs[MSRow](srows) + if err != nil { + return zero, err + } + + crows, err := DB.Query(`SELECT * FROM mintages_c WHERE country = ?`, country) + if err != nil { + return zero, err + } + defer crows.Close() + ys, err := scanToStructs[MCRow](crows) + if err != nil { + return zero, err + } + + return MintageData{xs, ys}, nil +} diff --git a/src/dbx/sql/000-genesis.sql b/src/dbx/sql/000-genesis.sql new file mode 100644 index 0000000..6daad31 --- /dev/null +++ b/src/dbx/sql/000-genesis.sql @@ -0,0 +1,74 @@ +PRAGMA encoding = "UTF-8"; + +CREATE TABLE migration ( + id INTEGER PRIMARY KEY CHECK (id = 1), + latest INTEGER +); +INSERT INTO migration (id, latest) VALUES (1, -1); + +CREATE TABLE mintages_s ( + country CHAR(2) NOT NULL COLLATE BINARY + CHECK(length(country) = 2), + type INTEGER NOT NULL -- Codes correspond to contants in mintages.go + CHECK(type BETWEEN 0 AND 2), + year INTEGER NOT NULL, + mintmark TEXT, + [€0,01] INTEGER, + [€0,02] INTEGER, + [€0,05] INTEGER, + [€0,10] INTEGER, + [€0,20] INTEGER, + [€0,50] INTEGER, + [€1,00] INTEGER, + [€2,00] INTEGER, + reference TEXT +); + +CREATE TABLE mintages_c ( + country CHAR(2) NOT NULL COLLATE BINARY + CHECK(length(country) = 2), + type INTEGER NOT NULL -- Codes correspond to contants in mintages.go + CHECK(type BETWEEN 0 AND 2), + name TEXT NOT NULL, + year INTEGER NOT NULL, + number INTEGER NOT NULL, + mintmark TEXT, + mintage INTEGER, + reference TEXT +); + +-- TODO: Remove dummy data +INSERT INTO mintages_s ( + country, + type, + year, + mintmark, + [€0,01], + [€0,02], + [€0,05], + [€0,10], + [€0,20], + [€0,50], + [€1,00], + [€2,00], + reference +) VALUES + ("ad", 0, 2014, NULL, 60000, 60000, 860000, 860000, 860000, 340000, 511843, 360000, NULL), + ("ad", 0, 2015, NULL, 0, 0, 0, 0, 0, 0, 0, 1072400, NULL), + ("ad", 0, 2016, NULL, 0, 0, 0, 0, 0, 0, 2339200, 0, NULL), + ("ad", 0, 2017, NULL, 2582395, 1515000, 2191421, 1103000, 1213000, 968800, 17000, 794588, NULL), + ("ad", 0, 2018, NULL, 2430000, 2550000, 1800000, 980000, 1014000, 890000, 0, 868000, NULL), + ("ad", 0, 2019, NULL, 2447000, 1727000, 2100000, 1610000, 1570000, 930000, 0, 1058310, NULL), + ("ad", 0, 2020, NULL, 0, 0, 0, 860000, 175000, 740000, 0, 1500000, NULL), + ("ad", 0, 2021, NULL, 200000, 700000, 0, 1400000, 1420000, 600000, 50000, 1474500, NULL), + ("ad", 0, 2022, NULL, 700000, 450000, 400000, 700000, 700000, 380000, 0, 1708000, NULL), + ("ad", 0, 2023, NULL, 0, 0, 0, 0, 0, 0, 0, 2075250, NULL), + ("ad", 0, 2024, NULL, 0, 900300, 1950000, 1000000, 700000, 500000, 1050000, 1601200, NULL), + ("ad", 0, 2025, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +CREATE TABLE users ( + email TEXT COLLATE BINARY, + username TEXT COLLATE BINARY, + password TEXT COLLATE BINARY, + adminp INTEGER +);
\ No newline at end of file diff --git a/src/dbx/users.go b/src/dbx/users.go new file mode 100644 index 0000000..a0712ee --- /dev/null +++ b/src/dbx/users.go @@ -0,0 +1,64 @@ +package dbx + +import ( + "database/sql" + "errors" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/text/unicode/norm" +) + +type User struct { + Email string + Username string + Password string + AdminP bool +} + +var LoginFailed = errors.New("No user with the given username and password") + +func CreateUser(user User) error { + user.Username = norm.NFC.String(user.Username) + user.Password = norm.NFC.String(user.Password) + + hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 15) + if err != nil { + return err + } + + _, err = DB.Exec(` + INSERT INTO users ( + email, + username, + password, + adminp + ) VALUES (?, ?, ?, ?) + `, user.Email, user.Username, string(hash), user.AdminP) + return err +} + +func Login(username, password string) (User, error) { + username = norm.NFC.String(username) + password = norm.NFC.String(password) + + u := User{} + /* TODO: Pass a context here? */ + err := DB.QueryRow(`SELECT * FROM users WHERE username = ?`, username). + Scan(&u.Email, &u.Username, &u.Password, &u.AdminP) + + switch { + case errors.Is(err, sql.ErrNoRows): + return User{}, LoginFailed + case err != nil: + return User{}, err + } + + err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) + switch { + case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword): + return User{}, LoginFailed + case err != nil: + return User{}, err + } + return u, nil +} diff --git a/src/http.go b/src/http.go index eaaeb71..4c2f5c6 100644 --- a/src/http.go +++ b/src/http.go @@ -12,8 +12,8 @@ import ( "strconv" "strings" + "git.thomasvoss.com/euro-cash.eu/src/dbx" "git.thomasvoss.com/euro-cash.eu/src/email" - "git.thomasvoss.com/euro-cash.eu/src/mintage" ) type middleware = func(http.Handler) http.Handler @@ -42,7 +42,9 @@ func Run(port int) { portStr := ":" + strconv.Itoa(port) log.Println("Listening on", portStr) - log.Fatal(http.ListenAndServe(portStr, mux)) + err := http.ListenAndServe(portStr, mux) + dbx.DB.Close() + log.Fatal(err) } func chain(xs ...middleware) middleware { @@ -138,12 +140,13 @@ func mintageHandler(next http.Handler) http.Handler { } var err error - td.Mintages, err = mintage.Parse(td.Code) + td.Mintages, err = dbx.GetMintages(td.Code) if err != nil { throwError(http.StatusInternalServerError, err, w, r) return } + processMintages(&td.Mintages, td.Type) next.ServeHTTP(w, r) }) } @@ -188,3 +191,35 @@ func throwError(status int, err error, w http.ResponseWriter, r *http.Request) { Msg: http.StatusText(status), }) } + +func processMintages(md *dbx.MintageData, typeStr string) { + var typ int + switch typeStr { + case "nifc": + typ = dbx.TypeNifc + case "proof": + typ = dbx.TypeProof + default: + typ = dbx.TypeCirc + } + + md.Standard = slices.DeleteFunc(md.Standard, + func(x dbx.MSRow) bool { return x.Type != typ }) + md.Commemorative = slices.DeleteFunc(md.Commemorative, + func(x dbx.MCRow) bool { return x.Type != typ }) + slices.SortFunc(md.Standard, func(x, y dbx.MSRow) int { + if x.Year != y.Year { + return x.Year - y.Year + } + return strings.Compare(x.Mintmark, y.Mintmark) + }) + slices.SortFunc(md.Commemorative, func(x, y dbx.MCRow) int { + if x.Year != y.Year { + return x.Year - y.Year + } + if x.Number != y.Number { + return x.Number - y.Number + } + return strings.Compare(x.Mintmark, y.Mintmark) + }) +} diff --git a/src/i18n.go b/src/i18n.go index 15015c8..e234fde 100644 --- a/src/i18n.go +++ b/src/i18n.go @@ -27,8 +27,8 @@ type locale struct { } var ( - /* To determine the correct date format to use, use the ‘datefmt’ script in - the repository root */ + /* To determine the correct date format to use, use the ‘datefmt’ script in + the repository root */ Locales = [...]locale{ { Bcp: "ca", @@ -176,7 +176,7 @@ var ( Bcp: "bg", Name: "български", dateFmt: "2.01.2006 г.", - Eurozone: false, /* TODO(2026): Set to true */ + Eurozone: false, /* TODO(2026): Set to true */ Enabled: true, }, { diff --git a/src/mintage/mintage.go b/src/mintage/mintage.go deleted file mode 100644 index 0e98bd1..0000000 --- a/src/mintage/mintage.go +++ /dev/null @@ -1,42 +0,0 @@ -package mintage - -type Data struct { - Standard []SRow - Commemorative []CRow -} - -type SRow struct { - Year int - Mintmark string - Mintages [denoms]int - Reference string -} - -type CRow struct { - Year int - Name string - Mintmark string - Mintage int - Reference string -} - -const ( - TypeCirc = iota - TypeNifc - TypeProof -) - -const ( - Unknown = -iota - 1 - Invalid -) - -const denoms = 8 - -var cache map[string][3]Data = make(map[string][3]Data) - -func ClearCache(country string) { - if _, ok := cache[country]; ok { - delete(cache, country) - } -} diff --git a/src/mintage/parser.go b/src/mintage/parser.go deleted file mode 100644 index fe9ebd7..0000000 --- a/src/mintage/parser.go +++ /dev/null @@ -1,168 +0,0 @@ -package mintage - -import ( - "encoding/csv" - "io" - "os" - "path/filepath" - "strconv" - "time" -) - -func Parse(country string) ([3]Data, error) { - if data, ok := cache[country]; ok { - return data, nil - } - - var ( - data [3]Data - err error - path = filepath.Join("data", "mintages", country) - ) - - data[TypeCirc].Standard, err = parseS(path + "-s-circ.csv") - if err != nil { - return data, err - } - data[TypeNifc].Standard, err = parseS(path + "-s-nifc.csv") - if err != nil { - return data, err - } - data[TypeProof].Standard, err = parseS(path + "-s-proof.csv") - if err != nil { - return data, err - } - data[TypeCirc].Commemorative, err = parseC(path + "-c-circ.csv") - if err != nil { - return data, err - } - data[TypeNifc].Commemorative, err = parseC(path + "-c-nifc.csv") - if err != nil { - return data, err - } - data[TypeProof].Commemorative, err = parseC(path + "-c-proof.csv") - if err == nil { - cache[country] = data - } - return data, err -} - -func parseS(path string) ([]SRow, error) { - rows := make([]SRow, 0, guessRows(false)) - - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - - r := csv.NewReader(f) - r.Comment = '#' - r.FieldsPerRecord = 11 - r.ReuseRecord = true - - /* Skip header */ - if _, err := r.Read(); err != nil { - return nil, err - } - - for { - record, err := r.Read() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - - data := SRow{ - Mintmark: record[1], - Reference: record[10], - } - - data.Year, err = strconv.Atoi(record[0]) - if err != nil { - return nil, err - } - - for i, s := range record[2:10] { - if s == "" { - data.Mintages[i] = Unknown - } else { - data.Mintages[i], err = strconv.Atoi(s) - if err != nil { - data.Mintages[i] = Invalid - } - } - } - - rows = append(rows, data) - } - - return rows, nil -} - -func parseC(path string) ([]CRow, error) { - rows := make([]CRow, 0, guessRows(true)) - - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - - r := csv.NewReader(f) - r.Comment = '#' - r.FieldsPerRecord = 5 - r.ReuseRecord = true - - /* Skip header */ - if _, err := r.Read(); err != nil { - return nil, err - } - - for { - record, err := r.Read() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - - data := CRow{ - Name: record[1], - Mintmark: record[2], - Reference: record[4], - } - - data.Year, err = strconv.Atoi(record[0]) - if err != nil { - return nil, err - } - - s := record[3] - if s == "" { - data.Mintage = Unknown - } else { - data.Mintage, err = strconv.Atoi(s) - if err != nil { - data.Mintage = Invalid - } - } - - rows = append(rows, data) - } - - return rows, nil -} - -func guessRows(commemorativep bool) int { - /* Try to guess the number of rows for Germany, because nobody needs more - rows than Germany. */ - n := (time.Now().Year() - 2002) * 5 - if commemorativep { - return n * 2 - } - return n -} diff --git a/src/templates.go b/src/templates.go index f6413d9..28b7700 100644 --- a/src/templates.go +++ b/src/templates.go @@ -7,13 +7,13 @@ import ( "log" "strings" - "git.thomasvoss.com/euro-cash.eu/src/mintage" + "git.thomasvoss.com/euro-cash.eu/src/dbx" ) type templateData struct { Printer Printer Code, Type string - Mintages [3]mintage.Data + Mintages dbx.MintageData Countries []country } @@ -24,13 +24,12 @@ var ( errorTmpl = buildTemplate("-error") templates map[string]*template.Template funcmap = map[string]any{ - "denoms": denoms, - "locales": locales, - "safe": asHTML, - "sprintf": fmt.Sprintf, - "strToCtype": strToCtype, - "toUpper": strings.ToUpper, - "tuple": templateMakeTuple, + "denoms": denoms, + "locales": locales, + "safe": asHTML, + "sprintf": fmt.Sprintf, + "toUpper": strings.ToUpper, + "tuple": templateMakeTuple, } ) @@ -83,17 +82,6 @@ func templateMakeTuple(args ...any) []any { return args } -func strToCtype(s string) int { - switch s { - case "nifc": - return mintage.TypeNifc - case "proof": - return mintage.TypeProof - default: - return mintage.TypeCirc - } -} - func (td templateData) T(fmt string, args ...any) string { return td.Printer.T(fmt, args...) } diff --git a/src/templates/coins-mintages.html.tmpl b/src/templates/coins-mintages.html.tmpl index 8554c3f..772db33 100644 --- a/src/templates/coins-mintages.html.tmpl +++ b/src/templates/coins-mintages.html.tmpl @@ -66,7 +66,6 @@ </div> <button type="submit">{{ .T "Filter" }}</button> </form> - {{ $data := (index .Mintages (strToCtype .Type)) }} <figure> <figcaption>{{ .T "Standard Issue Coins" }}</figcaption> <table class="mintage-table" role="grid"> @@ -81,7 +80,7 @@ <tbody> {{ $p := .Printer }} {{ $type := .Type }} - {{ range $data.Standard }} + {{ range .Mintages.Standard }} <tr> <th scope="col"> {{- .Year -}} @@ -106,7 +105,7 @@ </tbody> </table> </figure> - {{ if ne (len $data.Commemorative) 0 }} + {{ if ne (len .Mintages.Commemorative) 0 }} <figure> <figcaption>{{ .T "Commemorative Coins" }}</figcaption> <table class="mintage-table-cc" role="grid"> @@ -118,7 +117,7 @@ <tbody> {{ $p := .Printer }} {{ $type := .Type }} - {{ range $data.Commemorative }} + {{ range .Mintages.Commemorative }} <tr> <th scope="col"> {{- .Year -}} |