-
+
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Zipcode" }}
-
-
+
+
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Region" }}
@@ -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
+ },
+ },
}
}