You've already forked golang-saas-starter-kit
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:
@ -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{}
|
||||||
|
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 {
|
for _, c := range res {
|
||||||
list = append(list, c.Name)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +75,9 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
|||||||
}
|
}
|
||||||
// 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{
|
||||||
|
@ -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;
|
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
// Replace the existing region with an empty dropdown.
|
||||||
|
$('#divAccountRegion').html('<select class="form-control {{ ValidationFieldClass $.validationErrors "Account.Region" }}" id="inputAccountRegion" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required></select>');
|
||||||
|
|
||||||
|
// 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('<option value="'+res[c].value+'">'+res[c].text+'</option>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 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('<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() {
|
$('#inputAccountZipcode').on('change', function() {
|
||||||
console.log($(this).val());
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
regionAutocompleteParams = {postal_code: $(this).val()};
|
contentType: 'application/json',
|
||||||
$('#inputAccountRegion').keyup();
|
url: '/geo/geonames/postal_code/'+$(this).val(),
|
||||||
|
data: {country_code: $('#selectAccountCountry').val()},
|
||||||
countryAutocompleteParams = {postal_code: $(this).val()};
|
dataType: 'json'
|
||||||
$('#inputAccountCountry').keyup();
|
}).done(function (res) {
|
||||||
});
|
if (res !== undefined && res !== null && res.PostalCode !== undefined) {
|
||||||
|
$('#inputAccountCity').val(res.PlaceName);
|
||||||
$('#inputAccountRegion').on('keydown', function () {
|
$('#inputAccountRegion').val(res.StateCode);
|
||||||
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
|
|
||||||
}
|
|
||||||
).done(function (res) {
|
|
||||||
callback(res)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#inputAccountCountry').autoComplete({
|
|
||||||
events: {
|
|
||||||
search: function (qry, callback) {
|
|
||||||
if (countryAutocompleteParams === null) {
|
|
||||||
countryAutocompleteParams = {query: qry};
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax(
|
|
||||||
'/geo/countries/autocomplete',
|
|
||||||
{
|
|
||||||
data: countryAutocompleteParams
|
|
||||||
}
|
|
||||||
).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}}
|
65
internal/geonames/country_timezones.go
Normal file
65
internal/geonames/country_timezones.go
Normal 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
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
@ -175,7 +174,7 @@ func FindGeonameRegions(ctx context.Context, dbConn *sqlx.DB, orderBy, where str
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,9 +29,12 @@ type Country struct {
|
|||||||
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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
@ -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
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user