- 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.
## 特别鸣谢
-- [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) 提供二进制编译与下载服务。
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},
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)
[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
[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
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
"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())
ActUserId int64 // Action user id.
ActUserName string // Action user name.
ActEmail string
+ ActAvatar string `xorm:"-"`
RepoId int64
RepoUserName string
RepoName string
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"
// 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
// 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
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
// 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()
}
package auth
import (
+ "mime/multipart"
+
"github.com/Unknwon/macaron"
"github.com/macaron-contrib/binding"
)
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)"`
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)
}
ScriptType string
// Picture settings.
- PictureService string
- GravatarSource string
- DisableGravatar bool
+ PictureService string
+ AvatarUploadPath string
+ GravatarSource string
+ DisableGravatar bool
// Log settings.
LogRootPath string
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/"
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
package user
import (
+ "io/ioutil"
"strings"
"github.com/Unknwon/com"
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
-0.5.8.1119 Beta
\ No newline at end of file
+0.5.8.1121 Beta
\ No newline at end of file
{{$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>
</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>
<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>
{{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">
<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">
<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>