| @@ -71,7 +71,6 @@ There are 5 ways to install Gogs: | |||
| - Router and middleware mechanism of [Macaron](https://github.com/Unknwon/macaron). | |||
| - Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). | |||
| - System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog). | |||
| - Usage and modification from [beego](http://beego.me) modules. | |||
| - Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo. | |||
| - Thanks [gobuild.io](http://gobuild.io) for providing binary compile and download service. | |||
| - Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan. | |||
| @@ -59,8 +59,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自 | |||
| ## 特别鸣谢 | |||
| - [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。 | |||
| - [beego](http://beego.me) 模块的使用与修改。 | |||
| - 基于 [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。 | |||
| - 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。 | |||
| - 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。 | |||
| - 感谢 [gobuild.io](http://gobuild.io) 提供二进制编译与下载服务。 | |||
| @@ -94,6 +94,13 @@ func newMacaron() *macaron.Macaron { | |||
| SkipLogging: !setting.DisableRouterLog, | |||
| }, | |||
| )) | |||
| m.Use(macaron.Static( | |||
| setting.AvatarUploadPath, | |||
| macaron.StaticOptions{ | |||
| Prefix: "avatars", | |||
| SkipLogging: !setting.DisableRouterLog, | |||
| }, | |||
| )) | |||
| m.Use(macaron.Renderer(macaron.RenderOptions{ | |||
| Directory: path.Join(setting.StaticRootPath, "templates"), | |||
| Funcs: []template.FuncMap{base.TemplateFuncs}, | |||
| @@ -214,6 +221,7 @@ func runWeb(*cli.Context) { | |||
| m.Group("/user/settings", func() { | |||
| m.Get("", user.Settings) | |||
| m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) | |||
| m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar) | |||
| m.Get("/password", user.SettingsPassword) | |||
| m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost) | |||
| m.Get("/ssh", user.SettingsSSHKeys) | |||
| @@ -167,6 +167,7 @@ SESSION_LIFE_TIME = 86400 | |||
| [picture] | |||
| ; The place to picture data, either "server" or "qiniu", default is "server" | |||
| SERVICE = server | |||
| AVATAR_UPLOAD_PATH = data/avatars | |||
| ; Chinese users can choose "duoshuo" | |||
| GRAVATAR_SOURCE = gravatar | |||
| DISABLE_GRAVATAR = false | |||
| @@ -173,6 +173,7 @@ target_branch_not_exist = Target branch does not exist | |||
| [user] | |||
| change_avatar = Change your avatar at gravatar.com | |||
| change_custom_avatar = Change your avatar in settings | |||
| join_on = Joined on | |||
| repositories = Repositories | |||
| activity = Public Activity | |||
| @@ -201,6 +202,10 @@ change_username = Username Changed | |||
| change_username_desc = Username has been changed, do you want to continue? This will affect all links relate to your account. | |||
| continue = Continue | |||
| cancel = Cancel | |||
| choose_new_avatar = Choose new avatar | |||
| upload_avatar = Upload Avatar | |||
| uploaded_avatar_not_a_image = Uploaded file is not a image | |||
| upload_avatar_success = Your new avatar has been uploaded successfully. | |||
| change_password = Change Password | |||
| old_password = Current Password | |||
| @@ -17,7 +17,7 @@ import ( | |||
| "github.com/gogits/gogs/modules/setting" | |||
| ) | |||
| const APP_VER = "0.5.8.1119 Beta" | |||
| const APP_VER = "0.5.8.1121 Beta" | |||
| func init() { | |||
| runtime.GOMAXPROCS(runtime.NumCPU()) | |||
| @@ -58,6 +58,7 @@ type Action struct { | |||
| ActUserId int64 // Action user id. | |||
| ActUserName string // Action user name. | |||
| ActEmail string | |||
| ActAvatar string `xorm:"-"` | |||
| RepoId int64 | |||
| RepoUserName string | |||
| RepoName string | |||
| @@ -5,17 +5,21 @@ | |||
| package models | |||
| import ( | |||
| "bytes" | |||
| "container/list" | |||
| "crypto/sha256" | |||
| "encoding/hex" | |||
| "errors" | |||
| "fmt" | |||
| "image" | |||
| "image/jpeg" | |||
| "os" | |||
| "path/filepath" | |||
| "strings" | |||
| "time" | |||
| "github.com/Unknwon/com" | |||
| "github.com/nfnt/resize" | |||
| "github.com/gogits/gogs/modules/base" | |||
| "github.com/gogits/gogs/modules/git" | |||
| @@ -45,33 +49,40 @@ var ( | |||
| // User represents the object of individual and member of organization. | |||
| type User struct { | |||
| Id int64 | |||
| LowerName string `xorm:"UNIQUE NOT NULL"` | |||
| Name string `xorm:"UNIQUE NOT NULL"` | |||
| FullName string | |||
| Email string `xorm:"UNIQUE NOT NULL"` | |||
| Passwd string `xorm:"NOT NULL"` | |||
| LoginType LoginType | |||
| LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| LoginName string | |||
| Type UserType | |||
| Orgs []*User `xorm:"-"` | |||
| Repos []*Repository `xorm:"-"` | |||
| Id int64 | |||
| LowerName string `xorm:"UNIQUE NOT NULL"` | |||
| Name string `xorm:"UNIQUE NOT NULL"` | |||
| FullName string | |||
| Email string `xorm:"UNIQUE NOT NULL"` | |||
| Passwd string `xorm:"NOT NULL"` | |||
| LoginType LoginType | |||
| LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| LoginName string | |||
| Type UserType | |||
| Orgs []*User `xorm:"-"` | |||
| Repos []*Repository `xorm:"-"` | |||
| Location string | |||
| Website string | |||
| Rands string `xorm:"VARCHAR(10)"` | |||
| Salt string `xorm:"VARCHAR(10)"` | |||
| Created time.Time `xorm:"CREATED"` | |||
| Updated time.Time `xorm:"UPDATED"` | |||
| // Permissions. | |||
| IsActive bool | |||
| IsAdmin bool | |||
| AllowGitHook bool | |||
| // Avatar. | |||
| Avatar string `xorm:"VARCHAR(2048) NOT NULL"` | |||
| AvatarEmail string `xorm:"NOT NULL"` | |||
| UseCustomAvatar bool | |||
| // Counters. | |||
| NumFollowers int | |||
| NumFollowings int | |||
| NumStars int | |||
| NumRepos int | |||
| Avatar string `xorm:"VARCHAR(2048) NOT NULL"` | |||
| AvatarEmail string `xorm:"NOT NULL"` | |||
| Location string | |||
| Website string | |||
| IsActive bool | |||
| IsAdmin bool | |||
| AllowGitHook bool | |||
| Rands string `xorm:"VARCHAR(10)"` | |||
| Salt string `xorm:"VARCHAR(10)"` | |||
| Created time.Time `xorm:"CREATED"` | |||
| Updated time.Time `xorm:"UPDATED"` | |||
| // For organization. | |||
| Description string | |||
| @@ -96,9 +107,12 @@ func (u *User) HomeLink() string { | |||
| // AvatarLink returns user gravatar link. | |||
| func (u *User) AvatarLink() string { | |||
| if setting.DisableGravatar { | |||
| switch { | |||
| case u.UseCustomAvatar: | |||
| return setting.AppSubUrl + "/avatars/" + com.ToStr(u.Id) | |||
| case setting.DisableGravatar: | |||
| return setting.AppSubUrl + "/img/avatar_default.jpg" | |||
| } else if setting.Service.EnableCacheAvatar { | |||
| case setting.Service.EnableCacheAvatar: | |||
| return setting.AppSubUrl + "/avatar/" + u.Avatar | |||
| } | |||
| return setting.GravatarSource + u.Avatar | |||
| @@ -126,6 +140,43 @@ func (u *User) ValidtePassword(passwd string) bool { | |||
| return u.Passwd == newUser.Passwd | |||
| } | |||
| // UploadAvatar saves custom avatar for user. | |||
| // FIXME: splite uploads to different subdirs in case we have massive users. | |||
| func (u *User) UploadAvatar(data []byte) error { | |||
| savePath := filepath.Join(setting.AvatarUploadPath, com.ToStr(u.Id)) | |||
| u.UseCustomAvatar = true | |||
| img, _, err := image.Decode(bytes.NewReader(data)) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| m := resize.Resize(200, 200, img, resize.NearestNeighbor) | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err = sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil { | |||
| sess.Rollback() | |||
| return err | |||
| } | |||
| fw, err := os.Create(savePath) | |||
| if err != nil { | |||
| sess.Rollback() | |||
| return err | |||
| } | |||
| defer fw.Close() | |||
| if err = jpeg.Encode(fw, m, nil); err != nil { | |||
| sess.Rollback() | |||
| return err | |||
| } | |||
| return sess.Commit() | |||
| } | |||
| // IsOrganization returns true if user is actually a organization. | |||
| func (u *User) IsOrganization() bool { | |||
| return u.Type == ORGANIZATION | |||
| @@ -517,41 +568,38 @@ func GetUserIdsByNames(names []string) []int64 { | |||
| // UserCommit represtns a commit with validation of user. | |||
| type UserCommit struct { | |||
| UserName string | |||
| User *User | |||
| *git.Commit | |||
| } | |||
| // ValidateCommitWithEmail chceck if author's e-mail of commit is corresponsind to a user. | |||
| func ValidateCommitWithEmail(c *git.Commit) (uname string) { | |||
| func ValidateCommitWithEmail(c *git.Commit) *User { | |||
| u, err := GetUserByEmail(c.Author.Email) | |||
| if err == nil { | |||
| uname = u.Name | |||
| if err != nil { | |||
| return nil | |||
| } | |||
| return uname | |||
| return u | |||
| } | |||
| // ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users. | |||
| func ValidateCommitsWithEmails(oldCommits *list.List) *list.List { | |||
| emails := map[string]string{} | |||
| emails := map[string]*User{} | |||
| newCommits := list.New() | |||
| e := oldCommits.Front() | |||
| for e != nil { | |||
| c := e.Value.(*git.Commit) | |||
| uname := "" | |||
| var u *User | |||
| if v, ok := emails[c.Author.Email]; !ok { | |||
| u, err := GetUserByEmail(c.Author.Email) | |||
| if err == nil { | |||
| uname = u.Name | |||
| } | |||
| emails[c.Author.Email] = uname | |||
| u, _ = GetUserByEmail(c.Author.Email) | |||
| emails[c.Author.Email] = u | |||
| } else { | |||
| uname = v | |||
| u = v | |||
| } | |||
| newCommits.PushBack(UserCommit{ | |||
| UserName: uname, | |||
| Commit: c, | |||
| User: u, | |||
| Commit: c, | |||
| }) | |||
| e = e.Next() | |||
| } | |||
| @@ -5,6 +5,8 @@ | |||
| package auth | |||
| import ( | |||
| "mime/multipart" | |||
| "github.com/Unknwon/macaron" | |||
| "github.com/macaron-contrib/binding" | |||
| ) | |||
| @@ -86,6 +88,14 @@ func (f *UpdateProfileForm) Validate(ctx *macaron.Context, errs binding.Errors) | |||
| return validate(errs, ctx.Data, f, ctx.Locale) | |||
| } | |||
| type UploadAvatarForm struct { | |||
| Avatar *multipart.FileHeader `form:"avatar" binding:"Required"` | |||
| } | |||
| func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||
| return validate(errs, ctx.Data, f, ctx.Locale) | |||
| } | |||
| type ChangePasswordForm struct { | |||
| OldPassword string `form:"old_password" binding:"Required;MinSize(6);MaxSize(255)"` | |||
| Password string `form:"password" binding:"Required;MinSize(6);MaxSize(255)"` | |||
| @@ -121,7 +121,7 @@ func (this *Avatar) Encode(wr io.Writer, size int) (err error) { | |||
| if img, err = decodeImageFile(imgPath); err != nil { | |||
| return | |||
| } | |||
| m := resize.Resize(uint(size), 0, img, resize.Lanczos3) | |||
| m := resize.Resize(uint(size), 0, img, resize.NearestNeighbor) | |||
| return jpeg.Encode(wr, m, nil) | |||
| } | |||
| @@ -66,9 +66,10 @@ var ( | |||
| ScriptType string | |||
| // Picture settings. | |||
| PictureService string | |||
| GravatarSource string | |||
| DisableGravatar bool | |||
| PictureService string | |||
| AvatarUploadPath string | |||
| GravatarSource string | |||
| DisableGravatar bool | |||
| // Log settings. | |||
| LogRootPath string | |||
| @@ -259,6 +260,9 @@ func NewConfigContext() { | |||
| ScriptType = Cfg.MustValue("repository", "SCRIPT_TYPE", "bash") | |||
| PictureService = Cfg.MustValueRange("picture", "SERVICE", "server", []string{"server"}) | |||
| AvatarUploadPath = Cfg.MustValue("picture", "AVATAR_UPLOAD_PATH", "data/avatars") | |||
| os.MkdirAll(AvatarUploadPath, os.ModePerm) | |||
| switch Cfg.MustValue("picture", "GRAVATAR_SOURCE", "gravatar") { | |||
| case "duoshuo": | |||
| GravatarSource = "http://gravatar.duoshuo.com/avatar/" | |||
| @@ -100,6 +100,13 @@ func Dashboard(ctx *middleware.Context) { | |||
| continue | |||
| } | |||
| } | |||
| // FIXME: cache results? | |||
| u, err := models.GetUserByName(act.ActUserName) | |||
| if err != nil { | |||
| ctx.Handle(500, "GetUserByName", err) | |||
| return | |||
| } | |||
| act.ActAvatar = u.AvatarLink() | |||
| feeds = append(feeds, act) | |||
| } | |||
| ctx.Data["Feeds"] = feeds | |||
| @@ -5,6 +5,7 @@ | |||
| package user | |||
| import ( | |||
| "io/ioutil" | |||
| "strings" | |||
| "github.com/Unknwon/com" | |||
| @@ -83,6 +84,34 @@ func SettingsPost(ctx *middleware.Context, form auth.UpdateProfileForm) { | |||
| ctx.Redirect(setting.AppSubUrl + "/user/settings") | |||
| } | |||
| // FIXME: limit size. | |||
| func SettingsAvatar(ctx *middleware.Context, form auth.UploadAvatarForm) { | |||
| defer ctx.Redirect(setting.AppSubUrl + "/user/settings") | |||
| if form.Avatar != nil { | |||
| fr, err := form.Avatar.Open() | |||
| if err != nil { | |||
| ctx.Flash.Error(err.Error()) | |||
| return | |||
| } | |||
| data, err := ioutil.ReadAll(fr) | |||
| if err != nil { | |||
| ctx.Flash.Error(err.Error()) | |||
| return | |||
| } | |||
| if _, ok := base.IsImageFile(data); !ok { | |||
| ctx.Flash.Error(ctx.Tr("settings.uploaded_avatar_not_a_image")) | |||
| return | |||
| } | |||
| if err = ctx.User.UploadAvatar(data); err != nil { | |||
| ctx.Flash.Error(err.Error()) | |||
| return | |||
| } | |||
| ctx.Flash.Success(ctx.Tr("settings.upload_avatar_success")) | |||
| } | |||
| } | |||
| func SettingsPassword(ctx *middleware.Context) { | |||
| ctx.Data["Title"] = ctx.Tr("settings") | |||
| ctx.Data["PageIsUserSettings"] = true | |||
| @@ -1 +1 @@ | |||
| 0.5.8.1119 Beta | |||
| 0.5.8.1121 Beta | |||
| @@ -24,7 +24,13 @@ | |||
| {{$r := List .Commits}} | |||
| {{range $r}} | |||
| <tr> | |||
| <td class="author"><img class="avatar-20" src="{{AvatarLink .Author.Email}}" alt=""/> {{if .UserName}}<a href="{{AppSubUrl}}/{{.UserName}}">{{.Author.Name}}</a>{{else}}{{.Author.Name}}{{end}}</td> | |||
| <td class="author"> | |||
| {{if .User}} | |||
| <img class="avatar-20" src="{{.User.AvatarLink}}" alt=""/> <a href="{{AppSubUrl}}/{{.User.Name}}">{{.Author.Name}}</a> | |||
| {{else}} | |||
| <img class="avatar-20" src="{{AvatarLink .Author.Email}}" alt=""/> {{.Author.Name}} | |||
| {{end}} | |||
| </td> | |||
| <td class="sha"><a rel="nofollow" class="label label-green" href="{{AppSubUrl}}/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td> | |||
| <td class="message"><span class="text-truncate">{{.Summary}}</span></td> | |||
| <td class="date">{{TimeSince .Author.When $.Lang}}</td> | |||
| @@ -30,10 +30,11 @@ | |||
| </ul> | |||
| </span> | |||
| <p class="author"> | |||
| <img class="avatar-30" src="{{AvatarLink .Commit.Author.Email}}" /> | |||
| {{if .Author}} | |||
| <a href="{{AppSubUrl}}/{{.Author}}"><strong>{{.Commit.Author.Name}}</strong></a> | |||
| <img class="avatar-30" src="{{.Author.AvatarLink}}" /> | |||
| <a href="{{AppSubUrl}}/{{.Author.Name}}"><strong>{{.Commit.Author.Name}}</strong></a> | |||
| {{else}} | |||
| <img class="avatar-30" src="{{AvatarLink .Commit.Author.Email}}" /> | |||
| <strong>{{.Commit.Author.Name}}</strong> | |||
| {{end}} | |||
| <span class="text-grey" id="authored-time">{{TimeSince .Commit.Author.When $.Lang}}</span> | |||
| @@ -3,8 +3,14 @@ | |||
| <tr> | |||
| <th colspan="4" class="clear"> | |||
| <span class="author left"> | |||
| {{if .LastCommitUser}} | |||
| <img class="avatar-24 radius" src="{{.LastCommitUser.AvatarLink}}" /> | |||
| <a href="{{AppSubUrl}}/{{.LastCommitUser.Name}}"><strong>{{.LastCommit.Author.Name}}</strong></a>: | |||
| {{else}} | |||
| <img class="avatar-24 radius" src="{{AvatarLink .LastCommit.Author.Email}}" /> | |||
| {{if .LastCommitUser}}<a href="{{AppSubUrl}}/{{.LastCommitUser}}">{{end}}<strong>{{.LastCommit.Author.Name}}</strong>:{{if .LastCommitUser}}</a>{{end}} | |||
| <strong>{{.LastCommit.Author.Name}}</strong>: | |||
| {{end}} | |||
| | |||
| </span> | |||
| <span class="last-commit"><a href="{{.RepoLink}}/commit/{{.LastCommit.Id}}" rel="nofollow"> | |||
| <strong>{{ShortSha .LastCommit.Id.String}}</strong></a> | |||
| @@ -1,7 +1,7 @@ | |||
| {{range .Feeds}} | |||
| <div class="news clear"> | |||
| <div class="avatar left"> | |||
| <img class="avatar-30" src="{{AvatarLink .GetActEmail}}" alt=""> | |||
| <img class="avatar-30" src="{{.ActAvatar}}" alt=""> | |||
| </div> | |||
| <div class="content left {{if eq .GetOpType 5}}push-news{{end}} grid-4-5"> | |||
| <p class="text-bold"> | |||
| @@ -4,7 +4,11 @@ | |||
| <div id="user-profile-page" class="container clear"> | |||
| <div class="grid-1-5 left"> | |||
| <div> | |||
| {{if .Owner.UseCustomAvatar}} | |||
| <a href="{{AppSubUrl}}/user/settings" id="profile-avatar" original-title="{{.i18n.Tr "user.change_custom_avatar"}}"> | |||
| {{else}} | |||
| <a href="http://gravatar.com/emails/" id="profile-avatar" original-title="{{.i18n.Tr "user.change_avatar"}}"> | |||
| {{end}} | |||
| <img class="profile-avatar" src="{{.Owner.AvatarLink}}?s=200"title="{{.Owner.Name}}"/> | |||
| </a> | |||
| <div class="text-center" id="profile-name"> | |||
| @@ -11,49 +11,63 @@ | |||
| <div class="panel-header"> | |||
| <strong>{{.i18n.Tr "settings.public_profile"}}</strong> | |||
| </div> | |||
| <form class="form form-align panel-body" id="user-profile-form" action="{{AppSubUrl}}/user/settings" method="post"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="text-center panel-desc">{{.i18n.Tr "settings.profile_desc"}}</div> | |||
| <div class="field"> | |||
| <label>{{.i18n.Tr "settings.uid"}}</label> | |||
| <label class="text-left">{{.SignedUser.Id}}</label> | |||
| </div> | |||
| <div class="field"> | |||
| <label class="req" for="username">{{.i18n.Tr "username"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="username" name="uname" type="text" value="{{.SignedUser.Name}}" data-uname="{{.SignedUser.Name}}" required /> | |||
| </div> | |||
| <div class="white-popup-block mfp-hide" id="change-username-modal"> | |||
| <h1 class="text-red">{{.i18n.Tr "settings.change_username"}}</h1> | |||
| <p>{{.i18n.Tr "settings.change_username_desc"}}</p> | |||
| <br> | |||
| <button class="btn btn-red btn-large btn-radius" id="change-username-submit">{{.i18n.Tr "settings.continue"}}</button> | |||
| <button class="btn btn-large btn-radius popup-modal-dismiss">{{.i18n.Tr "settings.cancel"}}</button> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="full-name">{{.i18n.Tr "settings.full_name"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_FullName}}ipt-error{{end}}" id="full-name" name="fullname" type="text" value="{{.SignedUser.FullName}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label class="req" for="email">{{.i18n.Tr "email"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Email}}ipt-error{{end}}" id="email" name="email" type="email" value="{{.SignedUser.Email}}" required /> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="website">{{.i18n.Tr "settings.website"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Website}}ipt-error{{end}}" id="website" name="website" type="url" value="{{.SignedUser.Website}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="location">{{.i18n.Tr "settings.location"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Location}}ipt-error{{end}}" id="location" name="location" type="text" value="{{.SignedUser.Location}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="gravatar-email">Gravatar {{.i18n.Tr "email"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Avatar}}ipt-error{{end}}" id="gravatar-email" name="avatar" type="text" value="{{.SignedUser.AvatarEmail}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label></label> | |||
| <button class="btn btn-green btn-large btn-radius" id="change-username-btn" href="#change-username-modal">{{.i18n.Tr "settings.update_profile"}}</button> | |||
| </div> | |||
| </form> | |||
| <div class="panel-body"> | |||
| <form class="form form-align" id="user-profile-form" action="{{AppSubUrl}}/user/settings" method="post"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="text-center panel-desc">{{.i18n.Tr "settings.profile_desc"}}</div> | |||
| <div class="field"> | |||
| <label>{{.i18n.Tr "settings.uid"}}</label> | |||
| <label class="text-left">{{.SignedUser.Id}}</label> | |||
| </div> | |||
| <div class="field"> | |||
| <label class="req" for="username">{{.i18n.Tr "username"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_UserName}}ipt-error{{end}}" id="username" name="uname" type="text" value="{{.SignedUser.Name}}" data-uname="{{.SignedUser.Name}}" required /> | |||
| </div> | |||
| <div class="white-popup-block mfp-hide" id="change-username-modal"> | |||
| <h1 class="text-red">{{.i18n.Tr "settings.change_username"}}</h1> | |||
| <p>{{.i18n.Tr "settings.change_username_desc"}}</p> | |||
| <br> | |||
| <button class="btn btn-red btn-large btn-radius" id="change-username-submit">{{.i18n.Tr "settings.continue"}}</button> | |||
| <button class="btn btn-large btn-radius popup-modal-dismiss">{{.i18n.Tr "settings.cancel"}}</button> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="full-name">{{.i18n.Tr "settings.full_name"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_FullName}}ipt-error{{end}}" id="full-name" name="fullname" type="text" value="{{.SignedUser.FullName}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label class="req" for="email">{{.i18n.Tr "email"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Email}}ipt-error{{end}}" id="email" name="email" type="email" value="{{.SignedUser.Email}}" required /> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="website">{{.i18n.Tr "settings.website"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Website}}ipt-error{{end}}" id="website" name="website" type="url" value="{{.SignedUser.Website}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="location">{{.i18n.Tr "settings.location"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Location}}ipt-error{{end}}" id="location" name="location" type="text" value="{{.SignedUser.Location}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="gravatar-email">Gravatar {{.i18n.Tr "email"}}</label> | |||
| <input class="ipt ipt-large ipt-radius {{if .Err_Avatar}}ipt-error{{end}}" id="gravatar-email" name="avatar" type="text" value="{{.SignedUser.AvatarEmail}}" /> | |||
| </div> | |||
| <div class="field"> | |||
| <label></label> | |||
| <button class="btn btn-green btn-large btn-radius" id="change-username-btn" href="#change-username-modal">{{.i18n.Tr "settings.update_profile"}}</button> | |||
| </div> | |||
| </form> | |||
| <hr> | |||
| <form class="form form-align" id="user-profile-form" action="{{AppSubUrl}}/user/settings/avatar" method="post" enctype="multipart/form-data"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="field"> | |||
| <label>{{.i18n.Tr "settings.choose_new_avatar"}}</label> | |||
| <input name="avatar" type="file" required /> | |||
| </div> | |||
| <div class="field"> | |||
| <label></label> | |||
| <button class="btn btn-green btn-large btn-radius">{{.i18n.Tr "settings.upload_avatar"}}</button> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||