1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-27 00:51:13 +02:00

completed initial signup with autocompletes for region and city

This commit is contained in:
Lee Brown
2019-08-01 13:45:38 -08:00
parent 232648a2cc
commit b3d30a019e
8 changed files with 424 additions and 141 deletions

View File

@ -6,9 +6,9 @@ import (
"net/http" "net/http"
"strings" "strings"
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
) )
@ -18,35 +18,141 @@ type Geo struct {
Redis *redis.Client 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... // RegionsAutocomplete...
func (h *Geo) RegionsAutocomplete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { func (h *Geo) RegionsAutocomplete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
var filters []string var filters []string
var args []interface{} var args []interface{}
if qv := r.URL.Query().Get("postal_code"); qv != "" { if qv := r.URL.Query().Get("country_code"); qv != "" {
filters = append(filters,"postal_code like ?") filters = append(filters, "country_code = ?")
args = append(args, qv+"%") args = append(args, strings.ToUpper(qv))
} }
if qv := r.URL.Query().Get("query"); qv != "" { if qv := r.URL.Query().Get("query"); qv != "" {
filters = append(filters,"(state_name like ? or state_code like ?)") filters = append(filters, "(lower(state_name) like ? or state_code like ?)")
args = append(args, qv+"%", qv+"%") args = append(args, strings.ToLower(qv+"%"), strings.ToUpper(qv+"%"))
} }
where := strings.Join(filters, " AND ") 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 { if err != nil {
fmt.Printf("%+v", err) fmt.Printf("%+v", err)
return web.RespondJsonError(ctx, w, err) return web.RespondJsonError(ctx, w, err)
} }
var list []string var resp interface{}
for _, c := range res { if qv := r.URL.Query().Get("select"); qv != "" {
list = append(list, c.Name) 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) return web.RespondJson(ctx, w, list, http.StatusOK)
} }

View File

@ -70,11 +70,14 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
// Register geo // Register geo
g := Geo{ g := Geo{
MasterDB: masterDB, MasterDB: masterDB,
Redis: redis, Redis: redis,
} }
// These routes are not authenticated // These routes are not authenticated
app.Handle("GET", "/geo/regions/autocomplete", g.RegionsAutocomplete) 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 // Register root
r := Root{ r := Root{

View File

@ -35,9 +35,13 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="col-sm-6 mb-3 mb-sm-0">
<select class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Country" }}" id="selectAccountCountry" name="Account.Country" placeholder="Country" required> <select class="form-control {{ ValidationFieldClass $.validationErrors "Account.Country" }}" id="selectAccountCountry" name="Account.Country" placeholder="Country" required>
{{ range $i := $.countries }} {{ range $i := $.countries }}
<option value="{{ $i.Code }}" {{ if eq $.form.Account.Country $i.Code }}selected="selected"{{ end }}>{{ $i.Name }}</option> {{ $hasGeonames := false }}
{{ range $c := $.geonameCountries }}
{{ if eq $c $i.Code }}{{ $hasGeonames = true }}{{ end }}
{{ end }}
<option value="{{ $i.Code }}" data-geonames="{{ if $hasGeonames }}1{{ else }}0{{ end }}" {{ if eq $.form.Account.Country $i.Code }}selected="selected"{{ end }}>{{ $i.Name }}</option>
{{ end }} {{ end }}
</select> </select>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Country" }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Country" }}
@ -45,19 +49,23 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required> <div id="divAccountZipcode"></div>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Zipcode" }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Zipcode" }}
</div> </div>
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="col-sm-6 mb-3 mb-sm-0" id="divAccountRegion">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required> <div id="divAccountRegion"></div>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Region" }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Region" }}
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.City" }}" name="Account.City" value="{{ $.form.Account.City }}" placeholder="City" required> <input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.City" }}" id="inputAccountCity" name="Account.City" value="{{ $.form.Account.City }}" placeholder="City" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.City" }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.City" }}
</div> </div>
<!-- div class="col-sm-6 mb-3 mb-sm-0">
<select class="form-control {{ ValidationFieldClass $.validationErrors "Account.Timezone" }}" id="selectAccountTimezone" name="Account.Timezone" placeholder="Timezone"></select>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Timezone" }}
</div -->
</div> </div>
<hr> <hr>
@ -113,66 +121,95 @@
$(document).ready(function() { $(document).ready(function() {
$(document).find('body').addClass('bg-gradient-primary'); $(document).find('body').addClass('bg-gradient-primary');
var regionAutocompleteParams = null; $('#selectAccountCountry').on('change', function () {
var countryAutocompleteParams = null;
$('#inputAccountZipcode').on('change', function () { // When a country has data-geonames, then we can perform autocomplete on zipcode and
console.log($(this).val()); // populate a list of valid regions.
if ($(this).find('option:selected').attr('data-geonames') == 1) {
regionAutocompleteParams = {postal_code: $(this).val()}; // Replace the existing region with an empty dropdown.
$('#inputAccountRegion').keyup(); $('#divAccountRegion').html('<select class="form-control {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required></select>');
countryAutocompleteParams = {postal_code: $(this).val()}; // Query the API for a list of regions for the selected
$('#inputAccountCountry').keyup(); // country and populate the region dropdown.
}); $.ajax({
type: 'GET',
$('#inputAccountRegion').on('keydown', function () { contentType: 'application/json',
regionAutocompleteParams = null; url: '/geo/regions/autocomplete',
}); data: {country_code: $(this).val(), select: true},
dataType: 'json'
$('#inputAccountCountry').on('keydown', function () { }).done(function (res) {
countryAutocompleteParams = null; if (res !== undefined && res !== null) {
}); for (var c in res) {
$('#inputAccountRegion').append('<option value="'+res[c].value+'">'+res[c].text+'</option>');
$('#inputAccountRegion').autoComplete({
events: {
search: function (qry, callback) {
if (regionAutocompleteParams === null) {
regionAutocompleteParams = {query: qry};
}
$.ajax(
'/geo/regions/autocomplete',
{
data: regionAutocompleteParams
} }
).done(function (res) {
callback(res)
});
}
}
});
$('#inputAccountCountry').autoComplete({
events: {
search: function (qry, callback) {
if (countryAutocompleteParams === null) {
countryAutocompleteParams = {query: qry};
} }
});
$.ajax( /*
'/geo/countries/autocomplete', // Remove all the existing items from the timezone dropdown and repopulate it.
{ $('#selectAccountTimezone').find('option').remove().end()
data: countryAutocompleteParams $.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('<option value="'+res[c]+'">'+res[c]+'</option>');
}
}
});
*/
// Replace the existing zipcode text input with a new one that will supports autocomplete.
$('#divAccountZipcode').html('<input class="form-control {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>');
$('#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('<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" id="inputAccountZipcode" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>');
// Replace the existing region select with a text input.
$('#divAccountRegion').html('<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required>');
} }
}); }).change();
}); });
</script> </script>
{{end}} {{end}}

View File

@ -50,7 +50,7 @@ func FindCountries(ctx context.Context, dbConn *sqlx.DB, orderBy, where string,
v Country v Country
err error 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 { if err != nil {
err = errors.Wrapf(err, "query - %s", query.String()) err = errors.Wrapf(err, "query - %s", query.String())
return nil, err return nil, err

View File

@ -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
}

View File

@ -27,15 +27,15 @@ const (
var ( var (
// List of country codes that will geonames will be downloaded for. // List of country codes that will geonames will be downloaded for.
ValidGeonameCountries = []string{ ValidGeonameCountries = []string{
"AD", "AR", "AS", "AT", "AU", "AX", "BD", "BE", "BG", "BM", "AD", "AR", "AS", "AT", "AU", "AX", "BD", "BE", "BG", "BM",
"BR", "BY", "CA", "CH", "CO", "CR", "CZ", "DE", "DK", "DO", "BR", "BY", "CA", "CH", "CO", "CR", "CZ", "DE", "DK", "DO",
"DZ", "ES", "FI", "FO", "FR", "GB", "GF", "GG", "GL", "GP", "DZ", "ES", "FI", "FO", "FR", "GB", "GF", "GG", "GL", "GP",
"GT", "GU", "HR", "HU", "IE", "IM", "IN", "IS", "IT", "JE", "GT", "GU", "HR", "HU", "IE", "IM", "IN", "IS", "IT", "JE",
"JP", "LI", "LK", "LT", "LU", "LV", "MC", "MD", "MH", "MK", "JP", "LI", "LK", "LT", "LU", "LV", "MC", "MD", "MH", "MK",
"MP", "MQ", "MT", "MX", "MY", "NC", "NL", "NO", "NZ", "PH", "MP", "MQ", "MT", "MX", "MY", "NC", "NL", "NO", "NZ", "PH",
"PK", "PL", "PM", "PR", "PT", "RE", "RO", "RU", "SE", "SI", "PK", "PL", "PM", "PR", "PT", "RE", "RO", "RU", "SE", "SI",
"SJ", "SK", "SM", "TH", "TR", "UA", "US", "UY", "VA", "VI", "SJ", "SK", "SM", "TH", "TR", "UA", "US", "UY", "VA", "VI",
"WF", "YT", "ZA"} "WF", "YT", "ZA"}
) )
// FindGeonames .... // FindGeonames ....
@ -75,10 +75,10 @@ func FindGeonames(ctx context.Context, dbConn *sqlx.DB, orderBy, where string, a
v Geoname v Geoname
err error 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 { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
}else if v.PostalCode == "" { } else if v.PostalCode == "" {
continue continue
} }
@ -142,7 +142,6 @@ func FindGeonameRegions(ctx context.Context, dbConn *sqlx.DB, orderBy, where str
query.Select("distinct state_code", "state_name") query.Select("distinct state_code", "state_name")
query.From(geonamesTableName) query.From(geonamesTableName)
if orderBy == "" { if orderBy == "" {
orderBy = "state_name" orderBy = "state_name"
} }
@ -171,11 +170,11 @@ func FindGeonameRegions(ctx context.Context, dbConn *sqlx.DB, orderBy, where str
v Region v Region
err error err error
) )
err = rows.Scan(&v.Code,&v.Name) err = rows.Scan(&v.Code, &v.Name)
if err != nil { if err != nil {
err = errors.Wrapf(err, "query - %s", query.String()) err = errors.Wrapf(err, "query - %s", query.String())
return nil, err return nil, err
} else if v.Code == "" { } else if v.Code == "" || v.Name == "" {
continue continue
} }
@ -209,11 +208,11 @@ func LoadGeonames(ctx context.Context, rr chan<- interface{}, countries ...strin
// Possible types sent to the channel are limited to: // Possible types sent to the channel are limited to:
// - error // - error
// - GeoName // - 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) u := fmt.Sprintf("http://download.geonames.org/export/zip/%s.zip", country)
resp, err := pester.Get(u) resp, err := pester.Get(u)
if err != nil { 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 return
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -223,14 +222,14 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri
buff := bytes.NewBuffer([]byte{}) buff := bytes.NewBuffer([]byte{})
size, err := io.Copy(buff, br) size, err := io.Copy(buff, br)
if err != nil { if err != nil {
rr <- errors.WithStack(err) rr <- errors.WithStack(err)
return return
} }
b := bytes.NewReader(buff.Bytes()) b := bytes.NewReader(buff.Bytes())
zr, err := zip.NewReader(b, size) zr, err := zip.NewReader(b, size)
if err != nil { if err != nil {
rr <- errors.WithStack(err) rr <- errors.WithStack(err)
return return
} }
@ -241,7 +240,7 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri
fh, err := f.Open() fh, err := f.Open()
if err != nil { if err != nil {
rr <- errors.WithStack(err) rr <- errors.WithStack(err)
return return
} }
@ -260,49 +259,49 @@ func loadGeonameCountry(ctx context.Context, rr chan<- interface{}, country stri
lines, err := r.ReadAll() lines, err := r.ReadAll()
if err != nil { if err != nil {
rr <- errors.WithStack(err) rr <- errors.WithStack(err)
continue continue
} }
for _, row := range lines { for _, row := range lines {
/* /*
fmt.Println("CountryCode: row[0]", row[0]) fmt.Println("CountryCode: row[0]", row[0])
fmt.Println("PostalCode: row[1]", row[1]) fmt.Println("PostalCode: row[1]", row[1])
fmt.Println("PlaceName: row[2]", row[2]) fmt.Println("PlaceName: row[2]", row[2])
fmt.Println("StateName: row[3]", row[3]) fmt.Println("StateName: row[3]", row[3])
fmt.Println("StateCode : row[4]", row[4]) fmt.Println("StateCode : row[4]", row[4])
fmt.Println("CountyName: row[5]", row[5]) fmt.Println("CountyName: row[5]", row[5])
fmt.Println("CountyCode : row[6]", row[6]) fmt.Println("CountyCode : row[6]", row[6])
fmt.Println("CommunityName: row[7]", row[7]) fmt.Println("CommunityName: row[7]", row[7])
fmt.Println("CommunityCode: row[8]", row[8]) fmt.Println("CommunityCode: row[8]", row[8])
fmt.Println("Latitude: row[9]", row[9]) fmt.Println("Latitude: row[9]", row[9])
fmt.Println("Longitude: row[10]", row[10]) fmt.Println("Longitude: row[10]", row[10])
fmt.Println("Accuracy: row[11]", row[11]) fmt.Println("Accuracy: row[11]", row[11])
*/ */
gn := Geoname{ gn := Geoname{
CountryCode: row[0], CountryCode: row[0],
PostalCode: row[1], PostalCode: row[1],
PlaceName: row[2], PlaceName: row[2],
StateName: row[3], StateName: row[3],
StateCode : row[4], StateCode: row[4],
CountyName: row[5], CountyName: row[5],
CountyCode : row[6], CountyCode: row[6],
CommunityName: row[7], CommunityName: row[7],
CommunityCode: row[8], CommunityCode: row[8],
} }
if row[9] != "" { if row[9] != "" {
gn.Latitude, err = decimal.NewFromString(row[9]) gn.Latitude, err = decimal.NewFromString(row[9])
if err != nil { if err != nil {
rr <- errors.WithStack(err) rr <- errors.WithStack(err)
} }
} }
if row[10] != "" { if row[10] != "" {
gn.Longitude, err = decimal.NewFromString(row[10]) gn.Longitude, err = decimal.NewFromString(row[10])
if err != nil { 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 { if err := scanner.Err(); err != nil {
rr <- errors.WithStack(err) rr <- errors.WithStack(err)
} }
} }
} }

View File

@ -3,35 +3,38 @@ package geonames
import "github.com/shopspring/decimal" import "github.com/shopspring/decimal"
type Geoname struct { type Geoname struct {
CountryCode string // US CountryCode string // US
PostalCode string // 99686 PostalCode string // 99686
PlaceName string // Valdez PlaceName string // Valdez
StateName string // Alaska StateName string // Alaska
StateCode string // AK StateCode string // AK
CountyName string // Valdez-Cordova CountyName string // Valdez-Cordova
CountyCode string // 261 CountyCode string // 261
CommunityName string // CommunityName string //
CommunityCode string // CommunityCode string //
Latitude decimal.Decimal // 61.101 Latitude decimal.Decimal // 61.101
Longitude decimal.Decimal // -146.9 Longitude decimal.Decimal // -146.9
Accuracy int // 1 Accuracy int // 1
} }
type Country struct { type Country struct {
Code string // US Code string // US
Name string Name string
IsoAlpha3 string IsoAlpha3 string
Capital string Capital string
CurrencyCode string // .us CurrencyCode string // .us
CurrencyName string // USD Dollar CurrencyName string // USD Dollar
Phone string // 1 Phone string // 1
PostalCodeFormat string // #####-#### PostalCodeFormat string // #####-####
PostalCodeRegex string // ^\d{5}(-\d{4})?$ PostalCodeRegex string // ^\d{5}(-\d{4})?$
} }
type Region struct { type Region struct {
Code string // AK Code string // AK
Name string // Alaska Name string // Alaska
} }
type CountryTimezone struct {
CountryCode string // US
TimezoneId string // America/Anchorage
}

View File

@ -9,11 +9,11 @@ import (
"strings" "strings"
"geeks-accelerator/oss/saas-starter-kit/internal/geonames" "geeks-accelerator/oss/saas-starter-kit/internal/geonames"
"github.com/sethgrid/pester"
"github.com/geeks-accelerator/sqlxmigrate" "github.com/geeks-accelerator/sqlxmigrate"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sethgrid/pester"
) )
// migrationList returns a list of migrations to be executed. If the id of the // 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 { for r := range resChan {
switch v := r.(type) { switch v := r.(type) {
case geonames.Geoname: 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 { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
case error : case error:
return v return v
} }
} }
@ -338,7 +338,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
} }
// Pull the last comment to load the fields. // Pull the last comment to load the fields.
if stmt == nil { if stmt == nil {
prevLine = strings.TrimPrefix(prevLine, "#") prevLine = strings.TrimPrefix(prevLine, "#")
r := csv.NewReader(strings.NewReader(prevLine)) r := csv.NewReader(strings.NewReader(prevLine))
r.Comma = '\t' // Use tab-delimited instead of comma <---- here! 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" cn = "neighbours"
case "EquivalentFipsCode": case "EquivalentFipsCode":
cn = "equivalent_fips_code" cn = "equivalent_fips_code"
default : default:
return errors.Errorf("Failed to map column %s", fn) return errors.Errorf("Failed to map column %s", fn)
} }
columns = append(columns, cn) columns = append(columns, cn)
@ -402,7 +402,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
placeholders = append(placeholders, "?") 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) q = db.Rebind(q)
stmt, err = db.Prepare(q) stmt, err = db.Prepare(q)
if err != nil { if err != nil {
@ -468,5 +468,75 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return nil 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
},
},
} }
} }