- User profile un/follow - List user's followers/followingmaster
@@ -13,7 +13,7 @@ watch_dirs = [ | |||||
watch_exts = [".go"] | watch_exts = [".go"] | ||||
build_delay = 1500 | build_delay = 1500 | ||||
cmds = [ | cmds = [ | ||||
["go", "install", "-race"], # sqlite redis memcache cert pam tidb | |||||
["go", "install", "-v", "-race"], # sqlite redis memcache cert pam tidb | |||||
["go", "build", "-race"], | ["go", "build", "-race"], | ||||
["./gogs", "web"] | ["./gogs", "web"] | ||||
] | ] |
@@ -16,7 +16,7 @@ NOW = $(shell date -u '+%Y%m%d%I%M%S') | |||||
.IGNORE: public/css/gogs.css | .IGNORE: public/css/gogs.css | ||||
build: $(GENERATED) | build: $(GENERATED) | ||||
go install -ldflags '$(LDFLAGS)' -tags '$(TAGS)' | |||||
go install -v -ldflags '$(LDFLAGS)' -tags '$(TAGS)' | |||||
cp '$(GOPATH)/bin/gogs' . | cp '$(GOPATH)/bin/gogs' . | ||||
govet: | govet: | ||||
@@ -3,7 +3,7 @@ Gogs - Go Git Service [ |  | ||||
##### Current version: 0.8.12 | |||||
##### Current version: 0.8.13 | |||||
| Web | UI | Preview | | | Web | UI | Preview | | ||||
|:-------------:|:-------:|:-------:| | |:-------------:|:-------:|:-------:| | ||||
@@ -82,6 +82,7 @@ There are 5 ways to install Gogs: | |||||
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654) (Chinese) | - [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654) (Chinese) | ||||
- [Installing Gogs on FreeBSD](https://www.codejam.info/2015/03/installing-gogs-on-freebsd.html) | - [Installing Gogs on FreeBSD](https://www.codejam.info/2015/03/installing-gogs-on-freebsd.html) | ||||
- [Gogs on Raspberry Pi](http://blog.meinside.pe.kr/Gogs-on-Raspberry-Pi/) | - [Gogs on Raspberry Pi](http://blog.meinside.pe.kr/Gogs-on-Raspberry-Pi/) | ||||
- [Cloudflare Full SSL with GOGS (Go Git Service) using NGINX](http://www.listekconsulting.com/articles/cloudflare-full-ssl-with-gogs-go-git-service-using-nginx/) | |||||
### Screencasts | ### Screencasts | ||||
@@ -101,6 +102,7 @@ There are 5 ways to install Gogs: | |||||
- [Drone](https://github.com/drone/drone) (CI) | - [Drone](https://github.com/drone/drone) (CI) | ||||
- [Fabric8](http://fabric8.io/) (DevOps) | - [Fabric8](http://fabric8.io/) (DevOps) | ||||
- [Taiga](https://taiga.io/) (Project Management) | - [Taiga](https://taiga.io/) (Project Management) | ||||
- [Puppet](https://forge.puppetlabs.com/Siteminds/gogs) (IT) | |||||
### Product Support | ### Product Support | ||||
@@ -73,6 +73,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自 | |||||
- [Drone](https://github.com/drone/drone)(CI) | - [Drone](https://github.com/drone/drone)(CI) | ||||
- [Fabric8](http://fabric8.io/)(DevOps) | - [Fabric8](http://fabric8.io/)(DevOps) | ||||
- [Taiga](https://taiga.io/)(项目管理) | - [Taiga](https://taiga.io/)(项目管理) | ||||
- [Puppet](https://forge.puppetlabs.com/Siteminds/gogs)(IT) | |||||
### 产品支持 | ### 产品支持 | ||||
@@ -289,7 +289,13 @@ func runWeb(ctx *cli.Context) { | |||||
// ***** END: Admin ***** | // ***** END: Admin ***** | ||||
m.Group("", func() { | m.Group("", func() { | ||||
m.Get("/:username", user.Profile) | |||||
m.Group("/:username", func() { | |||||
m.Get("", user.Profile) | |||||
m.Get("/followers", user.Followers) | |||||
m.Get("/following", user.Following) | |||||
m.Get("/stars", user.Stars) | |||||
}) | |||||
m.Get("/attachments/:uuid", func(ctx *middleware.Context) { | m.Get("/attachments/:uuid", func(ctx *middleware.Context) { | ||||
attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid")) | attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid")) | ||||
if err != nil { | if err != nil { | ||||
@@ -319,6 +325,10 @@ func runWeb(ctx *cli.Context) { | |||||
m.Post("/issues/attachments", repo.UploadIssueAttachment) | m.Post("/issues/attachments", repo.UploadIssueAttachment) | ||||
}, ignSignIn) | }, ignSignIn) | ||||
m.Group("/:username", func() { | |||||
m.Get("/action/:action", user.Action) | |||||
}, reqSignIn) | |||||
if macaron.Env == macaron.DEV { | if macaron.Env == macaron.DEV { | ||||
m.Get("/template/*", dev.TemplatePreview) | m.Get("/template/*", dev.TemplatePreview) | ||||
} | } | ||||
@@ -230,8 +230,10 @@ join_on = Joined on | |||||
repositories = Repositories | repositories = Repositories | ||||
activity = Public Activity | activity = Public Activity | ||||
followers = Followers | followers = Followers | ||||
starred = Starred | |||||
starred = Starred repositories | |||||
following = Following | following = Following | ||||
follow = Follow | |||||
unfollow = Unfollow | |||||
form.name_reserved = Username '%s' is reserved. | form.name_reserved = Username '%s' is reserved. | ||||
form.name_pattern_not_allowed = Username pattern '%s' is not allowed. | form.name_pattern_not_allowed = Username pattern '%s' is not allowed. | ||||
@@ -17,7 +17,7 @@ import ( | |||||
"github.com/gogits/gogs/modules/setting" | "github.com/gogits/gogs/modules/setting" | ||||
) | ) | ||||
const APP_VER = "0.8.12.1219" | |||||
const APP_VER = "0.8.13.1221" | |||||
func init() { | func init() { | ||||
runtime.GOMAXPROCS(runtime.NumCPU()) | runtime.GOMAXPROCS(runtime.NumCPU()) | ||||
@@ -665,6 +665,47 @@ func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int | |||||
return ius, err | return ius, err | ||||
} | } | ||||
func UpdateMentions(userNames []string, issueId int64) error { | |||||
for i := range userNames { | |||||
userNames[i] = strings.ToLower(userNames[i]) | |||||
} | |||||
users := make([]*User, 0, len(userNames)) | |||||
if err := x.Where("lower_name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("lower_name ASC").Find(&users); err != nil { | |||||
return err | |||||
} | |||||
ids := make([]int64, 0, len(userNames)) | |||||
for _, user := range users { | |||||
ids = append(ids, user.Id) | |||||
if !user.IsOrganization() { | |||||
continue | |||||
} | |||||
if user.NumMembers == 0 { | |||||
continue | |||||
} | |||||
tempIds := make([]int64, 0, user.NumMembers) | |||||
orgUsers, err := GetOrgUsersByOrgId(user.Id) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, orgUser := range orgUsers { | |||||
tempIds = append(tempIds, orgUser.ID) | |||||
} | |||||
ids = append(ids, tempIds...) | |||||
} | |||||
if err := UpdateIssueUsersByMentions(ids, issueId); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// IssueStats represents issue statistic information. | // IssueStats represents issue statistic information. | ||||
type IssueStats struct { | type IssueStats struct { | ||||
OpenCount, ClosedCount int64 | OpenCount, ClosedCount int64 | ||||
@@ -56,7 +56,7 @@ type User struct { | |||||
LowerName string `xorm:"UNIQUE NOT NULL"` | LowerName string `xorm:"UNIQUE NOT NULL"` | ||||
Name string `xorm:"UNIQUE NOT NULL"` | Name string `xorm:"UNIQUE NOT NULL"` | ||||
FullName string | FullName string | ||||
// Email is the primary email address (to be used for communication). | |||||
// Email is the primary email address (to be used for communication) | |||||
Email string `xorm:"NOT NULL"` | Email string `xorm:"NOT NULL"` | ||||
Passwd string `xorm:"NOT NULL"` | Passwd string `xorm:"NOT NULL"` | ||||
LoginType LoginType | LoginType LoginType | ||||
@@ -78,24 +78,24 @@ type User struct { | |||||
// Maximum repository creation limit, -1 means use gloabl default | // Maximum repository creation limit, -1 means use gloabl default | ||||
MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"` | MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"` | ||||
// Permissions. | |||||
// Permissions | |||||
IsActive bool | IsActive bool | ||||
IsAdmin bool | IsAdmin bool | ||||
AllowGitHook bool | AllowGitHook bool | ||||
AllowImportLocal bool // Allow migrate repository by local path | AllowImportLocal bool // Allow migrate repository by local path | ||||
// Avatar. | |||||
// Avatar | |||||
Avatar string `xorm:"VARCHAR(2048) NOT NULL"` | Avatar string `xorm:"VARCHAR(2048) NOT NULL"` | ||||
AvatarEmail string `xorm:"NOT NULL"` | AvatarEmail string `xorm:"NOT NULL"` | ||||
UseCustomAvatar bool | UseCustomAvatar bool | ||||
// Counters. | |||||
NumFollowers int | |||||
NumFollowings int | |||||
NumStars int | |||||
NumRepos int | |||||
// Counters | |||||
NumFollowers int | |||||
NumFollowing int `xorm:"NOT NULL"` | |||||
NumStars int | |||||
NumRepos int | |||||
// For organization. | |||||
// For organization | |||||
Description string | Description string | ||||
NumTeams int | NumTeams int | ||||
NumMembers int | NumMembers int | ||||
@@ -263,6 +263,34 @@ func (u *User) AvatarLink() string { | |||||
return link | return link | ||||
} | } | ||||
// User.GetFollwoers returns range of user's followers. | |||||
func (u *User) GetFollowers(page int) ([]*User, error) { | |||||
users := make([]*User, 0, ItemsPerPage) | |||||
sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("follow.follow_id=?", u.Id) | |||||
if setting.UsePostgreSQL { | |||||
sess = sess.Join("LEFT", "follow", `"user".id=follow.user_id`) | |||||
} else { | |||||
sess = sess.Join("LEFT", "follow", "user.id=follow.user_id") | |||||
} | |||||
return users, sess.Find(&users) | |||||
} | |||||
func (u *User) IsFollowing(followID int64) bool { | |||||
return IsFollowing(u.Id, followID) | |||||
} | |||||
// GetFollowing returns range of user's following. | |||||
func (u *User) GetFollowing(page int) ([]*User, error) { | |||||
users := make([]*User, 0, ItemsPerPage) | |||||
sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("follow.user_id=?", u.Id) | |||||
if setting.UsePostgreSQL { | |||||
sess = sess.Join("LEFT", "follow", `"user".id=follow.follow_id`) | |||||
} else { | |||||
sess = sess.Join("LEFT", "follow", "user.id=follow.follow_id") | |||||
} | |||||
return users, sess.Find(&users) | |||||
} | |||||
// NewGitSig generates and returns the signature of given user. | // NewGitSig generates and returns the signature of given user. | ||||
func (u *User) NewGitSig() *git.Signature { | func (u *User) NewGitSig() *git.Signature { | ||||
return &git.Signature{ | return &git.Signature{ | ||||
@@ -1077,100 +1105,73 @@ func SearchUserByName(opt SearchOption) (us []*User, err error) { | |||||
return us, err | return us, err | ||||
} | } | ||||
// Follow is connection request for receiving user notification. | |||||
// ___________ .__ .__ | |||||
// \_ _____/___ | | | | ______ _ __ | |||||
// | __)/ _ \| | | | / _ \ \/ \/ / | |||||
// | \( <_> ) |_| |_( <_> ) / | |||||
// \___ / \____/|____/____/\____/ \/\_/ | |||||
// \/ | |||||
// Follow represents relations of user and his/her followers. | |||||
type Follow struct { | type Follow struct { | ||||
ID int64 `xorm:"pk autoincr"` | ID int64 `xorm:"pk autoincr"` | ||||
UserID int64 `xorm:"UNIQUE(follow)"` | UserID int64 `xorm:"UNIQUE(follow)"` | ||||
FollowID int64 `xorm:"UNIQUE(follow)"` | FollowID int64 `xorm:"UNIQUE(follow)"` | ||||
} | } | ||||
func IsFollowing(userID, followID int64) bool { | |||||
has, _ := x.Get(&Follow{UserID: userID, FollowID: followID}) | |||||
return has | |||||
} | |||||
// FollowUser marks someone be another's follower. | // FollowUser marks someone be another's follower. | ||||
func FollowUser(userId int64, followId int64) (err error) { | |||||
func FollowUser(userID, followID int64) (err error) { | |||||
if userID == followID || IsFollowing(userID, followID) { | |||||
return nil | |||||
} | |||||
sess := x.NewSession() | sess := x.NewSession() | ||||
defer sess.Close() | |||||
sess.Begin() | |||||
defer sessionRelease(sess) | |||||
if err = sess.Begin(); err != nil { | |||||
return err | |||||
} | |||||
if _, err = sess.Insert(&Follow{UserID: userId, FollowID: followId}); err != nil { | |||||
sess.Rollback() | |||||
if _, err = sess.Insert(&Follow{UserID: userID, FollowID: followID}); err != nil { | |||||
return err | return err | ||||
} | } | ||||
rawSql := "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?" | |||||
if _, err = sess.Exec(rawSql, followId); err != nil { | |||||
sess.Rollback() | |||||
if _, err = sess.Exec("UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil { | |||||
return err | return err | ||||
} | } | ||||
rawSql = "UPDATE `user` SET num_followings = num_followings + 1 WHERE id = ?" | |||||
if _, err = sess.Exec(rawSql, userId); err != nil { | |||||
sess.Rollback() | |||||
if _, err = sess.Exec("UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil { | |||||
return err | return err | ||||
} | } | ||||
return sess.Commit() | return sess.Commit() | ||||
} | } | ||||
// UnFollowUser unmarks someone be another's follower. | |||||
func UnFollowUser(userId int64, unFollowId int64) (err error) { | |||||
session := x.NewSession() | |||||
defer session.Close() | |||||
session.Begin() | |||||
if _, err = session.Delete(&Follow{UserID: userId, FollowID: unFollowId}); err != nil { | |||||
session.Rollback() | |||||
return err | |||||
// UnfollowUser unmarks someone be another's follower. | |||||
func UnfollowUser(userID, followID int64) (err error) { | |||||
if userID == followID || !IsFollowing(userID, followID) { | |||||
return nil | |||||
} | } | ||||
rawSql := "UPDATE `user` SET num_followers = num_followers - 1 WHERE id = ?" | |||||
if _, err = session.Exec(rawSql, unFollowId); err != nil { | |||||
session.Rollback() | |||||
sess := x.NewSession() | |||||
defer sessionRelease(sess) | |||||
if err = sess.Begin(); err != nil { | |||||
return err | return err | ||||
} | } | ||||
rawSql = "UPDATE `user` SET num_followings = num_followings - 1 WHERE id = ?" | |||||
if _, err = session.Exec(rawSql, userId); err != nil { | |||||
session.Rollback() | |||||
if _, err = sess.Delete(&Follow{UserID: userID, FollowID: followID}); err != nil { | |||||
return err | return err | ||||
} | } | ||||
return session.Commit() | |||||
} | |||||
func UpdateMentions(userNames []string, issueId int64) error { | |||||
for i := range userNames { | |||||
userNames[i] = strings.ToLower(userNames[i]) | |||||
} | |||||
users := make([]*User, 0, len(userNames)) | |||||
if err := x.Where("lower_name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("lower_name ASC").Find(&users); err != nil { | |||||
if _, err = sess.Exec("UPDATE `user` SET num_followers = num_followers - 1 WHERE id = ?", followID); err != nil { | |||||
return err | return err | ||||
} | } | ||||
ids := make([]int64, 0, len(userNames)) | |||||
for _, user := range users { | |||||
ids = append(ids, user.Id) | |||||
if !user.IsOrganization() { | |||||
continue | |||||
} | |||||
if user.NumMembers == 0 { | |||||
continue | |||||
} | |||||
tempIds := make([]int64, 0, user.NumMembers) | |||||
orgUsers, err := GetOrgUsersByOrgId(user.Id) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, orgUser := range orgUsers { | |||||
tempIds = append(tempIds, orgUser.ID) | |||||
} | |||||
ids = append(ids, tempIds...) | |||||
} | |||||
if err := UpdateIssueUsersByMentions(ids, issueId); err != nil { | |||||
if _, err = sess.Exec("UPDATE `user` SET num_following = num_following - 1 WHERE id = ?", userID); err != nil { | |||||
return err | return err | ||||
} | } | ||||
return nil | |||||
return sess.Commit() | |||||
} | } |
@@ -242,7 +242,8 @@ func Contexter() macaron.Handler { | |||||
ctx.Data["CsrfToken"] = x.GetToken() | ctx.Data["CsrfToken"] = x.GetToken() | ||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + x.GetToken() + `">`) | ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + x.GetToken() + `">`) | ||||
log.Debug("CSRF Token: %v | %v", ctx.Data["CsrfToken"], ctx.GetCookie("_csrf")) | |||||
log.Debug("Session ID: %s", sess.ID()) | |||||
log.Debug("CSRF Token: %v", ctx.Data["CsrfToken"]) | |||||
ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton | ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton | ||||
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding | ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding | ||||
@@ -2465,31 +2465,6 @@ footer .container .links > *:first-child { | |||||
.repository.new.release .prerelease.field { | .repository.new.release .prerelease.field { | ||||
margin-bottom: 0; | margin-bottom: 0; | ||||
} | } | ||||
.repository.watchers .list { | |||||
padding: 0; | |||||
} | |||||
.repository.watchers .list .item { | |||||
list-style: none; | |||||
width: 32%; | |||||
margin: 10px 10px 10px 0; | |||||
padding-bottom: 14px; | |||||
float: left; | |||||
} | |||||
.repository.watchers .list .item .avatar { | |||||
width: 48px; | |||||
height: 48px; | |||||
float: left; | |||||
display: block; | |||||
margin-right: 10px; | |||||
} | |||||
.repository.watchers .list .item .name { | |||||
margin-top: 0; | |||||
margin-bottom: 0; | |||||
font-weight: normal; | |||||
} | |||||
.repository.watchers .list .item .meta { | |||||
margin-top: 5px; | |||||
} | |||||
.repository.forks .list { | .repository.forks .list { | ||||
margin-top: 0; | margin-top: 0; | ||||
} | } | ||||
@@ -2551,6 +2526,31 @@ footer .container .links > *:first-child { | |||||
margin-left: 5px; | margin-left: 5px; | ||||
margin-top: -3px; | margin-top: -3px; | ||||
} | } | ||||
.user-cards .list { | |||||
padding: 0; | |||||
} | |||||
.user-cards .list .item { | |||||
list-style: none; | |||||
width: 32%; | |||||
margin: 10px 10px 10px 0; | |||||
padding-bottom: 14px; | |||||
float: left; | |||||
} | |||||
.user-cards .list .item .avatar { | |||||
width: 48px; | |||||
height: 48px; | |||||
float: left; | |||||
display: block; | |||||
margin-right: 10px; | |||||
} | |||||
.user-cards .list .item .name { | |||||
margin-top: 0; | |||||
margin-bottom: 0; | |||||
font-weight: normal; | |||||
} | |||||
.user-cards .list .item .meta { | |||||
margin-top: 5px; | |||||
} | |||||
#search-repo-box .results, | #search-repo-box .results, | ||||
#search-user-box .results { | #search-user-box .results { | ||||
padding: 0; | padding: 0; | ||||
@@ -2862,7 +2862,7 @@ footer .container .links > *:first-child { | |||||
margin-left: 5px; | margin-left: 5px; | ||||
margin-top: -3px; | margin-top: -3px; | ||||
} | } | ||||
.user { | |||||
.user:not(.icon) { | |||||
padding-top: 15px; | padding-top: 15px; | ||||
padding-bottom: 80px; | padding-bottom: 80px; | ||||
} | } | ||||
@@ -2893,9 +2893,24 @@ footer .container .links > *:first-child { | |||||
.user.profile .ui.card .extra.content ul li:not(:last-child) { | .user.profile .ui.card .extra.content ul li:not(:last-child) { | ||||
border-bottom: 1px solid #eaeaea; | border-bottom: 1px solid #eaeaea; | ||||
} | } | ||||
.user.profile .ui.card .extra.content ul li .octicon { | |||||
margin-left: 1px; | |||||
margin-right: 5px; | |||||
} | |||||
.user.profile .ui.card .extra.content ul li.follow .ui.button { | |||||
width: 100%; | |||||
} | |||||
.user.profile .ui.repository.list { | .user.profile .ui.repository.list { | ||||
margin-top: 25px; | margin-top: 25px; | ||||
} | } | ||||
.user.followers .header.name { | |||||
font-size: 20px; | |||||
line-height: 24px; | |||||
vertical-align: middle; | |||||
} | |||||
.user.followers .follow .ui.button { | |||||
padding: 8px 15px; | |||||
} | |||||
.dashboard { | .dashboard { | ||||
padding-top: 15px; | padding-top: 15px; | ||||
padding-bottom: 80px; | padding-bottom: 80px; | ||||
@@ -859,35 +859,6 @@ | |||||
} | } | ||||
} | } | ||||
&.watchers { | |||||
.list { | |||||
padding: 0; | |||||
.item { | |||||
list-style: none; | |||||
width: 32%; | |||||
margin: 10px 10px 10px 0; | |||||
padding-bottom: 14px; | |||||
float: left; | |||||
.avatar { | |||||
width: 48px; | |||||
height: 48px; | |||||
float: left; | |||||
display: block; | |||||
margin-right: 10px; | |||||
} | |||||
.name { | |||||
margin-top: 0; | |||||
margin-bottom: 0; | |||||
font-weight: normal; | |||||
} | |||||
.meta { | |||||
margin-top: 5px; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
&.forks { | &.forks { | ||||
.list { | .list { | ||||
margin-top: 0; | margin-top: 0; | ||||
@@ -982,6 +953,36 @@ | |||||
} | } | ||||
// End of .repository | // End of .repository | ||||
&.user-cards { | |||||
.list { | |||||
padding: 0; | |||||
.item { | |||||
list-style: none; | |||||
width: 32%; | |||||
margin: 10px 10px 10px 0; | |||||
padding-bottom: 14px; | |||||
float: left; | |||||
.avatar { | |||||
width: 48px; | |||||
height: 48px; | |||||
float: left; | |||||
display: block; | |||||
margin-right: 10px; | |||||
} | |||||
.name { | |||||
margin-top: 0; | |||||
margin-bottom: 0; | |||||
font-weight: normal; | |||||
} | |||||
.meta { | |||||
margin-top: 5px; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
#search-repo-box, | #search-repo-box, | ||||
#search-user-box { | #search-user-box { | ||||
.results { | .results { | ||||
@@ -1,6 +1,8 @@ | |||||
.user { | .user { | ||||
padding-top: 15px; | |||||
padding-bottom: @footer-margin * 2; | |||||
&:not(.icon) { | |||||
padding-top: 15px; | |||||
padding-bottom: @footer-margin * 2; | |||||
} | |||||
&.settings { | &.settings { | ||||
.list { | .list { | ||||
@@ -38,6 +40,17 @@ | |||||
&:not(:last-child) { | &:not(:last-child) { | ||||
border-bottom: 1px solid #eaeaea; | border-bottom: 1px solid #eaeaea; | ||||
} | } | ||||
.octicon { | |||||
margin-left: 1px; | |||||
margin-right: 5px; | |||||
} | |||||
&.follow { | |||||
.ui.button { | |||||
width: 100%; | |||||
} | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -47,4 +60,18 @@ | |||||
margin-top: 25px; | margin-top: 25px; | ||||
} | } | ||||
} | } | ||||
&.followers { | |||||
.header.name { | |||||
font-size: 20px; | |||||
line-height: 24px; | |||||
vertical-align: middle; | |||||
} | |||||
.follow { | |||||
.ui.button { | |||||
padding: 8px 15px; | |||||
} | |||||
} | |||||
} | |||||
} | } |
@@ -135,19 +135,32 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
m.Group("/users", func() { | m.Group("/users", func() { | ||||
m.Group("/:username", func() { | m.Group("/:username", func() { | ||||
m.Get("/keys", user.ListPublicKeys) | m.Get("/keys", user.ListPublicKeys) | ||||
m.Get("/followers", user.ListFollowers) | |||||
m.Group("/following", func() { | |||||
m.Get("", user.ListFollowing) | |||||
m.Get("/:target", user.CheckFollowing) | |||||
}) | |||||
}) | }) | ||||
}, ReqToken()) | }, ReqToken()) | ||||
m.Group("/user", func() { | m.Group("/user", func() { | ||||
m.Combo("/emails").Get(user.ListEmails). | |||||
Post(bind(api.CreateEmailOption{}), user.AddEmail). | |||||
Delete(bind(api.CreateEmailOption{}), user.DeleteEmail) | |||||
m.Get("/followers", user.ListMyFollowers) | |||||
m.Group("/following", func() { | |||||
m.Get("", user.ListMyFollowing) | |||||
m.Combo("/:username").Get(user.CheckMyFollowing).Put(user.Follow).Delete(user.Unfollow) | |||||
}) | |||||
m.Group("/keys", func() { | m.Group("/keys", func() { | ||||
m.Combo("").Get(user.ListMyPublicKeys). | m.Combo("").Get(user.ListMyPublicKeys). | ||||
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey) | Post(bind(api.CreateKeyOption{}), user.CreatePublicKey) | ||||
m.Combo("/:id").Get(user.GetPublicKey). | m.Combo("/:id").Get(user.GetPublicKey). | ||||
Delete(user.DeletePublicKey) | Delete(user.DeletePublicKey) | ||||
}) | }) | ||||
m.Combo("/emails").Get(user.ListEmails). | |||||
Post(bind(api.CreateEmailOption{}), user.AddEmail). | |||||
Delete(bind(api.CreateEmailOption{}), user.DeleteEmail) | |||||
}, ReqToken()) | }, ReqToken()) | ||||
// Repositories | // Repositories | ||||
@@ -0,0 +1,121 @@ | |||||
// Copyright 2015 The Gogs 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 user | |||||
import ( | |||||
api "github.com/gogits/go-gogs-client" | |||||
"github.com/gogits/gogs/models" | |||||
"github.com/gogits/gogs/modules/middleware" | |||||
"github.com/gogits/gogs/routers/api/v1/convert" | |||||
) | |||||
func responseApiUsers(ctx *middleware.Context, users []*models.User) { | |||||
apiUsers := make([]*api.User, len(users)) | |||||
for i := range users { | |||||
apiUsers[i] = convert.ToApiUser(users[i]) | |||||
} | |||||
ctx.JSON(200, &apiUsers) | |||||
} | |||||
func listUserFollowers(ctx *middleware.Context, u *models.User) { | |||||
users, err := u.GetFollowers(ctx.QueryInt("page")) | |||||
if err != nil { | |||||
ctx.APIError(500, "GetUserFollowers", err) | |||||
return | |||||
} | |||||
responseApiUsers(ctx, users) | |||||
} | |||||
func ListMyFollowers(ctx *middleware.Context) { | |||||
listUserFollowers(ctx, ctx.User) | |||||
} | |||||
// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#list-followers-of-a-user | |||||
func ListFollowers(ctx *middleware.Context) { | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
listUserFollowers(ctx, u) | |||||
} | |||||
func listUserFollowing(ctx *middleware.Context, u *models.User) { | |||||
users, err := u.GetFollowing(ctx.QueryInt("page")) | |||||
if err != nil { | |||||
ctx.APIError(500, "GetFollowing", err) | |||||
return | |||||
} | |||||
responseApiUsers(ctx, users) | |||||
} | |||||
func ListMyFollowing(ctx *middleware.Context) { | |||||
listUserFollowing(ctx, ctx.User) | |||||
} | |||||
// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#list-users-followed-by-another-user | |||||
func ListFollowing(ctx *middleware.Context) { | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
listUserFollowing(ctx, u) | |||||
} | |||||
func checkUserFollowing(ctx *middleware.Context, u *models.User, followID int64) { | |||||
if u.IsFollowing(followID) { | |||||
ctx.Status(204) | |||||
} else { | |||||
ctx.Error(404) | |||||
} | |||||
} | |||||
// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#check-if-you-are-following-a-user | |||||
func CheckMyFollowing(ctx *middleware.Context) { | |||||
target := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
checkUserFollowing(ctx, ctx.User, target.Id) | |||||
} | |||||
// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#check-if-one-user-follows-another | |||||
func CheckFollowing(ctx *middleware.Context) { | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
target := GetUserByParamsName(ctx, ":target") | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
checkUserFollowing(ctx, u, target.Id) | |||||
} | |||||
// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#follow-a-user | |||||
func Follow(ctx *middleware.Context) { | |||||
target := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
if err := models.FollowUser(ctx.User.Id, target.Id); err != nil { | |||||
ctx.APIError(500, "FollowUser", err) | |||||
return | |||||
} | |||||
ctx.Status(204) | |||||
} | |||||
// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#unfollow-a-user | |||||
func Unfollow(ctx *middleware.Context) { | |||||
target := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
if err := models.UnfollowUser(ctx.User.Id, target.Id); err != nil { | |||||
ctx.APIError(500, "UnfollowUser", err) | |||||
return | |||||
} | |||||
ctx.Status(204) | |||||
} |
@@ -244,11 +244,7 @@ func Action(ctx *middleware.Context) { | |||||
} | } | ||||
if err != nil { | if err != nil { | ||||
log.Error(4, "Action(%s): %v", ctx.Params(":action"), err) | |||||
ctx.JSON(200, map[string]interface{}{ | |||||
"ok": false, | |||||
"err": err.Error(), | |||||
}) | |||||
ctx.Handle(500, fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) | |||||
return | return | ||||
} | } | ||||
@@ -217,7 +217,7 @@ func Home(ctx *middleware.Context) { | |||||
ctx.HTML(200, HOME) | ctx.HTML(200, HOME) | ||||
} | } | ||||
func renderItems(ctx *middleware.Context, total int, getter func(page int) ([]*models.User, error)) { | |||||
func RenderUserCards(ctx *middleware.Context, total int, getter func(page int) ([]*models.User, error), tpl base.TplName) { | |||||
page := ctx.QueryInt("page") | page := ctx.QueryInt("page") | ||||
if page <= 0 { | if page <= 0 { | ||||
page = 1 | page = 1 | ||||
@@ -230,21 +230,23 @@ func renderItems(ctx *middleware.Context, total int, getter func(page int) ([]*m | |||||
ctx.Handle(500, "getter", err) | ctx.Handle(500, "getter", err) | ||||
return | return | ||||
} | } | ||||
ctx.Data["Watchers"] = items | |||||
ctx.Data["Cards"] = items | |||||
ctx.HTML(200, WATCHERS) | |||||
ctx.HTML(200, tpl) | |||||
} | } | ||||
func Watchers(ctx *middleware.Context) { | func Watchers(ctx *middleware.Context) { | ||||
ctx.Data["Title"] = ctx.Tr("repo.watchers") | ctx.Data["Title"] = ctx.Tr("repo.watchers") | ||||
ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers") | |||||
ctx.Data["PageIsWatchers"] = true | ctx.Data["PageIsWatchers"] = true | ||||
renderItems(ctx, ctx.Repo.Repository.NumWatches, ctx.Repo.Repository.GetWatchers) | |||||
RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, ctx.Repo.Repository.GetWatchers, WATCHERS) | |||||
} | } | ||||
func Stars(ctx *middleware.Context) { | func Stars(ctx *middleware.Context) { | ||||
ctx.Data["Title"] = ctx.Tr("repo.stargazers") | ctx.Data["Title"] = ctx.Tr("repo.stargazers") | ||||
ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers") | |||||
ctx.Data["PageIsStargazers"] = true | ctx.Data["PageIsStargazers"] = true | ||||
renderItems(ctx, ctx.Repo.Repository.NumStars, ctx.Repo.Repository.GetStargazers) | |||||
RenderUserCards(ctx, ctx.Repo.Repository.NumStars, ctx.Repo.Repository.GetStargazers, WATCHERS) | |||||
} | } | ||||
func Forks(ctx *middleware.Context) { | func Forks(ctx *middleware.Context) { | ||||
@@ -7,7 +7,6 @@ package user | |||||
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"fmt" | "fmt" | ||||
"strings" | |||||
"github.com/Unknwon/com" | "github.com/Unknwon/com" | ||||
"github.com/Unknwon/paginater" | "github.com/Unknwon/paginater" | ||||
@@ -21,7 +20,6 @@ import ( | |||||
const ( | const ( | ||||
DASHBOARD base.TplName = "user/dashboard/dashboard" | DASHBOARD base.TplName = "user/dashboard/dashboard" | ||||
ISSUES base.TplName = "user/dashboard/issues" | ISSUES base.TplName = "user/dashboard/issues" | ||||
STARS base.TplName = "user/stars" | |||||
PROFILE base.TplName = "user/profile" | PROFILE base.TplName = "user/profile" | ||||
ORG_HOME base.TplName = "org/home" | ORG_HOME base.TplName = "org/home" | ||||
) | ) | ||||
@@ -338,67 +336,6 @@ func showOrgProfile(ctx *middleware.Context) { | |||||
ctx.HTML(200, ORG_HOME) | ctx.HTML(200, ORG_HOME) | ||||
} | } | ||||
func Profile(ctx *middleware.Context) { | |||||
ctx.Data["Title"] = "Profile" | |||||
ctx.Data["PageIsUserProfile"] = true | |||||
uname := ctx.Params(":username") | |||||
// Special handle for FireFox requests favicon.ico. | |||||
if uname == "favicon.ico" { | |||||
ctx.Redirect(setting.AppSubUrl + "/img/favicon.png") | |||||
return | |||||
} else if strings.HasSuffix(uname, ".png") { | |||||
ctx.Error(404) | |||||
return | |||||
} | |||||
isShowKeys := false | |||||
if strings.HasSuffix(uname, ".keys") { | |||||
isShowKeys = true | |||||
uname = strings.TrimSuffix(uname, ".keys") | |||||
} | |||||
u, err := models.GetUserByName(uname) | |||||
if err != nil { | |||||
if models.IsErrUserNotExist(err) { | |||||
ctx.Handle(404, "GetUserByName", err) | |||||
} else { | |||||
ctx.Handle(500, "GetUserByName", err) | |||||
} | |||||
return | |||||
} | |||||
// Show SSH keys. | |||||
if isShowKeys { | |||||
ShowSSHKeys(ctx, u.Id) | |||||
return | |||||
} | |||||
if u.IsOrganization() { | |||||
showOrgProfile(ctx) | |||||
return | |||||
} | |||||
ctx.Data["Owner"] = u | |||||
tab := ctx.Query("tab") | |||||
ctx.Data["TabName"] = tab | |||||
switch tab { | |||||
case "activity": | |||||
retrieveFeeds(ctx, u.Id, 0, true) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
default: | |||||
ctx.Data["Repos"], err = models.GetRepositories(u.Id, ctx.IsSigned && ctx.User.Id == u.Id) | |||||
if err != nil { | |||||
ctx.Handle(500, "GetRepositories", err) | |||||
return | |||||
} | |||||
} | |||||
ctx.HTML(200, PROFILE) | |||||
} | |||||
func Email2User(ctx *middleware.Context) { | func Email2User(ctx *middleware.Context) { | ||||
u, err := models.GetUserByEmail(ctx.Query("email")) | u, err := models.GetUserByEmail(ctx.Query("email")) | ||||
if err != nil { | if err != nil { | ||||
@@ -0,0 +1,145 @@ | |||||
// Copyright 2015 The Gogs 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 user | |||||
import ( | |||||
"fmt" | |||||
"strings" | |||||
"github.com/gogits/gogs/models" | |||||
"github.com/gogits/gogs/modules/base" | |||||
"github.com/gogits/gogs/modules/middleware" | |||||
"github.com/gogits/gogs/modules/setting" | |||||
"github.com/gogits/gogs/routers/repo" | |||||
) | |||||
const ( | |||||
FOLLOWERS base.TplName = "user/meta/followers" | |||||
STARS base.TplName = "user/meta/stars" | |||||
) | |||||
// GetUserByParams returns user whose name is presented in URL paramenter. | |||||
func GetUserByParams(ctx *middleware.Context) *models.User { | |||||
user, err := models.GetUserByName(ctx.Params(":username")) | |||||
if err != nil { | |||||
if models.IsErrUserNotExist(err) { | |||||
ctx.Error(404) | |||||
} else { | |||||
ctx.Handle(500, "GetUserByName", err) | |||||
} | |||||
return nil | |||||
} | |||||
return user | |||||
} | |||||
func Profile(ctx *middleware.Context) { | |||||
uname := ctx.Params(":username") | |||||
// Special handle for FireFox requests favicon.ico. | |||||
if uname == "favicon.ico" { | |||||
ctx.Redirect(setting.AppSubUrl + "/img/favicon.png") | |||||
return | |||||
} else if strings.HasSuffix(uname, ".png") { | |||||
ctx.Error(404) | |||||
return | |||||
} | |||||
isShowKeys := false | |||||
if strings.HasSuffix(uname, ".keys") { | |||||
isShowKeys = true | |||||
} | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
// Show SSH keys. | |||||
if isShowKeys { | |||||
ShowSSHKeys(ctx, u.Id) | |||||
return | |||||
} | |||||
if u.IsOrganization() { | |||||
showOrgProfile(ctx) | |||||
return | |||||
} | |||||
ctx.Data["Title"] = u.DisplayName() | |||||
ctx.Data["PageIsUserProfile"] = true | |||||
ctx.Data["Owner"] = u | |||||
tab := ctx.Query("tab") | |||||
ctx.Data["TabName"] = tab | |||||
switch tab { | |||||
case "activity": | |||||
retrieveFeeds(ctx, u.Id, 0, true) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
default: | |||||
var err error | |||||
ctx.Data["Repos"], err = models.GetRepositories(u.Id, ctx.IsSigned && ctx.User.Id == u.Id) | |||||
if err != nil { | |||||
ctx.Handle(500, "GetRepositories", err) | |||||
return | |||||
} | |||||
} | |||||
ctx.HTML(200, PROFILE) | |||||
} | |||||
func Followers(ctx *middleware.Context) { | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
ctx.Data["Title"] = u.DisplayName() | |||||
ctx.Data["CardsTitle"] = ctx.Tr("user.followers") | |||||
ctx.Data["PageIsFollowers"] = true | |||||
ctx.Data["Owner"] = u | |||||
repo.RenderUserCards(ctx, u.NumFollowers, u.GetFollowers, FOLLOWERS) | |||||
} | |||||
func Following(ctx *middleware.Context) { | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
ctx.Data["Title"] = u.DisplayName() | |||||
ctx.Data["CardsTitle"] = ctx.Tr("user.following") | |||||
ctx.Data["PageIsFollowing"] = true | |||||
ctx.Data["Owner"] = u | |||||
repo.RenderUserCards(ctx, u.NumFollowing, u.GetFollowing, FOLLOWERS) | |||||
} | |||||
func Stars(ctx *middleware.Context) { | |||||
} | |||||
func Action(ctx *middleware.Context) { | |||||
u := GetUserByParams(ctx) | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
var err error | |||||
switch ctx.Params(":action") { | |||||
case "follow": | |||||
err = models.FollowUser(ctx.User.Id, u.Id) | |||||
case "unfollow": | |||||
err = models.UnfollowUser(ctx.User.Id, u.Id) | |||||
} | |||||
if err != nil { | |||||
ctx.Handle(500, fmt.Sprintf("Action (%s)", ctx.Params(":action")), err) | |||||
return | |||||
} | |||||
redirectTo := ctx.Query("redirect_to") | |||||
if len(redirectTo) == 0 { | |||||
redirectTo = u.HomeLink() | |||||
} | |||||
ctx.Redirect(redirectTo) | |||||
} |
@@ -1 +1 @@ | |||||
0.8.12.1219 | |||||
0.8.13.1221 |
@@ -0,0 +1,47 @@ | |||||
<div class="ui container user-cards"> | |||||
<h2 class="ui dividing header"> | |||||
{{.CardsTitle}} | |||||
</h2> | |||||
<ul class="list"> | |||||
{{range .Cards}} | |||||
<li class="item ui segment"> | |||||
<a href="{{.HomeLink}}"> | |||||
<img class="avatar" src="{{.AvatarLink}}"/> | |||||
</a> | |||||
<h3 class="name"><a href="{{.HomeLink}}">{{.DisplayName}}</a></h3> | |||||
<div class="meta"> | |||||
{{if .Website}} | |||||
<span class="icon octicon octicon-link"></span> <a href="{{.Website}}" target="_blank">{{.Website}}</a> | |||||
{{else if .Location}} | |||||
<span class="icon octicon octicon-location"></span> {{.Location}} | |||||
{{else}} | |||||
<span class="icon octicon octicon-clock"></span> {{$.i18n.Tr "user.join_on"}} {{DateFmtShort .Created}} | |||||
{{end}} | |||||
</div> | |||||
</li> | |||||
{{end}} | |||||
</ul> | |||||
{{with .Page}} | |||||
{{if gt .TotalPages 1}} | |||||
<div class="center page buttons"> | |||||
<div class="ui borderless pagination menu"> | |||||
<a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.Link}}?page={{.Previous}}"{{end}}> | |||||
<i class="left arrow icon"></i> {{$.i18n.Tr "repo.issues.previous"}} | |||||
</a> | |||||
{{range .Pages}} | |||||
{{if eq .Num -1}} | |||||
<a class="disabled item">...</a> | |||||
{{else}} | |||||
<a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.Link}}?page={{.Num}}"{{end}}>{{.Num}}</a> | |||||
{{end}} | |||||
{{end}} | |||||
<a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.Link}}?page={{.Next}}"{{end}}> | |||||
{{$.i18n.Tr "repo.issues.next"}} <i class="icon right arrow"></i> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
{{end}} | |||||
{{end}} | |||||
</div> |
@@ -1,56 +1,6 @@ | |||||
{{template "base/head" .}} | {{template "base/head" .}} | ||||
<div class="repository watchers"> | <div class="repository watchers"> | ||||
{{template "repo/header" .}} | {{template "repo/header" .}} | ||||
<div class="ui container"> | |||||
<h2 class="ui dividing header"> | |||||
{{if .PageIsWatchers}} | |||||
{{.i18n.Tr "repo.watchers"}} | |||||
{{else}} | |||||
{{.i18n.Tr "repo.stargazers"}} | |||||
{{end}} | |||||
</h2> | |||||
<ul class="list"> | |||||
{{range .Watchers}} | |||||
<li class="item ui segment"> | |||||
<a href="{{.HomeLink}}"> | |||||
<img class="avatar" src="{{.AvatarLink}}"/> | |||||
</a> | |||||
<h3 class="name"><a href="{{.HomeLink}}">{{.DisplayName}}</a></h3> | |||||
<div class="meta"> | |||||
{{if .Website}} | |||||
<span class="icon octicon octicon-link"></span> <a href="{{.Website}}" target="_blank">{{.Website}}</a> | |||||
{{else if .Location}} | |||||
<span class="icon octicon octicon-location"></span> {{.Location}} | |||||
{{else}} | |||||
<span class="icon octicon octicon-clock"></span> {{$.i18n.Tr "user.join_on"}} {{DateFmtShort .Created}} | |||||
{{end}} | |||||
</div> | |||||
</li> | |||||
{{end}} | |||||
</ul> | |||||
{{with .Page}} | |||||
{{if gt .TotalPages 1}} | |||||
<div class="center page buttons"> | |||||
<div class="ui borderless pagination menu"> | |||||
<a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.Link}}?page={{.Previous}}"{{end}}> | |||||
<i class="left arrow icon"></i> {{$.i18n.Tr "repo.issues.previous"}} | |||||
</a> | |||||
{{range .Pages}} | |||||
{{if eq .Num -1}} | |||||
<a class="disabled item">...</a> | |||||
{{else}} | |||||
<a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.Link}}?page={{.Num}}"{{end}}>{{.Num}}</a> | |||||
{{end}} | |||||
{{end}} | |||||
<a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.Link}}?page={{.Next}}"{{end}}> | |||||
{{$.i18n.Tr "repo.issues.next"}} <i class="icon right arrow"></i> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
{{end}} | |||||
{{end}} | |||||
</div> | |||||
{{template "repo/user_cards" .}} | |||||
</div> | </div> | ||||
{{template "base/footer" .}} | {{template "base/footer" .}} |
@@ -0,0 +1,6 @@ | |||||
{{template "base/head" .}} | |||||
<div class="user followers"> | |||||
{{template "user/meta/header" .}} | |||||
{{template "repo/user_cards" .}} | |||||
</div> | |||||
{{template "base/footer" .}} |
@@ -0,0 +1,25 @@ | |||||
{{with .Owner}} | |||||
<div class="ui container"> | |||||
<img class="ui avatar image" src="{{.AvatarLink}}"> | |||||
<span class="header name"> | |||||
<a href="{{.HomeLink}}">{{.Name}}</a> | |||||
{{with .FullName}}({{.}}){{end}} | |||||
</span> | |||||
<div class="ui right"> | |||||
{{if or $.PageIsFollowers $.PageIsFollowing}} | |||||
{{if and $.IsSigned (ne $.SignedUserName .Name)}} | |||||
<div class="follow"> | |||||
{{if $.SignedUser.IsFollowing .Id}} | |||||
<a class="ui small basic red button" href="{{.HomeLink}}/action/unfollow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{$.i18n.Tr "user.unfollow"}}</a> | |||||
{{else}} | |||||
<a class="ui small basic green button" href="{{.HomeLink}}/action/follow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{$.i18n.Tr "user.follow"}}</a> | |||||
{{end}} | |||||
</div> | |||||
{{end}} | |||||
{{end}} | |||||
</div> | |||||
</div> | |||||
{{end}} | |||||
<div class="ui divider"></div> |
@@ -39,6 +39,33 @@ | |||||
</li> | </li> | ||||
{{end}} | {{end}} | ||||
<li><i class="icon octicon octicon-clock"></i> {{.i18n.Tr "user.join_on"}} {{DateFmtShort .Owner.Created}}</li> | <li><i class="icon octicon octicon-clock"></i> {{.i18n.Tr "user.join_on"}} {{DateFmtShort .Owner.Created}}</li> | ||||
<li> | |||||
<i class="user icon"></i> | |||||
<a href="{{.Owner.HomeLink}}/followers"> | |||||
{{.Owner.NumFollowers}} {{.i18n.Tr "user.followers"}} | |||||
</a> | |||||
- | |||||
<a href="{{.Owner.HomeLink}}/following"> | |||||
{{.Owner.NumFollowing}} {{.i18n.Tr "user.following"}} | |||||
</a> | |||||
</li> | |||||
{{/* | |||||
<li> | |||||
<i class="octicon octicon-star"></i> | |||||
<a href="{{.Owner.HomeLink}}/stars"> | |||||
{{.Owner.NumStars}} {{.i18n.Tr "user.starred"}} | |||||
</a> | |||||
</li> | |||||
*/}} | |||||
{{if and .IsSigned (ne .SignedUserName .Owner.Name)}} | |||||
<li class="follow"> | |||||
{{if .SignedUser.IsFollowing .Owner.Id}} | |||||
<a class="ui basic red button" href="{{.Link}}/action/unfollow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.unfollow"}}</a> | |||||
{{else}} | |||||
<a class="ui basic green button" href="{{.Link}}/action/follow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.follow"}}</a> | |||||
{{end}} | |||||
</li> | |||||
{{end}} | |||||
</ul> | </ul> | ||||
</div> | </div> | ||||
</div> | </div> | ||||