* 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 everythingmaster
@@ -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"), | |||||
"name": "newUsername", | |||||
"email": "user2@example.com", | |||||
"_csrf": GetCSRF(t, session, "/user/settings"), | |||||
"name": "newUsername", | |||||
"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"), | |||||
"name": reservedUsername, | |||||
"email": "user2@example.com", | |||||
"_csrf": GetCSRF(t, session, "/user/settings"), | |||||
"name": reservedUsername, | |||||
"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", | |||||
"revisionTime": "2018-04-17T00:54:29Z" | |||||
"revision": "1c8d12f79a51605ed91587aa6b86cf38fc0f987f", | |||||
"revisionTime": "2018-05-01T11:15:19Z" | |||||
}, | }, | ||||
{ | { | ||||
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", | "checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", | ||||