From b3d30a019edc940a9bda1c7a93bfec0db7fdc28d Mon Sep 17 00:00:00 2001 From: Lee Brown Date: Thu, 1 Aug 2019 13:45:38 -0800 Subject: [PATCH] completed initial signup with autocompletes for region and city --- cmd/web-app/handlers/api_geo.go | 128 +++++++++++++-- cmd/web-app/handlers/routes.go | 7 +- .../templates/content/signup-step1.tmpl | 151 +++++++++++------- internal/geonames/countries.go | 2 +- internal/geonames/country_timezones.go | 65 ++++++++ internal/geonames/geonames.go | 83 +++++----- internal/geonames/models.go | 47 +++--- internal/schema/migrations.go | 82 +++++++++- 8 files changed, 424 insertions(+), 141 deletions(-) create mode 100644 internal/geonames/country_timezones.go diff --git a/cmd/web-app/handlers/api_geo.go b/cmd/web-app/handlers/api_geo.go index 7efefdb..3e4c9af 100644 --- a/cmd/web-app/handlers/api_geo.go +++ b/cmd/web-app/handlers/api_geo.go @@ -6,9 +6,9 @@ import ( "net/http" "strings" + "geeks-accelerator/oss/saas-starter-kit/internal/geonames" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "github.com/jmoiron/sqlx" - "geeks-accelerator/oss/saas-starter-kit/internal/geonames" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" ) @@ -18,35 +18,141 @@ type Geo struct { Redis *redis.Client } +// GeonameByPostalCode... +func (h *Geo) GeonameByPostalCode(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + var filters []string + var args []interface{} + + if qv := r.URL.Query().Get("country_code"); qv != "" { + filters = append(filters, "country_code = ?") + args = append(args, strings.ToUpper(qv)) + } + + if qv := r.URL.Query().Get("postal_code"); qv != "" { + filters = append(filters, "postal_code = ?") + args = append(args, strings.ToLower(qv)) + } else { + filters = append(filters, "lower(postal_code) = ?") + args = append(args, strings.ToLower(params["postalCode"])) + } + + where := strings.Join(filters, " AND ") + + res, err := geonames.FindGeonames(ctx, h.MasterDB, "postal_code", where, args...) + if err != nil { + fmt.Printf("%+v", err) + return web.RespondJsonError(ctx, w, err) + } + + var resp interface{} + if len(res) == 1 { + resp = res[0] + } else { + // Autocomplete does not like null returned. + resp = make(map[string]interface{}) + } + + return web.RespondJson(ctx, w, resp, http.StatusOK) +} + +// PostalCodesAutocomplete... +func (h *Geo) PostalCodesAutocomplete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + var filters []string + var args []interface{} + + if qv := r.URL.Query().Get("country_code"); qv != "" { + filters = append(filters, "country_code = ?") + args = append(args, strings.ToUpper(qv)) + } + + if qv := r.URL.Query().Get("query"); qv != "" { + filters = append(filters, "lower(postal_code) like ?") + args = append(args, strings.ToLower(qv+"%")) + } + + where := strings.Join(filters, " AND ") + + res, err := geonames.FindGeonamePostalCodes(ctx, h.MasterDB, where, args...) + if err != nil { + return web.RespondJsonError(ctx, w, err) + } + var list []string = res + + return web.RespondJson(ctx, w, list, http.StatusOK) +} + // RegionsAutocomplete... func (h *Geo) RegionsAutocomplete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { var filters []string var args []interface{} - if qv := r.URL.Query().Get("postal_code"); qv != "" { - filters = append(filters,"postal_code like ?") - args = append(args, qv+"%") + if qv := r.URL.Query().Get("country_code"); qv != "" { + filters = append(filters, "country_code = ?") + args = append(args, strings.ToUpper(qv)) } if qv := r.URL.Query().Get("query"); qv != "" { - filters = append(filters,"(state_name like ? or state_code like ?)") - args = append(args, qv+"%", qv+"%") + filters = append(filters, "(lower(state_name) like ? or state_code like ?)") + args = append(args, strings.ToLower(qv+"%"), strings.ToUpper(qv+"%")) } where := strings.Join(filters, " AND ") - res, err := geonames.FindGeonameRegions(ctx, h.MasterDB, where, args) + res, err := geonames.FindGeonameRegions(ctx, h.MasterDB, "state_name", where, args...) if err != nil { fmt.Printf("%+v", err) return web.RespondJsonError(ctx, w, err) } - var list []string - for _, c := range res { - list = append(list, c.Name) + var resp interface{} + if qv := r.URL.Query().Get("select"); qv != "" { + list := []map[string]string{} + for _, c := range res { + list = append(list, map[string]string{ + "value": c.Code, + "text": c.Name, + }) + } + resp = list + } else { + list := []string{} + for _, c := range res { + list = append(list, c.Name) + } + resp = list + } + + return web.RespondJson(ctx, w, resp, http.StatusOK) +} + +// CountryTimezones.... +func (h *Geo) CountryTimezones(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + var filters []string + var args []interface{} + + if qv := r.URL.Query().Get("country_code"); qv != "" { + filters = append(filters, "country_code = ?") + args = append(args, strings.ToUpper(qv)) + } else { + filters = append(filters, "country_code = ?") + args = append(args, strings.ToUpper(params["countryCode"])) + } + + where := strings.Join(filters, " AND ") + + res, err := geonames.FindCountryTimezones(ctx, h.MasterDB, "timezone_id", where, args...) + if err != nil { + return web.RespondJsonError(ctx, w, err) + } + + list := []string{} + for _, t := range res { + list = append(list, t.TimezoneId) } return web.RespondJson(ctx, w, list, http.StatusOK) } - diff --git a/cmd/web-app/handlers/routes.go b/cmd/web-app/handlers/routes.go index 17d8237..528e6d8 100644 --- a/cmd/web-app/handlers/routes.go +++ b/cmd/web-app/handlers/routes.go @@ -70,11 +70,14 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir // Register geo g := Geo{ - MasterDB: masterDB, - Redis: redis, + MasterDB: masterDB, + Redis: redis, } // These routes are not authenticated app.Handle("GET", "/geo/regions/autocomplete", g.RegionsAutocomplete) + app.Handle("GET", "/geo/postal_codes/autocomplete", g.PostalCodesAutocomplete) + app.Handle("GET", "/geo/geonames/postal_code/:postalCode", g.GeonameByPostalCode) + app.Handle("GET", "/geo/country/:countryCode/timezones", g.CountryTimezones) // Register root r := Root{ diff --git a/cmd/web-app/templates/content/signup-step1.tmpl b/cmd/web-app/templates/content/signup-step1.tmpl index ce2df6c..5502991 100644 --- a/cmd/web-app/templates/content/signup-step1.tmpl +++ b/cmd/web-app/templates/content/signup-step1.tmpl @@ -35,9 +35,13 @@
- {{ range $i := $.countries }} - + {{ $hasGeonames := false }} + {{ range $c := $.geonameCountries }} + {{ if eq $c $i.Code }}{{ $hasGeonames = true }}{{ end }} + {{ end }} + {{ end }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Country" }} @@ -45,19 +49,23 @@
- +
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Zipcode" }}
-
- +
+
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Region" }}
- + {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.City" }}
+

@@ -113,66 +121,95 @@ $(document).ready(function() { $(document).find('body').addClass('bg-gradient-primary'); - var regionAutocompleteParams = null; - var countryAutocompleteParams = null; + $('#selectAccountCountry').on('change', function () { - $('#inputAccountZipcode').on('change', function () { - console.log($(this).val()); + // When a country has data-geonames, then we can perform autocomplete on zipcode and + // populate a list of valid regions. + if ($(this).find('option:selected').attr('data-geonames') == 1) { - regionAutocompleteParams = {postal_code: $(this).val()}; - $('#inputAccountRegion').keyup(); + // Replace the existing region with an empty dropdown. + $('#divAccountRegion').html(''); - countryAutocompleteParams = {postal_code: $(this).val()}; - $('#inputAccountCountry').keyup(); - }); - - $('#inputAccountRegion').on('keydown', function () { - regionAutocompleteParams = null; - }); - - $('#inputAccountCountry').on('keydown', function () { - countryAutocompleteParams = null; - }); - - $('#inputAccountRegion').autoComplete({ - events: { - search: function (qry, callback) { - if (regionAutocompleteParams === null) { - regionAutocompleteParams = {query: qry}; - } - - $.ajax( - '/geo/regions/autocomplete', - { - data: regionAutocompleteParams + // Query the API for a list of regions for the selected + // country and populate the region dropdown. + $.ajax({ + type: 'GET', + contentType: 'application/json', + url: '/geo/regions/autocomplete', + data: {country_code: $(this).val(), select: true}, + dataType: 'json' + }).done(function (res) { + if (res !== undefined && res !== null) { + for (var c in res) { + $('#inputAccountRegion').append(''); } - ).done(function (res) { - callback(res) - }); - } - } - }); - - $('#inputAccountCountry').autoComplete({ - events: { - search: function (qry, callback) { - if (countryAutocompleteParams === null) { - countryAutocompleteParams = {query: qry}; } + }); - $.ajax( - '/geo/countries/autocomplete', - { - data: countryAutocompleteParams + /* + // Remove all the existing items from the timezone dropdown and repopulate it. + $('#selectAccountTimezone').find('option').remove().end() + $.ajax({ + type: 'GET', + contentType: 'application/json', + url: '/geo/country/'+$(this).val()+'/timezones', + data: {}, + dataType: 'json' + }).done(function (res) { + if (res !== undefined && res !== null) { + for (var c in res) { + $('#selectAccountTimezone').append(''); + } + } + }); + */ + + // Replace the existing zipcode text input with a new one that will supports autocomplete. + $('#divAccountZipcode').html(''); + $('#inputAccountZipcode').autoComplete({ + minLength: 2, + events: { + search: function (qry, callback) { + $.ajax({ + type: 'GET', + contentType: 'application/json', + url: '/geo/postal_codes/autocomplete', + data: {query: qry, country_code: $('#selectAccountCountry').val()}, + dataType: 'json' + }).done(function (res) { + callback(res) + }); + } + } + }); + + // When the value of zipcode changes, try to find an exact match for the zipcode and + // can therefore set the correct region and city. + $('#inputAccountZipcode').on('change', function() { + $.ajax({ + type: 'GET', + contentType: 'application/json', + url: '/geo/geonames/postal_code/'+$(this).val(), + data: {country_code: $('#selectAccountCountry').val()}, + dataType: 'json' + }).done(function (res) { + if (res !== undefined && res !== null && res.PostalCode !== undefined) { + $('#inputAccountCity').val(res.PlaceName); + $('#inputAccountRegion').val(res.StateCode); } - ).done(function (res) { - callback(res) }); - } + }); + + } else { + + // Replace the existing zipcode input with no autocomplete. + $('#divAccountZipcode').html(''); + + // Replace the existing region select with a text input. + $('#divAccountRegion').html(''); + } - }); - - + }).change(); }); {{end}} \ No newline at end of file diff --git a/internal/geonames/countries.go b/internal/geonames/countries.go index 5720769..4eaaf65 100644 --- a/internal/geonames/countries.go +++ b/internal/geonames/countries.go @@ -50,7 +50,7 @@ func FindCountries(ctx context.Context, dbConn *sqlx.DB, orderBy, where string, v Country err error ) - err = rows.Scan(&v.Code,&v.IsoAlpha3,&v.Name,&v.Capital,&v.CurrencyCode,&v.CurrencyName,&v.Phone,&v.PostalCodeFormat,&v.PostalCodeRegex) + err = rows.Scan(&v.Code, &v.IsoAlpha3, &v.Name, &v.Capital, &v.CurrencyCode, &v.CurrencyName, &v.Phone, &v.PostalCodeFormat, &v.PostalCodeRegex) if err != nil { err = errors.Wrapf(err, "query - %s", query.String()) return nil, err diff --git a/internal/geonames/country_timezones.go b/internal/geonames/country_timezones.go new file mode 100644 index 0000000..807f595 --- /dev/null +++ b/internal/geonames/country_timezones.go @@ -0,0 +1,65 @@ +package geonames + +import ( + "context" + "github.com/huandu/go-sqlbuilder" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +const ( + // The database table for CountryTimezone + countrieTimezonesTableName = "country_timezones" +) + +// FindCountryTimezones .... +func FindCountryTimezones(ctx context.Context, dbConn *sqlx.DB, orderBy, where string, args ...interface{}) ([]*CountryTimezone, error) { + span, ctx := tracer.StartSpanFromContext(ctx, "internal.geonames.FindCountryTimezones") + defer span.Finish() + + query := sqlbuilder.NewSelectBuilder() + query.Select("country_code,timezone_id") + query.From(countrieTimezonesTableName) + + if orderBy == "" { + orderBy = "timezone_id" + } + query.OrderBy(orderBy) + + if where != "" { + query.Where(where) + } + + queryStr, queryArgs := query.Build() + queryStr = dbConn.Rebind(queryStr) + args = append(args, queryArgs...) + + // Fetch all country timezones from the db. + rows, err := dbConn.QueryContext(ctx, queryStr, args...) + if err != nil { + err = errors.Wrapf(err, "query - %s", query.String()) + err = errors.WithMessage(err, "find country timezones failed") + return nil, err + } + + // iterate over each row + resp := []*CountryTimezone{} + for rows.Next() { + var ( + v CountryTimezone + err error + ) + err = rows.Scan(&v.CountryCode, &v.TimezoneId) + if err != nil { + err = errors.Wrapf(err, "query - %s", query.String()) + return nil, err + } else if v.CountryCode == "" || v.TimezoneId == "" { + continue + } + + resp = append(resp, &v) + } + + return resp, nil +} diff --git a/internal/geonames/geonames.go b/internal/geonames/geonames.go index ce25dcd..e74547d 100644 --- a/internal/geonames/geonames.go +++ b/internal/geonames/geonames.go @@ -27,15 +27,15 @@ const ( var ( // List of country codes that will geonames will be downloaded for. ValidGeonameCountries = []string{ - "AD", "AR", "AS", "AT", "AU", "AX", "BD", "BE", "BG", "BM", - "BR", "BY", "CA", "CH", "CO", "CR", "CZ", "DE", "DK", "DO", - "DZ", "ES", "FI", "FO", "FR", "GB", "GF", "GG", "GL", "GP", - "GT", "GU", "HR", "HU", "IE", "IM", "IN", "IS", "IT", "JE", - "JP", "LI", "LK", "LT", "LU", "LV", "MC", "MD", "MH", "MK", - "MP", "MQ", "MT", "MX", "MY", "NC", "NL", "NO", "NZ", "PH", - "PK", "PL", "PM", "PR", "PT", "RE", "RO", "RU", "SE", "SI", - "SJ", "SK", "SM", "TH", "TR", "UA", "US", "UY", "VA", "VI", - "WF", "YT", "ZA"} + "AD", "AR", "AS", "AT", "AU", "AX", "BD", "BE", "BG", "BM", + "BR", "BY", "CA", "CH", "CO", "CR", "CZ", "DE", "DK", "DO", + "DZ", "ES", "FI", "FO", "FR", "GB", "GF", "GG", "GL", "GP", + "GT", "GU", "HR", "HU", "IE", "IM", "IN", "IS", "IT", "JE", + "JP", "LI", "LK", "LT", "LU", "LV", "MC", "MD", "MH", "MK", + "MP", "MQ", "MT", "MX", "MY", "NC", "NL", "NO", "NZ", "PH", + "PK", "PL", "PM", "PR", "PT", "RE", "RO", "RU", "SE", "SI", + "SJ", "SK", "SM", "TH", "TR", "UA", "US", "UY", "VA", "VI", + "WF", "YT", "ZA"} ) // FindGeonames .... @@ -75,10 +75,10 @@ func FindGeonames(ctx context.Context, dbConn *sqlx.DB, orderBy, where string, a v Geoname err error ) - err = rows.Scan(&v.CountryCode,&v.PostalCode,&v.PlaceName,&v.StateName,&v.StateCode,&v.CountyName,&v.CountyCode,&v.CommunityName,&v.CommunityCode,&v.Latitude,&v.Longitude,&v.Accuracy) + err = rows.Scan(&v.CountryCode, &v.PostalCode, &v.PlaceName, &v.StateName, &v.StateCode, &v.CountyName, &v.CountyCode, &v.CommunityName, &v.CommunityCode, &v.Latitude, &v.Longitude, &v.Accuracy) if err != nil { return nil, errors.WithStack(err) - }else if v.PostalCode == "" { + } else if v.PostalCode == "" { continue } @@ -142,7 +142,6 @@ func FindGeonameRegions(ctx context.Context, dbConn *sqlx.DB, orderBy, where str query.Select("distinct state_code", "state_name") query.From(geonamesTableName) - if orderBy == "" { orderBy = "state_name" } @@ -171,11 +170,11 @@ func FindGeonameRegions(ctx context.Context, dbConn *sqlx.DB, orderBy, where str v Region err error ) - err = rows.Scan(&v.Code,&v.Name) + err = rows.Scan(&v.Code, &v.Name) if err != nil { err = errors.Wrapf(err, "query - %s", query.String()) return nil, err - } else if v.Code == "" { + } else if v.Code == "" || v.Name == "" { continue } @@ -209,11 +208,11 @@ func LoadGeonames(ctx context.Context, rr chan<- interface{}, countries ...strin // Possible types sent to the channel are limited to: // - error // - GeoName -func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country string ) { +func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country string) { u := fmt.Sprintf("http://download.geonames.org/export/zip/%s.zip", country) resp, err := pester.Get(u) if err != nil { - rr <- errors.WithMessagef(err, "Failed to read countries from '%s'", u) + rr <- errors.WithMessagef(err, "Failed to read countries from '%s'", u) return } defer resp.Body.Close() @@ -223,14 +222,14 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri buff := bytes.NewBuffer([]byte{}) size, err := io.Copy(buff, br) if err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) return } b := bytes.NewReader(buff.Bytes()) zr, err := zip.NewReader(b, size) if err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) return } @@ -241,7 +240,7 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri fh, err := f.Open() if err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) return } @@ -260,49 +259,49 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri lines, err := r.ReadAll() if err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) continue } for _, row := range lines { /* - fmt.Println("CountryCode: row[0]", row[0]) - fmt.Println("PostalCode: row[1]", row[1]) - fmt.Println("PlaceName: row[2]", row[2]) - fmt.Println("StateName: row[3]", row[3]) - fmt.Println("StateCode : row[4]", row[4]) - fmt.Println("CountyName: row[5]", row[5]) - fmt.Println("CountyCode : row[6]", row[6]) - fmt.Println("CommunityName: row[7]", row[7]) - fmt.Println("CommunityCode: row[8]", row[8]) - fmt.Println("Latitude: row[9]", row[9]) - fmt.Println("Longitude: row[10]", row[10]) - fmt.Println("Accuracy: row[11]", row[11]) + fmt.Println("CountryCode: row[0]", row[0]) + fmt.Println("PostalCode: row[1]", row[1]) + fmt.Println("PlaceName: row[2]", row[2]) + fmt.Println("StateName: row[3]", row[3]) + fmt.Println("StateCode : row[4]", row[4]) + fmt.Println("CountyName: row[5]", row[5]) + fmt.Println("CountyCode : row[6]", row[6]) + fmt.Println("CommunityName: row[7]", row[7]) + fmt.Println("CommunityCode: row[8]", row[8]) + fmt.Println("Latitude: row[9]", row[9]) + fmt.Println("Longitude: row[10]", row[10]) + fmt.Println("Accuracy: row[11]", row[11]) */ gn := Geoname{ - CountryCode: row[0], - PostalCode: row[1], - PlaceName: row[2], - StateName: row[3], - StateCode : row[4], - CountyName: row[5], - CountyCode : row[6], + CountryCode: row[0], + PostalCode: row[1], + PlaceName: row[2], + StateName: row[3], + StateCode: row[4], + CountyName: row[5], + CountyCode: row[6], CommunityName: row[7], CommunityCode: row[8], } if row[9] != "" { gn.Latitude, err = decimal.NewFromString(row[9]) if err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) } } if row[10] != "" { gn.Longitude, err = decimal.NewFromString(row[10]) if err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) } } @@ -318,7 +317,7 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri } if err := scanner.Err(); err != nil { - rr <- errors.WithStack(err) + rr <- errors.WithStack(err) } } } diff --git a/internal/geonames/models.go b/internal/geonames/models.go index 2564d6d..a70274d 100644 --- a/internal/geonames/models.go +++ b/internal/geonames/models.go @@ -3,35 +3,38 @@ package geonames import "github.com/shopspring/decimal" type Geoname struct { - CountryCode string // US - PostalCode string // 99686 - PlaceName string // Valdez - StateName string // Alaska - StateCode string // AK - CountyName string // Valdez-Cordova - CountyCode string // 261 - CommunityName string // - CommunityCode string // - Latitude decimal.Decimal // 61.101 - Longitude decimal.Decimal // -146.9 - Accuracy int // 1 + CountryCode string // US + PostalCode string // 99686 + PlaceName string // Valdez + StateName string // Alaska + StateCode string // AK + CountyName string // Valdez-Cordova + CountyCode string // 261 + CommunityName string // + CommunityCode string // + Latitude decimal.Decimal // 61.101 + Longitude decimal.Decimal // -146.9 + Accuracy int // 1 } type Country struct { - Code string // US - Name string - IsoAlpha3 string - Capital string - CurrencyCode string // .us - CurrencyName string // USD Dollar - Phone string // 1 - PostalCodeFormat string // #####-#### - PostalCodeRegex string // ^\d{5}(-\d{4})?$ + Code string // US + Name string + IsoAlpha3 string + Capital string + CurrencyCode string // .us + CurrencyName string // USD Dollar + Phone string // 1 + PostalCodeFormat string // #####-#### + PostalCodeRegex string // ^\d{5}(-\d{4})?$ } - type Region struct { Code string // AK Name string // Alaska } +type CountryTimezone struct { + CountryCode string // US + TimezoneId string // America/Anchorage +} diff --git a/internal/schema/migrations.go b/internal/schema/migrations.go index bcb9880..bf1ccb0 100644 --- a/internal/schema/migrations.go +++ b/internal/schema/migrations.go @@ -9,11 +9,11 @@ import ( "strings" "geeks-accelerator/oss/saas-starter-kit/internal/geonames" - "github.com/sethgrid/pester" "github.com/geeks-accelerator/sqlxmigrate" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/pkg/errors" + "github.com/sethgrid/pester" ) // migrationList returns a list of migrations to be executed. If the id of the @@ -255,11 +255,11 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration { for r := range resChan { switch v := r.(type) { case geonames.Geoname: - _, err = stmt.Exec(v.CountryCode,v.PostalCode,v.PlaceName,v.StateName,v.StateCode,v.CountyName,v.CountyCode,v.CommunityName,v.CommunityCode,v.Latitude,v.Longitude,v.Accuracy) + _, err = stmt.Exec(v.CountryCode, v.PostalCode, v.PlaceName, v.StateName, v.StateCode, v.CountyName, v.CountyCode, v.CommunityName, v.CommunityCode, v.Latitude, v.Longitude, v.Accuracy) if err != nil { return errors.WithStack(err) } - case error : + case error: return v } } @@ -338,7 +338,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration { } // Pull the last comment to load the fields. - if stmt == nil { + if stmt == nil { prevLine = strings.TrimPrefix(prevLine, "#") r := csv.NewReader(strings.NewReader(prevLine)) r.Comma = '\t' // Use tab-delimited instead of comma <---- here! @@ -391,7 +391,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration { cn = "neighbours" case "EquivalentFipsCode": cn = "equivalent_fips_code" - default : + default: return errors.Errorf("Failed to map column %s", fn) } columns = append(columns, cn) @@ -402,7 +402,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration { placeholders = append(placeholders, "?") } - q := "insert into countryinfo ("+strings.Join(columns, ",")+") values("+strings.Join(placeholders, ",")+")" + q := "insert into countryinfo (" + strings.Join(columns, ",") + ") values(" + strings.Join(placeholders, ",") + ")" q = db.Rebind(q) stmt, err = db.Prepare(q) if err != nil { @@ -468,5 +468,75 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration { return nil }, }, + // Load new country_timezones table. + { + ID: "20190731-03d", + Migrate: func(tx *sql.Tx) error { + + queries := []string{ + `DROP TABLE IF EXISTS country_timezones`, + `CREATE TABLE country_timezones( + country_code char(2) not null, + timezone_id character varying(50) not null, + CONSTRAINT country_timezones_pkey UNIQUE (country_code, timezone_id))`, + } + + for _, q := range queries { + _, err := db.Exec(q) + if err != nil { + return errors.WithMessagef(err, "Failed to execute sql query '%s'", q) + } + } + + u := "http://download.geonames.org/export/dump/timeZones.txt" + resp, err := pester.Get(u) + if err != nil { + return errors.WithMessagef(err, "Failed to read timezones info from '%s'", u) + } + defer resp.Body.Close() + + q := "insert into country_timezones (country_code,timezone_id) values(?, ?)" + q = db.Rebind(q) + stmt, err := db.Prepare(q) + if err != nil { + return errors.WithMessagef(err, "Failed to prepare sql query '%s'", q) + } + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + + // Skip comments. + if strings.HasPrefix(line, "CountryCode") { + continue + } + + r := csv.NewReader(strings.NewReader(line)) + r.Comma = '\t' // Use tab-delimited instead of comma <---- here! + r.FieldsPerRecord = -1 + + lines, err := r.ReadAll() + if err != nil { + return errors.WithStack(err) + } + + for _, row := range lines { + _, err = stmt.Exec(row[0], row[1]) + if err != nil { + return errors.WithStack(err) + } + } + } + + if err := scanner.Err(); err != nil { + return errors.WithStack(err) + } + + return nil + }, + Rollback: func(tx *sql.Tx) error { + return nil + }, + }, } }