diff --git a/.gopmfile b/.gopmfile index 235a8a436f..25006beb7b 100644 --- a/.gopmfile +++ b/.gopmfile @@ -11,14 +11,17 @@ github.com/codegangsta/cli = github.com/go-sql-driver/mysql = github.com/go-xorm/core = github.com/go-xorm/xorm = -github.com/gogits/cache = github.com/gogits/gfm = github.com/gogits/git = github.com/gogits/oauth2 = github.com/juju2013/goldap = github.com/lib/pq = +github.com/macaron-contrib/cache = +github.com/macaron-contrib/captcha = +github.com/macaron-contrib/csrf = github.com/macaron-contrib/i18n = github.com/macaron-contrib/session = +github.com/macaron-contrib/toolbox = github.com/nfnt/resize = [res] diff --git a/cmd/web.go b/cmd/web.go index 03704c64de..03e5e86066 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -232,6 +232,7 @@ func runWeb(*cli.Context) { m.Group("/:org", func(r *macaron.Router) { r.Get("/dashboard", user.Dashboard) r.Get("/members", org.Members) + r.Get("/members/action/:action", org.MembersAction) r.Get("/teams", org.Teams) r.Get("/teams/:team", org.SingleTeam) @@ -248,6 +249,10 @@ func runWeb(*cli.Context) { r.Route("/delete", "GET,POST", org.SettingsDelete) }) }, middleware.OrgAssignment(true, true, true)) + + m.Group("/:org", func(r *macaron.Router) { + r.Route("/invitations/new", "GET,POST", org.Invitation) + }, middleware.OrgAssignment(true, false, false, true)) }, reqSignIn) // Repository routers. diff --git a/conf/app.ini b/conf/app.ini index ac1c6a3ba9..99ed628ec1 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -254,5 +254,5 @@ DRIVER = CONN = [i18n] -LANGS = en-US,zh-CN -NAMES = English,简体中文 +LANGS = en-US,zh-CN,de-DE +NAMES = English,简体中文,Deutsch diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index d7b5c45a02..d44dca081b 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -250,6 +250,17 @@ settings.delete_account = Delete This Organization settings.delete_prompt = The operation will delete this organization permanently, and CANNOT be undo! settings.confirm_delete_account = Confirm Deletion +members.public = Public +members.public_helper = make private +members.private = Private +members.private_helper = make public +members.owner = Owner +members.member = Member +members.conceal = Conceal +members.remove = Remove +members.invite_desc = Start typing a username to invite a new member to %s: +members.invite_now = Invite Now + [action] create_repo = created repository %s commit_repo = pushed to %s at %s diff --git a/conf/locale/locale_zh-CN.ini b/conf/locale/locale_zh-CN.ini index 2499e12154..b84aca3d9e 100644 --- a/conf/locale/locale_zh-CN.ini +++ b/conf/locale/locale_zh-CN.ini @@ -250,6 +250,17 @@ settings.delete_account = 删除当前组织 settings.delete_prompt = 删除操作会永久清除该组织的信息,并且 不可恢复! settings.confirm_delete_account = 确认删除组织 +members.public = 公开成员 +members.public_helper = 设为私有 +members.private = 私有成员 +members.private_helper = 设为公开 +members.owner = 管理员 +members.member = 普通成员 +members.conceal = 隐藏身份 +members.remove = 移除成员 +members.invite_desc = 请输入被邀请到组织 %s 的用户名称: +members.invite_now = 立即邀请 + [action] create_repo = 创建了仓库 %s commit_repo = 推送了 %s 分支的代码到 %s diff --git a/gogs.go b/gogs.go index 4a3e353cdf..6a9ded37d5 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.4.7.0814 Alpha" +const APP_VER = "0.4.7.0815 Alpha" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/org.go b/models/org.go index edae828b54..b40b313bc3 100644 --- a/models/org.go +++ b/models/org.go @@ -59,6 +59,16 @@ func (org *User) GetMembers() error { return nil } +// AddMember adds new member to organization. +func (org *User) AddMember(uid int64) error { + return AddOrgUser(org.Id, uid) +} + +// RemoveMember removes member from organization. +func (org *User) RemoveMember(uid int64) error { + return RemoveOrgUser(org.Id, uid) +} + // CreateOrganization creates record of a new organization. func CreateOrganization(org, owner *User) (*User, error) { if !IsLegalName(org.Name) { @@ -241,8 +251,7 @@ func NewTeam(t *Team) error { } // Update organization number of teams. - rawSql := "UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, t.OrgId); err != nil { + if _, err = sess.Exec("UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?", t.OrgId); err != nil { sess.Rollback() return err } @@ -270,8 +279,8 @@ func UpdateTeam(t *Team) error { // OrgUser represents an organization-user relation. type OrgUser struct { Id int64 - Uid int64 `xorm:"INDEX"` - OrgId int64 `xorm:"INDEX"` + Uid int64 `xorm:"INDEX UNIQUE(s)"` + OrgId int64 `xorm:"INDEX UNIQUE(s)"` IsPublic bool IsOwner bool NumTeam int @@ -289,6 +298,12 @@ func IsOrganizationMember(orgId, uid int64) bool { return has } +// IsPublicMembership returns ture if given user public his/her membership. +func IsPublicMembership(orgId, uid int64) bool { + has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).And("is_public=?", true).Get(new(OrgUser)) + return has +} + // GetOrgUsersByUserId returns all organization-user relations by user ID. func GetOrgUsersByUserId(uid int64) ([]*OrgUser, error) { ous := make([]*OrgUser, 0, 10) @@ -303,6 +318,77 @@ func GetOrgUsersByOrgId(orgId int64) ([]*OrgUser, error) { return ous, err } +// ChangeOrgUserStatus changes public or private membership status. +func ChangeOrgUserStatus(orgId, uid int64, public bool) error { + ou := new(OrgUser) + has, err := x.Where("uid=?", uid).And("org_id=?", orgId).Get(ou) + if err != nil { + return err + } else if !has { + return nil + } + + ou.IsPublic = public + _, err = x.Id(ou.Id).AllCols().Update(ou) + return err +} + +// AddOrgUser adds new user to given organization. +func AddOrgUser(orgId, uid int64) error { + if IsOrganizationMember(orgId, uid) { + return nil + } + + ou := &OrgUser{ + Uid: uid, + OrgId: orgId, + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if _, err := sess.Insert(ou); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgId); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +// RemoveOrgUser removes user from given organization. +func RemoveOrgUser(orgId, uid int64) error { + ou := new(OrgUser) + + has, err := x.Where("uid=?", uid).And("org_id=?", orgId).Get(ou) + if err != nil { + return err + } else if !has { + return nil + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if _, err := sess.Id(ou.Id).Delete(ou); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members - 1 WHERE id = ?", orgId); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + // ___________ ____ ___ // \__ ___/___ _____ _____ | | \______ ___________ // | |_/ __ \\__ \ / \| | / ___// __ \_ __ \ diff --git a/models/user.go b/models/user.go index f4526b51d1..757c290b9c 100644 --- a/models/user.go +++ b/models/user.go @@ -128,6 +128,16 @@ func (u *User) IsOrganization() bool { return u.Type == ORGANIZATION } +// IsUserOrgOwner returns true if user is in the owner team of given organization. +func (u *User) IsUserOrgOwner(orgId int64) bool { + return IsOrganizationOwner(orgId, u.Id) +} + +// IsPublicMember returns true if user public his/her membership in give organization. +func (u *User) IsPublicMember(orgId int64) bool { + return IsPublicMembership(orgId, u.Id) +} + // GetOrganizationCount returns count of membership of organization of user. func (u *User) GetOrganizationCount() (int64, error) { return x.Where("uid=?", u.Id).Count(new(OrgUser)) diff --git a/modules/middleware/context.go b/modules/middleware/context.go index aa1266d649..6ce0f6e1f7 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -68,7 +68,9 @@ type Context struct { Org struct { IsOwner bool IsMember bool + IsAdminTeam bool // In owner team or team that has admin permission level. Organization *models.User + OrgLink string } } @@ -181,7 +183,6 @@ func Contexter() macaron.Handler { Flash: f, Session: sess, } - // Compute current URL for real-time change language. link := ctx.Req.RequestURI i := strings.Index(link, "?") diff --git a/modules/middleware/org.go b/modules/middleware/org.go index 05316a518f..77e999a328 100644 --- a/modules/middleware/org.go +++ b/modules/middleware/org.go @@ -13,8 +13,9 @@ import ( func OrgAssignment(redirect bool, args ...bool) macaron.Handler { return func(ctx *Context) { var ( - requireMember bool - requireOwner bool + requireMember bool + requireOwner bool + requireAdminTeam bool ) if len(args) >= 1 { requireMember = args[0] @@ -22,6 +23,9 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { if len(args) >= 2 { requireOwner = args[1] } + if len(args) >= 3 { + requireAdminTeam = args[2] + } orgName := ctx.Params(":org") @@ -43,13 +47,24 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Org.IsOwner = ctx.Org.Organization.IsOrgOwner(ctx.User.Id) if ctx.Org.IsOwner { ctx.Org.IsMember = true + ctx.Org.IsAdminTeam = true } else { - ctx.Org.IsMember = ctx.Org.Organization.IsOrgMember(ctx.User.Id) + if ctx.Org.Organization.IsOrgMember(ctx.User.Id) { + ctx.Org.IsMember = true + // TODO: ctx.Org.IsAdminTeam + } } } - if (requireMember && !ctx.Org.IsMember) || (requireOwner && !ctx.Org.IsOwner) { + if (requireMember && !ctx.Org.IsMember) || + (requireOwner && !ctx.Org.IsOwner) || + (requireAdminTeam && !ctx.Org.IsAdminTeam) { ctx.Handle(404, "OrgAssignment", err) return } + ctx.Data["IsAdminTeam"] = ctx.Org.IsAdminTeam + ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner + + ctx.Org.OrgLink = "/org/" + ctx.Org.Organization.Name + ctx.Data["OrgLink"] = ctx.Org.OrgLink } } diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index a028aab802..3db1932af0 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -146,6 +146,7 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { } ctx.Repo.GitRepo = gitRepo ctx.Repo.RepoLink = "/" + u.Name + "/" + repo.Name + ctx.Data["RepoLink"] = ctx.Repo.RepoLink tags, err := ctx.Repo.GitRepo.GetTags() if err != nil { @@ -157,7 +158,6 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Data["Title"] = u.Name + "/" + repo.Name ctx.Data["Repository"] = repo ctx.Data["Owner"] = ctx.Repo.Repository.Owner - ctx.Data["RepoLink"] = ctx.Repo.RepoLink ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner ctx.Data["IsRepositoryTrueOwner"] = ctx.Repo.IsTrueOwner diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css index 93a5d2f8c9..e7a3a66f26 100644 --- a/public/ng/css/gogs.css +++ b/public/ng/css/gogs.css @@ -851,6 +851,7 @@ The dashboard page style margin-left: 1em; } #dashboard-news .push-news .news-content li img { + vertical-align: inherit; margin-bottom: -2px; } /* @@ -1691,6 +1692,30 @@ textarea#issue-add-content { #org-home-header { min-height: 100px; } +#org-header { + height: 48px; +} +#org-header .org-name { + padding-left: 10px; + font-size: 1.4em; + height: 50px; + line-height: 50px; + margin-bottom: 0; +} +#org-header > div > .menu-line > li.right > a { + font-size: 1.2em; + color: #444444; +} +#org-header > div > .menu-line > li.right > a:hover { + background-color: transparent; + color: #d9453d; +} +#org-header > div > .menu-line > li.right > a .octicon { + margin-right: 6px; +} +#org-header > div > .menu-line > li.right .current { + border-bottom: 2px solid #D26911; +} #org-home-header-info { padding-top: 10px; } @@ -1776,3 +1801,30 @@ textarea#issue-add-content { margin-bottom: 0; color: #777; } +#org-member-toolbar { + padding: 10px 0; +} +#org-member-list .org-member-item { + height: 50px; + line-height: 50px; + border-top: 1px solid #eee; + padding: 15px 20px; +} +#org-member-list .org-member-item .member-name { + padding-left: 15px; +} +#org-member-list .org-member-item ul { + list-style: none; +} +#org-member-list .org-member-item ul li { + text-align: center; + display: inline-block; +} +.invite-box { + padding: 50px 0; + min-height: 130px; + text-align: center; +} +.invite-box input { + width: 250px; +} diff --git a/public/ng/less/gogs/dashboard.less b/public/ng/less/gogs/dashboard.less index f8838ae6fd..6bf1150ba7 100644 --- a/public/ng/less/gogs/dashboard.less +++ b/public/ng/less/gogs/dashboard.less @@ -251,6 +251,7 @@ The dashboard page style .news-content li { margin-left: 1em; img { + vertical-align: inherit; margin-bottom: -2px; } } diff --git a/public/ng/less/gogs/organization.less b/public/ng/less/gogs/organization.less index ed8c05397a..cbdec3132e 100644 --- a/public/ng/less/gogs/organization.less +++ b/public/ng/less/gogs/organization.less @@ -9,6 +9,38 @@ #org-home-header { min-height: 100px; } +#org-header { + height: 48px; + .org-name { + padding-left: 10px; + font-size: 1.4em; + height: 50px; + line-height: 50px; + margin-bottom: 0; + } + > div { + > .menu-line { + > li { + &.right { + > a { + font-size: 1.2em; + color: @dashboardHeaderLinkColor; + &:hover { + background-color: transparent; + color: @dashboardHeaderLinkHoverColor; + } + .octicon { + margin-right: 6px; + } + } + .current { + border-bottom: 2px solid #D26911; + } + } + } + } + } +} #org-home-header-info { padding-top: 10px; h2 { @@ -93,4 +125,33 @@ margin-top: 0; margin-bottom: 0; color: #777; +} +#org-member-toolbar { + padding: 10px 0; +} +#org-member-list { + .org-member-item { + height: 50px; + line-height: 50px; + border-top: 1px solid #eee; + padding: 15px 20px; + .member-name { + padding-left: 15px; + } + ul { + list-style: none; + li { + text-align: center; + display: inline-block; + } + } + } +} +.invite-box { + padding: 50px 0; + min-height: 130px; + text-align: center; + input { + width: 250px; + } } \ No newline at end of file diff --git a/public/ng/less/ui/label.less b/public/ng/less/ui/label.less index 1cf0a81d85..a2a8a67905 100644 --- a/public/ng/less/ui/label.less +++ b/public/ng/less/ui/label.less @@ -1,8 +1,8 @@ @import "var"; .label { - padding: 2px 6px; - color: @labelFontColor; + padding: 2px 6px; + color: @labelFontColor; } .label-red { @@ -30,7 +30,7 @@ } .label-radius{ - border-radius: .2em; + border-radius: .2em; } .label-link{ diff --git a/routers/org/members.go b/routers/org/members.go index ac278d4e6d..d98061765a 100644 --- a/routers/org/members.go +++ b/routers/org/members.go @@ -5,10 +5,101 @@ package org import ( + "github.com/Unknwon/com" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware" ) +const ( + MEMBERS base.TplName = "org/members" + INVITE base.TplName = "org/invite" +) + func Members(ctx *middleware.Context) { - ctx.Data["Title"] = "Organization " + ctx.Params(":org") + " Members" - ctx.HTML(200, "org/members") + org := ctx.Org.Organization + ctx.Data["Title"] = org.Name + ctx.Data["PageIsOrgMembers"] = true + + if err := org.GetMembers(); err != nil { + ctx.Handle(500, "GetMembers", err) + return + } + ctx.Data["Members"] = org.Members + + ctx.HTML(200, MEMBERS) +} + +func MembersAction(ctx *middleware.Context) { + uid := com.StrTo(ctx.Query("uid")).MustInt64() + if uid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/members") + return + } + + org := ctx.Org.Organization + var err error + switch ctx.Params(":action") { + case "private": + if ctx.User.Id != uid && !ctx.Org.IsOwner { + ctx.Error(404) + return + } + err = models.ChangeOrgUserStatus(org.Id, uid, false) + case "public": + if ctx.User.Id != uid { + ctx.Error(404) + return + } + err = models.ChangeOrgUserStatus(org.Id, uid, true) + case "remove": + if !ctx.Org.IsOwner { + ctx.Error(404) + return + } + err = org.RemoveMember(uid) + } + + if err != nil { + log.Error(4, "Action(%s): %v", ctx.Params(":action"), err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + ctx.Redirect(ctx.Org.OrgLink + "/members") +} + +func Invitation(ctx *middleware.Context) { + org := ctx.Org.Organization + ctx.Data["Title"] = org.Name + ctx.Data["PageIsOrgMembers"] = true + + if ctx.Req.Method == "POST" { + uname := ctx.Query("uname") + u, err := models.GetUserByName(uname) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + ctx.Redirect(ctx.Org.OrgLink + "/invitations/new") + } else { + ctx.Handle(500, " GetUserByName", err) + } + return + } + + if err = org.AddMember(u.Id); err != nil { + ctx.Handle(500, " AddMember", err) + return + } + + log.Trace("New member added(%s): %s", org.Name, u.Name) + ctx.Redirect(ctx.Org.OrgLink + "/members") + return + } + + ctx.HTML(200, INVITE) } diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 3450ea76f7..7356f50330 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -227,7 +227,7 @@ func Action(ctx *middleware.Context) { } if err != nil { - log.Error(4, "repo.Action(%s): %v", ctx.Params(":action"), err) + log.Error(4, "Action(%s): %v", ctx.Params(":action"), err) ctx.JSON(200, map[string]interface{}{ "ok": false, "err": err.Error(), diff --git a/templates/.VERSION b/templates/.VERSION index f398e90110..906c01dc84 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.4.7.0814 Alpha \ No newline at end of file +0.4.7.0815 Alpha \ No newline at end of file diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl new file mode 100644 index 0000000000..8566d0a3c5 --- /dev/null +++ b/templates/org/header.tmpl @@ -0,0 +1,16 @@ +