Added user language setting (#3875)
* Added user language setting * Added translation string for setting * Fixed import order + typo * improved checking if the user has a language saved in the db * The current saved language is now set a default inside the dropdown * fmt * When a user signs in and doesn't have a language saved, the current browser language is saved * updated gitea-sdk * Merge branch 'master' of https://github.com/go-gitea/gitea into save-user-language # Conflicts: # models/migrations/migrations.go # models/migrations/v62.go * Made tests work again * trigger CI * trigger CI * fmt * re-trigger that FUCKING CI SO IT REALLY PICKS UP THE LATEST COMMIT ISTEAD OF PREDENDING TO DO SO * re-trigger that FUCKING CI SO IT REALLY PICKS UP THE LATEST COMMIT ISTEAD OF PREDENDING TO DO SO * When loggin in, only the language col gets updated instead of everythingrelease/v1.15
parent
795dcc8ecf
commit
1fdf560678
|
@ -27,9 +27,10 @@ func TestRenameUsername(t *testing.T) {
|
||||||
|
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||||
"_csrf": GetCSRF(t, session, "/user/settings"),
|
"_csrf": GetCSRF(t, session, "/user/settings"),
|
||||||
"name": "newUsername",
|
"name": "newUsername",
|
||||||
"email": "user2@example.com",
|
"email": "user2@example.com",
|
||||||
|
"language": "en-us",
|
||||||
})
|
})
|
||||||
session.MakeRequest(t, req, http.StatusFound)
|
session.MakeRequest(t, req, http.StatusFound)
|
||||||
|
|
||||||
|
@ -81,9 +82,10 @@ func TestRenameReservedUsername(t *testing.T) {
|
||||||
for _, reservedUsername := range reservedUsernames {
|
for _, reservedUsername := range reservedUsernames {
|
||||||
t.Logf("Testing username %s", reservedUsername)
|
t.Logf("Testing username %s", reservedUsername)
|
||||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||||
"_csrf": GetCSRF(t, session, "/user/settings"),
|
"_csrf": GetCSRF(t, session, "/user/settings"),
|
||||||
"name": reservedUsername,
|
"name": reservedUsername,
|
||||||
"email": "user2@example.com",
|
"email": "user2@example.com",
|
||||||
|
"language": "en-us",
|
||||||
})
|
})
|
||||||
resp := session.MakeRequest(t, req, http.StatusFound)
|
resp := session.MakeRequest(t, req, http.StatusFound)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ func TestXSSUserFullName(t *testing.T) {
|
||||||
"name": user.Name,
|
"name": user.Name,
|
||||||
"full_name": fullName,
|
"full_name": fullName,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
|
"language": "en-us",
|
||||||
})
|
})
|
||||||
session.MakeRequest(t, req, http.StatusFound)
|
session.MakeRequest(t, req, http.StatusFound)
|
||||||
|
|
||||||
|
|
|
@ -178,6 +178,8 @@ var migrations = []Migration{
|
||||||
NewMigration("add size column for attachments", addSizeToAttachment),
|
NewMigration("add size column for attachments", addSizeToAttachment),
|
||||||
// v62 -> v63
|
// v62 -> v63
|
||||||
NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP),
|
NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP),
|
||||||
|
// v63 -> v64
|
||||||
|
NewMigration("add language column for user setting", addLanguageSetting),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addLanguageSetting(x *xorm.Engine) error {
|
||||||
|
type User struct {
|
||||||
|
Language string `xorm:"VARCHAR(5)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync2(new(User)); err != nil {
|
||||||
|
return fmt.Errorf("Sync2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -94,6 +94,7 @@ type User struct {
|
||||||
Website string
|
Website string
|
||||||
Rands string `xorm:"VARCHAR(10)"`
|
Rands string `xorm:"VARCHAR(10)"`
|
||||||
Salt string `xorm:"VARCHAR(10)"`
|
Salt string `xorm:"VARCHAR(10)"`
|
||||||
|
Language string `xorm:"VARCHAR(5)"`
|
||||||
|
|
||||||
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
@ -185,6 +186,7 @@ func (u *User) APIFormat() *api.User {
|
||||||
FullName: u.FullName,
|
FullName: u.FullName,
|
||||||
Email: u.getEmail(),
|
Email: u.getEmail(),
|
||||||
AvatarURL: u.AvatarLink(),
|
AvatarURL: u.AvatarLink(),
|
||||||
|
Language: u.Language,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,7 @@ type UpdateProfileForm struct {
|
||||||
KeepEmailPrivate bool
|
KeepEmailPrivate bool
|
||||||
Website string `binding:"ValidUrl;MaxSize(255)"`
|
Website string `binding:"ValidUrl;MaxSize(255)"`
|
||||||
Location string `binding:"MaxSize(50)"`
|
Location string `binding:"MaxSize(50)"`
|
||||||
|
Language string `binding:"Size(5)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|
|
@ -331,6 +331,7 @@ change_username = Your username has been changed.
|
||||||
change_username_prompt = Note: username changes also change your account URL.
|
change_username_prompt = Note: username changes also change your account URL.
|
||||||
continue = Continue
|
continue = Continue
|
||||||
cancel = Cancel
|
cancel = Cancel
|
||||||
|
language = Language
|
||||||
|
|
||||||
lookup_avatar_by_mail = Look Up Avatar by Email Address
|
lookup_avatar_by_mail = Look Up Avatar by Email Address
|
||||||
federated_avatar_lookup = Federated Avatar Lookup
|
federated_avatar_lookup = Federated Avatar Lookup
|
||||||
|
|
|
@ -7318,6 +7318,11 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "ID"
|
"x-go-name": "ID"
|
||||||
},
|
},
|
||||||
|
"language": {
|
||||||
|
"description": "User locale",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Language"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"description": "the user's username",
|
"description": "the user's username",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
@ -339,6 +339,18 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
|
||||||
ctx.Session.Set("uid", u.ID)
|
ctx.Session.Set("uid", u.ID)
|
||||||
ctx.Session.Set("uname", u.Name)
|
ctx.Session.Set("uname", u.Name)
|
||||||
|
|
||||||
|
// Language setting of the user overwrites the one previously set
|
||||||
|
// If the user does not have a locale set, we save the current one.
|
||||||
|
if len(u.Language) == 0 {
|
||||||
|
u.Language = ctx.Locale.Language()
|
||||||
|
if err := models.UpdateUserCols(u, "language"); err != nil {
|
||||||
|
log.Error(4, fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetCookie("lang", u.Language, nil, setting.AppSubURL)
|
||||||
|
|
||||||
// Clear whatever CSRF has right now, force to generate a new one
|
// Clear whatever CSRF has right now, force to generate a new one
|
||||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
||||||
|
|
||||||
|
@ -704,6 +716,7 @@ func SignOut(ctx *context.Context) {
|
||||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL)
|
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL)
|
||||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL)
|
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL)
|
||||||
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL)
|
||||||
|
ctx.SetCookie("lang", "", -1, setting.AppSubURL) // Setting the lang cookie will trigger the middleware to reset the language ot previous state.
|
||||||
ctx.Redirect(setting.AppSubURL + "/")
|
ctx.Redirect(setting.AppSubURL + "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/Unknwon/i18n"
|
||||||
"github.com/pquerna/otp"
|
"github.com/pquerna/otp"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
@ -105,6 +106,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) {
|
||||||
ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
|
ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
|
||||||
ctx.User.Website = form.Website
|
ctx.User.Website = form.Website
|
||||||
ctx.User.Location = form.Location
|
ctx.User.Location = form.Location
|
||||||
|
ctx.User.Language = form.Language
|
||||||
if err := models.UpdateUserSetting(ctx.User); err != nil {
|
if err := models.UpdateUserSetting(ctx.User); err != nil {
|
||||||
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
|
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
|
||||||
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
|
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
|
||||||
|
@ -115,8 +117,11 @@ func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the language to the one we just set
|
||||||
|
ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL)
|
||||||
|
|
||||||
log.Trace("User settings updated: %s", ctx.User.Name)
|
log.Trace("User settings updated: %s", ctx.User.Name)
|
||||||
ctx.Flash.Success(ctx.Tr("settings.update_profile_success"))
|
ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,20 @@
|
||||||
<input id="location" name="location" value="{{.SignedUser.Location}}">
|
<input id="location" name="location" value="{{.SignedUser.Location}}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="language">{{.i18n.Tr "settings.language"}}</label>
|
||||||
|
<div class="ui language selection dropdown" id="language">
|
||||||
|
<input name="language" type="hidden">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
|
||||||
|
<div class="menu">
|
||||||
|
{{range .AllLangs}}
|
||||||
|
<div class="item{{if eq $.SignedUser.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
|
<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,6 +22,8 @@ type User struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
// URL to the user's avatar
|
// URL to the user's avatar
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
|
// User locale
|
||||||
|
Language string `json:"language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for User, adding field(s) for backward compatibility
|
// MarshalJSON implements the json.Marshaler interface for User, adding field(s) for backward compatibility
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
"revisionTime": "2018-04-21T01:08:19Z"
|
"revisionTime": "2018-04-21T01:08:19Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "xXzi8Xx7HA3M0z3lR/1wr1Vz1fc=",
|
"checksumSHA1": "WMD6+Qh2+5hd9uiq910pF/Ihylw=",
|
||||||
"path": "code.gitea.io/sdk/gitea",
|
"path": "code.gitea.io/sdk/gitea",
|
||||||
"revision": "142acef5ce79f78585afcce31748af46c72a3dea",
|
"revision": "1c8d12f79a51605ed91587aa6b86cf38fc0f987f",
|
||||||
"revisionTime": "2018-04-17T00:54:29Z"
|
"revisionTime": "2018-05-01T11:15:19Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=",
|
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=",
|
||||||
|
|
Loading…
Reference in New Issue