Provide datalist for zoneinfo and locale

This commit is contained in:
Melon 2023-09-07 16:34:51 +01:00
parent ece74ea36a
commit 65f77dbe79
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 235 additions and 24 deletions

View File

@ -27,7 +27,7 @@ type NullStringScanner struct{ sql.NullString }
func (s *NullStringScanner) Null() bool { return !s.Valid } func (s *NullStringScanner) Null() bool { return !s.Valid }
func (s *NullStringScanner) Scan(src any) error { return s.NullString.Scan(src) } func (s *NullStringScanner) Scan(src any) error { return s.NullString.Scan(src) }
func (s NullStringScanner) MarshalJSON() ([]byte, error) { func (s NullStringScanner) MarshalJSON() ([]byte, error) {
return marshalValueOrNull(s.Null(), s.String) return marshalValueOrNull(s.Null(), s.NullString.String)
} }
func (s *NullStringScanner) UnmarshalJSON(bytes []byte) error { func (s *NullStringScanner) UnmarshalJSON(bytes []byte) error {
if string(bytes) == "null" { if string(bytes) == "null" {
@ -40,6 +40,12 @@ func (s *NullStringScanner) UnmarshalJSON(bytes []byte) error {
} }
return s.Scan(&a) return s.Scan(&a)
} }
func (s NullStringScanner) String() string {
if s.Null() {
return ""
}
return s.NullString.String
}
type NullDateScanner struct{ sql.NullTime } type NullDateScanner struct{ sql.NullTime }
@ -59,6 +65,12 @@ func (t *NullDateScanner) UnmarshalJSON(bytes []byte) error {
} }
return t.Scan(&a) return t.Scan(&a)
} }
func (t NullDateScanner) String() string {
if t.Null() {
return ""
}
return t.NullTime.Time.UTC().Format(time.DateOnly)
}
type LocationScanner struct{ *time.Location } type LocationScanner struct{ *time.Location }

View File

@ -9,6 +9,10 @@ import (
"time" "time"
) )
func updatedAt() string {
return time.Now().UTC().Format(time.DateTime)
}
type Tx struct{ tx *sql.Tx } type Tx struct{ tx *sql.Tx }
func (t *Tx) Commit() error { func (t *Tx) Commit() error {
@ -37,7 +41,7 @@ func (t *Tx) InsertUser(name, un, pw, email string) error {
if err != nil { if err != nil {
return err return err
} }
_, err = t.tx.Exec(`INSERT INTO users (subject, name, username, password, email) VALUES (?, ?, ?, ?, ?)`, uuid.NewString(), name, un, pwHash, email) _, err = t.tx.Exec(`INSERT INTO users (subject, name, username, password, email, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, uuid.NewString(), name, un, pwHash, email, updatedAt())
return err return err
} }
@ -96,7 +100,7 @@ func (t *Tx) ChangeUserPassword(sub uuid.UUID, pwOld, pwNew string) error {
if err != nil { if err != nil {
return err return err
} }
exec, err := t.tx.Exec(`UPDATE users SET password = ?, updated_at = ? WHERE subject = ? AND password = ?`, pwNewHash, time.Now().Format(time.DateTime), sub, pwHash) exec, err := t.tx.Exec(`UPDATE users SET password = ?, updated_at = ? WHERE subject = ? AND password = ?`, pwNewHash, updatedAt(), sub, pwHash)
if err != nil { if err != nil {
return err return err
} }
@ -129,7 +133,7 @@ WHERE subject = ?`,
v.Birthdate, v.Birthdate,
v.ZoneInfo.String(), v.ZoneInfo.String(),
v.Locale.String(), v.Locale.String(),
time.Now().Format(time.DateTime), updatedAt(),
sub, sub,
) )
if err != nil { if err != nil {

View File

@ -16,7 +16,7 @@ func TestTx_ChangeUserPassword(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
d, err := Open("file::memory:") d, err := Open("file::memory:")
assert.NoError(t, err) assert.NoError(t, err)
_, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email) VALUES (?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost") _, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost", updatedAt())
assert.NoError(t, err) assert.NoError(t, err)
tx, err := d.Begin() tx, err := d.Begin()
assert.NoError(t, err) assert.NoError(t, err)
@ -39,7 +39,7 @@ func TestTx_ModifyUser(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
d, err := Open("file::memory:") d, err := Open("file::memory:")
assert.NoError(t, err) assert.NoError(t, err)
_, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email) VALUES (?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost") _, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost", updatedAt())
assert.NoError(t, err) assert.NoError(t, err)
tx, err := d.Begin() tx, err := d.Begin()
assert.NoError(t, err) assert.NoError(t, err)

104
lists/locales.go Normal file
View File

@ -0,0 +1,104 @@
package lists
import (
"golang.org/x/text/language"
"golang.org/x/text/language/display"
"sync"
)
var (
localeOnce sync.Once
localeNames []struct{ Value, Label string }
)
func ListLocale() []struct{ Value, Label string } {
localeOnce.Do(func() {
localeNames = make([]struct{ Value, Label string }, len(localeList))
for i := range localeList {
localeNames[i] = struct{ Value, Label string }{Value: localeList[i].String(), Label: display.Self.Name(localeList[i])}
}
})
return localeNames
}
var localeList = []language.Tag{
language.Afrikaans,
language.Amharic,
language.Arabic,
language.ModernStandardArabic,
language.Azerbaijani,
language.Bulgarian,
language.Bengali,
language.Catalan,
language.Czech,
language.Danish,
language.German,
language.Greek,
language.English,
language.AmericanEnglish,
language.BritishEnglish,
language.Spanish,
language.EuropeanSpanish,
language.LatinAmericanSpanish,
language.Estonian,
language.Persian,
language.Finnish,
language.Filipino,
language.French,
language.CanadianFrench,
language.Gujarati,
language.Hebrew,
language.Hindi,
language.Croatian,
language.Hungarian,
language.Armenian,
language.Indonesian,
language.Icelandic,
language.Italian,
language.Japanese,
language.Georgian,
language.Kazakh,
language.Khmer,
language.Kannada,
language.Korean,
language.Kirghiz,
language.Lao,
language.Lithuanian,
language.Latvian,
language.Macedonian,
language.Malayalam,
language.Mongolian,
language.Marathi,
language.Malay,
language.Burmese,
language.Nepali,
language.Dutch,
language.Norwegian,
language.Punjabi,
language.Polish,
language.Portuguese,
language.BrazilianPortuguese,
language.EuropeanPortuguese,
language.Romanian,
language.Russian,
language.Sinhala,
language.Slovak,
language.Slovenian,
language.Albanian,
language.Serbian,
language.SerbianLatin,
language.Swedish,
language.Swahili,
language.Tamil,
language.Telugu,
language.Thai,
language.Turkish,
language.Ukrainian,
language.Urdu,
language.Uzbek,
language.Vietnamese,
language.Chinese,
language.SimplifiedChinese,
language.TraditionalChinese,
language.Zulu,
}

15
lists/locales_test.go Normal file
View File

@ -0,0 +1,15 @@
package lists
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestListLocale(t *testing.T) {
locales := ListLocale()
assert.True(t, len(locales) > 4)
assert.Equal(t, struct{ Value, Label string }{Value: "af", Label: "Afrikaans"}, locales[0])
assert.Equal(t, struct{ Value, Label string }{Value: "am", Label: "አማርኛ"}, locales[1])
assert.Equal(t, struct{ Value, Label string }{Value: "zh-Hant", Label: "繁體中文"}, locales[len(locales)-2])
assert.Equal(t, struct{ Value, Label string }{Value: "zu", Label: "isiZulu"}, locales[len(locales)-1])
}

53
lists/zoneinfo.go Normal file
View File

@ -0,0 +1,53 @@
package lists
import (
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
var (
zoneDirs = []string{
// Update path according to your OS
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
}
zoneInfoOnce sync.Once
zoneNames []string
)
func ListZoneInfo() []string {
zoneInfoOnce.Do(func() {
zoneNames = make([]string, 0)
for _, zoneDir := range zoneDirs {
zoneNames = append(zoneNames, FindTimeZoneFiles(zoneDir)...)
}
sort.Strings(zoneNames)
})
return zoneNames
}
func FindTimeZoneFiles(zoneDir string) []string {
dArr := make([]string, 0)
dArr = append(dArr, "")
arr := make([]string, 0)
for i := 0; i < len(dArr); i++ {
dir := dArr[i]
files, _ := os.ReadDir(filepath.Join(zoneDir, dir))
for _, f := range files {
if f.Name() != strings.ToUpper(f.Name()[:1])+f.Name()[1:] {
continue
}
if f.IsDir() {
dArr = append(dArr, filepath.Join(dir, f.Name()))
} else {
arr = append(arr, filepath.Join(dir, f.Name()))
}
}
}
return arr
}

15
lists/zoneinfo_test.go Normal file
View File

@ -0,0 +1,15 @@
package lists
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestListZoneInfo(t *testing.T) {
zoneinfos := ListZoneInfo()
assert.True(t, len(zoneinfos) > 4)
assert.Equal(t, "Africa/Abidjan", zoneinfos[0])
assert.Equal(t, "Africa/Accra", zoneinfos[1])
assert.Equal(t, "WET", zoneinfos[len(zoneinfos)-2])
assert.Equal(t, "Zulu", zoneinfos[len(zoneinfos)-1])
}

View File

@ -37,15 +37,16 @@
</div> </div>
<div> <div>
<label for="field_birthdate">Birthdate</label> <label for="field_birthdate">Birthdate</label>
<input type="text" name="birthdate" id="field_birthdate" value="{{.User.Birthdate}}"> <input type="date" name="birthdate" id="field_birthdate" value="{{.User.Birthdate}}">
<label>Reset? <input type="checkbox" name="reset_birthdate"></label> <label>Reset? <input type="checkbox" name="reset_birthdate"></label>
</div> </div>
<div> <div>
<label for="field_zoneinfo">Time Zone</label> <label for="field_zoneinfo">Time Zone</label>
<input type="text" name="zoneinfo" id="field_zoneinfo" value="{{.User.ZoneInfo}}" list="list_zoneinfo"> <input type="text" name="zoneinfo" id="field_zoneinfo" value="{{.User.ZoneInfo}}" list="list_zoneinfo">
<datalist id="list_zoneinfo"> <datalist id="list_zoneinfo">
<!-- Fill in --> {{range .ListZoneInfo}}
<option value="Europe/London"></option> <option value="{{.}}"></option>
{{end}}
</datalist> </datalist>
<label>Reset? <input type="checkbox" name="reset_zoneinfo"></label> <label>Reset? <input type="checkbox" name="reset_zoneinfo"></label>
</div> </div>
@ -53,8 +54,9 @@
<label for="field_locale">Language</label> <label for="field_locale">Language</label>
<input type="text" name="locale" id="field_locale" value="{{.User.Locale}}" list="list_locale"> <input type="text" name="locale" id="field_locale" value="{{.User.Locale}}" list="list_locale">
<datalist id="list_locale"> <datalist id="list_locale">
<!-- Fill in --> {{range .ListLocale}}
<option value="en-US"></option> <option value="{{.Value}}">{{.Label}}</option>
{{end}}
</datalist> </datalist>
<label>Reset? <input type="checkbox" name="reset_zoneinfo"></label> <label>Reset? <input type="checkbox" name="reset_zoneinfo"></label>
</div> </div>

View File

@ -8,6 +8,7 @@ import (
errors2 "errors" errors2 "errors"
"fmt" "fmt"
"github.com/1f349/tulip/database" "github.com/1f349/tulip/database"
"github.com/1f349/tulip/lists"
"github.com/1f349/tulip/openid" "github.com/1f349/tulip/openid"
"github.com/1f349/tulip/pages" "github.com/1f349/tulip/pages"
"github.com/go-oauth2/oauth2/v4" "github.com/go-oauth2/oauth2/v4"
@ -112,20 +113,23 @@ func NewHttpServer(listen, domain string, db *database.DB, privKey []byte, clien
return return
} }
hs.DbTx(rw, func(tx *database.Tx) error { var userWithName *database.User
userWithName, err := tx.GetUserDisplayName(auth.ID) if hs.DbTx(rw, func(tx *database.Tx) (err error) {
userWithName, err = tx.GetUserDisplayName(auth.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get user display name: %w", err) return fmt.Errorf("failed to get user display name: %w", err)
} }
if err := pages.RenderPageTemplate(rw, "index", map[string]any{ return
"Auth": auth, }) {
"User": userWithName, return
"Nonce": lNonce, }
}); err != nil { if err := pages.RenderPageTemplate(rw, "index", map[string]any{
log.Printf("Failed to render page: edit: %s\n", err) "Auth": auth,
} "User": userWithName,
return nil "Nonce": lNonce,
}) }); err != nil {
log.Printf("Failed to render page: edit: %s\n", err)
}
})) }))
r.POST("/logout", hs.RequireAuthentication("403 Forbidden", http.StatusForbidden, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) { r.POST("/logout", hs.RequireAuthentication("403 Forbidden", http.StatusForbidden, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
lNonce, ok := auth.Session.Get("action-nonce") lNonce, ok := auth.Session.Get("action-nonce")
@ -228,8 +232,10 @@ func NewHttpServer(listen, domain string, db *database.DB, privKey []byte, clien
return return
} }
if err := pages.RenderPageTemplate(rw, "edit", map[string]any{ if err := pages.RenderPageTemplate(rw, "edit", map[string]any{
"User": user, "User": user,
"Nonce": lNonce, "Nonce": lNonce,
"ListZoneInfo": lists.ListZoneInfo(),
"ListLocale": lists.ListLocale(),
}); err != nil { }); err != nil {
log.Printf("Failed to render page: edit: %s\n", err) log.Printf("Failed to render page: edit: %s\n", err)
} }