diff --git a/cmd/web.go b/cmd/web.go index 03e5e8606..064a0358f 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -236,6 +236,7 @@ func runWeb(*cli.Context) { r.Get("/teams", org.Teams) r.Get("/teams/:team", org.SingleTeam) + r.Get("/teams/:team/action/:action", org.TeamsAction) }, middleware.OrgAssignment(true, true)) m.Group("/:org", func(r *macaron.Router) { @@ -248,11 +249,9 @@ func runWeb(*cli.Context) { r.Post("", bindIgnErr(auth.UpdateOrgSettingForm{}), org.SettingsPost) 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)) + }, middleware.OrgAssignment(true, true, true)) }, reqSignIn) // Repository routers. diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index d44dca081..4e191dca4 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -79,6 +79,7 @@ Retype = Re-type password SSHTitle = SSH key name HttpsUrl = HTTPS URL PayloadUrl = Payload URL +TeamName = Team name require_error = ` cannot be empty.` alpha_dash_error = ` must be valid alpha or numeric or dash(-_) characters.` @@ -94,16 +95,19 @@ password_not_match = Password and re-type password are not same. username_been_taken = Username has been already taken. repo_name_been_taken = Repository name has been already taken. org_name_been_taken = Organization name has been already taken. +team_name_been_taken = Team name has been already taken. email_been_used = E-mail address has been already used. ssh_key_been_used = Public key name has been used. illegal_username = Your username contains illegal characters. illegal_repo_name = Repository name contains illegal characters. illegal_org_name = Organization name contains illegal characters. +illegal_team_name = Team name contains illegal characters. username_password_incorrect = Username or password is not correct. enterred_invalid_repo_name = Please make sure you entered repository name is correct. enterred_invalid_owner_name = Please make sure you entered owner name is correct. enterred_invalid_password = Please make sure you entered password is correct. user_not_exist = Given user does not exist. +last_org_owner = The user to remove is the last member in owner team. There must be another owner. invalid_ssh_key = Sorry, we're not able to verify your SSH key: %s auth_failed = Authentication failed: %v @@ -237,6 +241,11 @@ lower_members = members lower_repositories = repositories create_new_team = Create New Team org_desc = Description +team_name = Team Name +team_desc = Description +team_name_helper = You'll use this name to mention this team in conversations. +team_desc_helper = What is this team all about? +team_permission_desc = What permission level should this team have? settings = Settings settings.options = Options @@ -258,9 +267,19 @@ members.owner = Owner members.member = Member members.conceal = Conceal members.remove = Remove +members.leave = Leave members.invite_desc = Start typing a username to invite a new member to %s: members.invite_now = Invite Now +teams.join = Join +teams.leave = Leave +teams.read_access = Read Access +teams.read_access_helper = This team will be able to view and clone its repositories. +teams.write_access = Write Access +teams.write_access_helper = This team will be able to read its repositories, as well as push to them. +teams.admin_access = Admin Access +teams.admin_access_helper = This team will be able to push/pull to its repositories, as well as add other collaborators to them. + [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 b84aca3d9..e3607d8e6 100644 --- a/conf/locale/locale_zh-CN.ini +++ b/conf/locale/locale_zh-CN.ini @@ -79,6 +79,7 @@ Retype = 确认密码 SSHTitle = SSH 密钥名称 HttpsUrl = HTTPS URL 地址 PayloadUrl = 推送地址 +TeamName = 团队名称 require_error = 不能为空。 alpha_dash_error = 必须为英文字母、阿拉伯数字或横线(-_)。 @@ -94,16 +95,19 @@ password_not_match = 密码与确认密码未匹配。 username_been_taken = 用户名已经被占用。 repo_name_been_taken = 仓库名称已经被占用。 org_name_been_taken = 组织名称已经被占用。 +team_name_been_taken = 团队名称已经被占用。 email_been_used = 邮箱地址已经被使用。 ssh_key_been_used = SSH 密钥已经被使用。 illegal_username = 您的用户名包含非法字符。 illegal_repo_name = 仓库名称包含非法字符。 illegal_org_name = 组织名称包含非法字符。 +illegal_team_name = 团队名称包含非法字符。 username_password_incorrect = 用户名或密码不正确。 enterred_invalid_repo_name = 请检查您输入的仓库名称是正确。 enterred_invalid_owner_name = 请检查您输入的新所有者用户名是否正确。 enterred_invalid_password = 请检查您输入的密码是否正确。 user_not_exist = 被操作的用户不存在! +last_org_owner = 被移除用户为最后一位管理员。请添加一位新的管理员再进行移除成员操作! invalid_ssh_key = 很抱歉,我们无法验证您输入的 SSH 密钥:%s auth_failed = 授权验证失败:%v @@ -237,6 +241,11 @@ lower_members = 名成员 lower_repositories = 个仓库 create_new_team = 创建新的团队 org_desc = 组织描述 +team_name = 团队名称 +team_desc = 团队描述 +team_name_helper = 您可以使用该名称来通知改组全体成员。 +team_desc_helper = 一句话描述这个团队是做什么的。 +team_permission_desc = 请选择该团队所具有的权限等级: settings = 组织设置 settings.options = 基本设置 @@ -258,9 +267,19 @@ members.owner = 管理员 members.member = 普通成员 members.conceal = 隐藏身份 members.remove = 移除成员 +members.leave = 离开组织 members.invite_desc = 请输入被邀请到组织 %s 的用户名称: members.invite_now = 立即邀请 +teams.join = 加入团队 +teams.leave = 离开团队 +teams.read_access = 读取权限 +teams.read_access_helper = 这个团队将拥有查看和克隆所属仓库的权限。 +teams.write_access = 写入权限 +teams.write_access_helper = 这个团队将拥有查看、克隆和推送所属仓库的权限。 +teams.admin_access = 管理权限 +teams.admin_access_helper = 这个团队将拥有查看、克隆、推送和添加其他组织成员到团队的权限。 + [action] create_repo = 创建了仓库 %s commit_repo = 推送了 %s 分支的代码到 %s diff --git a/gogs.go b/gogs.go index 6a9ded37d..f20f0a10d 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.4.7.0815 Alpha" +const APP_VER = "0.4.7.0816 Alpha" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/org.go b/models/org.go index b40b313bc..b81042228 100644 --- a/models/org.go +++ b/models/org.go @@ -15,6 +15,9 @@ import ( var ( ErrOrgNotExist = errors.New("Organization does not exist") ErrTeamAlreadyExist = errors.New("Team already exist") + ErrTeamNotExist = errors.New("Team does not exist") + ErrTeamNameIllegal = errors.New("Team name contains illegal characters") + ErrLastOrgOwner = errors.New("The user to remove is the last member in owner team") ) // IsOrgOwner returns true if given user is in the owner team. @@ -27,14 +30,14 @@ func (org *User) IsOrgMember(uid int64) bool { return IsOrganizationMember(org.Id, uid) } +// GetTeam returns named team of organization. +func (org *User) GetTeam(name string) (*Team, error) { + return GetTeam(org.Id, name) +} + // GetOwnerTeam returns owner team of organization. func (org *User) GetOwnerTeam() (*Team, error) { - t := &Team{ - OrgId: org.Id, - Name: OWNER_TEAM, - } - _, err := x.Get(t) - return t, err + return org.GetTeam(OWNER_TEAM) } // GetTeams returns all teams that belong to organization. @@ -179,96 +182,6 @@ func DeleteOrganization(org *User) (err error) { return sess.Commit() } -// ___________ -// \__ ___/___ _____ _____ -// | |_/ __ \\__ \ / \ -// | |\ ___/ / __ \| Y Y \ -// |____| \___ >____ /__|_| / -// \/ \/ \/ - -type AuthorizeType int - -const ( - ORG_READABLE AuthorizeType = iota + 1 - ORG_WRITABLE - ORG_ADMIN -) - -const OWNER_TEAM = "Owners" - -// Team represents a organization team. -type Team struct { - Id int64 - OrgId int64 `xorm:"INDEX"` - LowerName string - Name string - Description string - Authorize AuthorizeType - RepoIds string `xorm:"TEXT"` - NumMembers int - NumRepos int - Members []*User `xorm:"-"` -} - -// IsTeamMember returns true if given user is a member of team. -func (t *Team) IsMember(uid int64) bool { - return IsTeamMember(t.OrgId, t.Id, uid) -} - -// GetMembers returns all members in given team of organization. -func (t *Team) GetMembers() (err error) { - t.Members, err = GetTeamMembers(t.OrgId, t.Id) - return err -} - -// NewTeam creates a record of new team. -// It's caller's responsibility to assign organization ID. -func NewTeam(t *Team) error { - has, err := x.Id(t.OrgId).Get(new(User)) - if err != nil { - return err - } else if !has { - return ErrOrgNotExist - } - - t.LowerName = strings.ToLower(t.Name) - has, err = x.Where("org_id=?", t.OrgId).And("lower_name=?", t.LowerName).Get(new(Team)) - if err != nil { - return err - } else if has { - return ErrTeamAlreadyExist - } - - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if _, err = sess.Insert(t); err != nil { - sess.Rollback() - return err - } - - // Update organization number of teams. - if _, err = sess.Exec("UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?", t.OrgId); err != nil { - sess.Rollback() - return err - } - return sess.Commit() -} - -// UpdateTeam updates information of team. -func UpdateTeam(t *Team) error { - if len(t.Description) > 255 { - t.Description = t.Description[:255] - } - - t.LowerName = strings.ToLower(t.Name) - _, err := x.Id(t.Id).AllCols().Update(t) - return err -} - // ________ ____ ___ // \_____ \_______ ____ | | \______ ___________ // / | \_ __ \/ ___\| | / ___// __ \_ __ \ @@ -372,6 +285,21 @@ func RemoveOrgUser(orgId, uid int64) error { return nil } + // Check if the user to delete is the last member in owner team. + if IsOrganizationOwner(orgId, uid) { + org, err := GetUserById(orgId) + if err != nil { + return err + } + t, err := org.GetOwnerTeam() + if err != nil { + return err + } + if t.NumMembers == 1 { + return ErrLastOrgOwner + } + } + sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { @@ -389,6 +317,127 @@ func RemoveOrgUser(orgId, uid int64) error { return sess.Commit() } +// ___________ +// \__ ___/___ _____ _____ +// | |_/ __ \\__ \ / \ +// | |\ ___/ / __ \| Y Y \ +// |____| \___ >____ /__|_| / +// \/ \/ \/ + +type AuthorizeType int + +const ( + ORG_READABLE AuthorizeType = iota + 1 + ORG_WRITABLE + ORG_ADMIN +) + +const OWNER_TEAM = "Owners" + +// Team represents a organization team. +type Team struct { + Id int64 + OrgId int64 `xorm:"INDEX"` + LowerName string + Name string + Description string + Authorize AuthorizeType + RepoIds string `xorm:"TEXT"` + NumMembers int + NumRepos int + Members []*User `xorm:"-"` +} + +// IsTeamMember returns true if given user is a member of team. +func (t *Team) IsMember(uid int64) bool { + return IsTeamMember(t.OrgId, t.Id, uid) +} + +// GetMembers returns all members in given team of organization. +func (t *Team) GetMembers() (err error) { + t.Members, err = GetTeamMembers(t.OrgId, t.Id) + return err +} + +// NewTeam creates a record of new team. +// It's caller's responsibility to assign organization ID. +func NewTeam(t *Team) error { + if !IsLegalName(t.Name) { + return ErrTeamNameIllegal + } + + has, err := x.Id(t.OrgId).Get(new(User)) + if err != nil { + return err + } else if !has { + return ErrOrgNotExist + } + + t.LowerName = strings.ToLower(t.Name) + has, err = x.Where("org_id=?", t.OrgId).And("lower_name=?", t.LowerName).Get(new(Team)) + if err != nil { + return err + } else if has { + return ErrTeamAlreadyExist + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Insert(t); err != nil { + sess.Rollback() + return err + } + + // Update organization number of teams. + if _, err = sess.Exec("UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?", t.OrgId); err != nil { + sess.Rollback() + return err + } + return sess.Commit() +} + +// GetTeam returns team by given team name and organization. +func GetTeam(orgId int64, name string) (*Team, error) { + t := &Team{ + OrgId: orgId, + LowerName: strings.ToLower(name), + } + has, err := x.Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTeamNotExist + } + return t, nil +} + +// GetTeamById returns team by given ID. +func GetTeamById(teamId int64) (*Team, error) { + t := new(Team) + has, err := x.Id(teamId).Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTeamNotExist + } + return t, nil +} + +// UpdateTeam updates information of team. +func UpdateTeam(t *Team) error { + if len(t.Description) > 255 { + t.Description = t.Description[:255] + } + + t.LowerName = strings.ToLower(t.Name) + _, err := x.Id(t.Id).AllCols().Update(t) + return err +} + // ___________ ____ ___ // \__ ___/___ _____ _____ | | \______ ___________ // | |_/ __ \\__ \ / \| | / ___// __ \_ __ \ @@ -427,3 +476,68 @@ func GetTeamMembers(orgId, teamId int64) ([]*User, error) { } return us, nil } + +// AddTeamMember adds new member to given team of given organization. +func AddTeamMember(orgId, teamId, uid int64) error { + if !IsOrganizationMember(orgId, uid) || IsTeamMember(orgId, teamId, uid) { + return nil + } + + // We can use raw SQL here but we also want to vertify there is a such team. + t, err := GetTeamById(teamId) + if err != nil { + return err + } + t.NumMembers++ + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + tu := &TeamUser{ + Uid: uid, + OrgId: orgId, + TeamId: teamId, + } + + if _, err = sess.Insert(tu); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Id(t.Id).Update(t); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +// RemoveMember removes member from given team of given organization. +func RemoveMember(orgId, teamId, uid int64) error { + if !IsTeamMember(orgId, teamId, uid) { + return nil + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + tu := &TeamUser{ + Uid: uid, + OrgId: orgId, + TeamId: teamId, + } + + if _, err := sess.Delete(tu); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Exec("UPDATE `team` SET num_members = num_members - 1 WHERE id = ?", teamId); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} diff --git a/modules/auth/org.go b/modules/auth/org.go index 9598e6f83..6183e8c82 100644 --- a/modules/auth/org.go +++ b/modules/auth/org.go @@ -49,7 +49,7 @@ func (f *UpdateOrgSettingForm) Validate(ctx *macaron.Context, errs *binding.Erro // \/ \/ \/ type CreateTeamForm struct { - TeamName string `form:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` + TeamName string `form:"team_name" binding:"Required;AlphaDashDot;MaxSize(30)"` Description string `form:"desc" binding:"MaxSize(255)"` Permission string `form:"permission"` } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index 6ce0f6e1f..80975e999 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -71,6 +71,8 @@ type Context struct { IsAdminTeam bool // In owner team or team that has admin permission level. Organization *models.User OrgLink string + + Team *models.Team } } diff --git a/modules/middleware/org.go b/modules/middleware/org.go index 77e999a32..c85221a5a 100644 --- a/modules/middleware/org.go +++ b/modules/middleware/org.go @@ -41,15 +41,16 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { } return } - ctx.Data["Org"] = ctx.Org.Organization + org := ctx.Org.Organization + ctx.Data["Org"] = org if ctx.IsSigned { - ctx.Org.IsOwner = ctx.Org.Organization.IsOrgOwner(ctx.User.Id) + ctx.Org.IsOwner = org.IsOrgOwner(ctx.User.Id) if ctx.Org.IsOwner { ctx.Org.IsMember = true ctx.Org.IsAdminTeam = true } else { - if ctx.Org.Organization.IsOrgMember(ctx.User.Id) { + if org.IsOrgMember(ctx.User.Id) { ctx.Org.IsMember = true // TODO: ctx.Org.IsAdminTeam } @@ -64,7 +65,24 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Data["IsAdminTeam"] = ctx.Org.IsAdminTeam ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner - ctx.Org.OrgLink = "/org/" + ctx.Org.Organization.Name + ctx.Org.OrgLink = "/org/" + org.Name ctx.Data["OrgLink"] = ctx.Org.OrgLink + + // Team. + teamName := ctx.Params(":team") + if len(teamName) > 0 { + ctx.Org.Team, err = org.GetTeam(teamName) + if err != nil { + if err == models.ErrTeamNotExist { + ctx.Handle(404, "GetTeam", err) + } else if redirect { + ctx.Redirect("/") + } else { + ctx.Handle(500, "GetTeam", err) + } + return + } + ctx.Data["Team"] = ctx.Org.Team + } } } diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css index 6b400df28..3d784fab2 100644 --- a/public/ng/css/gogs.css +++ b/public/ng/css/gogs.css @@ -1172,28 +1172,34 @@ The register and sign-in page style width: 520px; } /* repository create */ +#team-create-form, #repo-migrate-form, #repo-create-form { width: 800px; margin: 60px auto auto auto; background: white; } +#team-create-form h2, #repo-migrate-form h2, #repo-create-form h2 { margin: .5em 1em; } +#team-create-form .field, #repo-migrate-form .field, #repo-create-form .field { margin: 1.2em 0 2em 0; } +#team-create-form .ipt, #repo-migrate-form .ipt, #repo-create-form .ipt { width: 540px; } +#team-create-form textarea, #repo-migrate-form textarea, #repo-create-form textarea { height: 120px; } +#team-create-form .avatar, #repo-migrate-form .avatar, #repo-create-form .avatar { vertical-align: middle; @@ -1201,6 +1207,7 @@ The register and sign-in page style width: 28px; height: 28px; } +#team-create-form:hover, #repo-migrate-form:hover, #repo-create-form:hover { box-shadow: 0px 0px 6px #CCC; @@ -1681,6 +1688,9 @@ textarea#issue-add-content { box-sizing: border-box; height: 120px; } +.org-header-alert .alert { + margin-top: 10px; +} .org-header { padding: 16px 0; background-color: #FFF; @@ -1767,10 +1777,10 @@ textarea#issue-add-content { .org-sidebar .panel-footer { padding: .8em 1.2em; } -#org-member-avatar-group { +.org-sidebar .member-avatar-group { padding: 15px; } -#org-member-avatar-group img { +.org-sidebar .member-avatar-group img { width: 59px; height: 59px; border-radius: 3px; @@ -1801,13 +1811,14 @@ textarea#issue-add-content { margin-bottom: 0; color: #777; } -#org-member-toolbar { +.org-toolbar { padding: 10px 0; + border-bottom: 1px solid #eee; } #org-member-list .org-member-item { height: 50px; line-height: 50px; - border-top: 1px solid #eee; + border-bottom: 1px solid #eee; padding: 15px 20px; } #org-member-list .org-member-item .member-name { @@ -1832,3 +1843,19 @@ textarea#issue-add-content { #org-member-list-block { padding-top: 2px; } +.org-team-list .org-team-list-item { + float: left; + padding: 15px; + width: 555px; +} +.org-team-list .org-team-list-item .member-avatar-group { + padding: 5px 15px; +} +.org-team-list .org-team-list-item .member-avatar-group img { + width: 38px; + height: 38px; + border-radius: 3px; +} +#team-create-form .note { + margin-left: 153px; +} diff --git a/public/ng/less/gogs/organization.less b/public/ng/less/gogs/organization.less index 380582953..e8063f232 100644 --- a/public/ng/less/gogs/organization.less +++ b/public/ng/less/gogs/organization.less @@ -1,3 +1,6 @@ +.org-header-alert .alert { + margin-top: 10px; +} .org-header { padding: 16px 0; background-color: #FFF; @@ -91,13 +94,13 @@ .panel-footer { padding: .8em 1.2em; } -} -#org-member-avatar-group { - padding: 15px; - img { - width: 59px; - height: 59px; - border-radius: 3px; + .member-avatar-group { + padding: 15px; + img { + width: 59px; + height: 59px; + border-radius: 3px; + } } } #org-home-team-list { @@ -126,14 +129,15 @@ margin-bottom: 0; color: #777; } -#org-member-toolbar { +.org-toolbar { padding: 10px 0; + border-bottom: 1px solid #eee; } #org-member-list { .org-member-item { height: 50px; line-height: 50px; - border-top: 1px solid #eee; + border-bottom: 1px solid #eee; padding: 15px 20px; .member-name { padding-left: 15px; @@ -158,4 +162,24 @@ } #org-member-list-block { padding-top: 2px; +} +.org-team-list { + .org-team-list-item { + float: left; + padding: 15px; + width: 555px; + .member-avatar-group { + padding: 5px 15px; + img { + width: 38px; + height: 38px; + border-radius: 3px; + } + } + } +} +#team-create-form { + .note { + margin-left: 153px; + } } \ No newline at end of file diff --git a/public/ng/less/gogs/repository.less b/public/ng/less/gogs/repository.less index 0aa19a04a..8f9a97fa0 100644 --- a/public/ng/less/gogs/repository.less +++ b/public/ng/less/gogs/repository.less @@ -310,6 +310,7 @@ border-top-right-radius: .25em; } /* repository create */ +#team-create-form, #repo-migrate-form, #repo-create-form { width: 800px; diff --git a/routers/org/members.go b/routers/org/members.go index d98061765..1e249e8be 100644 --- a/routers/org/members.go +++ b/routers/org/members.go @@ -14,13 +14,13 @@ import ( ) const ( - MEMBERS base.TplName = "org/members" - INVITE base.TplName = "org/invite" + MEMBERS base.TplName = "org/member/members" + MEMBER_INVITE base.TplName = "org/member/invite" ) func Members(ctx *middleware.Context) { org := ctx.Org.Organization - ctx.Data["Title"] = org.Name + ctx.Data["Title"] = org.FullName ctx.Data["PageIsOrgMembers"] = true if err := org.GetMembers(); err != nil { @@ -60,6 +60,18 @@ func MembersAction(ctx *middleware.Context) { return } err = org.RemoveMember(uid) + if err == models.ErrLastOrgOwner { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + ctx.Redirect(ctx.Org.OrgLink + "/members") + return + } + case "leave": + err = org.RemoveMember(ctx.User.Id) + if err == models.ErrLastOrgOwner { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + ctx.Redirect(ctx.Org.OrgLink + "/members") + return + } } if err != nil { @@ -75,7 +87,7 @@ func MembersAction(ctx *middleware.Context) { func Invitation(ctx *middleware.Context) { org := ctx.Org.Organization - ctx.Data["Title"] = org.Name + ctx.Data["Title"] = org.FullName ctx.Data["PageIsOrgMembers"] = true if ctx.Req.Method == "POST" { @@ -101,5 +113,5 @@ func Invitation(ctx *middleware.Context) { return } - ctx.HTML(200, INVITE) + ctx.HTML(200, MEMBER_INVITE) } diff --git a/routers/org/org.go b/routers/org/org.go index 254ba8148..27ccf02d3 100644 --- a/routers/org/org.go +++ b/routers/org/org.go @@ -19,7 +19,7 @@ const ( func Home(ctx *middleware.Context) { org := ctx.Org.Organization - ctx.Data["Title"] = org.Name + ctx.Data["Title"] = org.FullName repos, err := models.GetRepositories(org.Id, ctx.IsSigned && org.IsOrgMember(ctx.User.Id)) if err != nil { diff --git a/routers/org/teams.go b/routers/org/teams.go index d494ddc04..bc12a9716 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -13,31 +13,22 @@ import ( ) const ( - TEAMS base.TplName = "org/teams" - TEAM_NEW base.TplName = "org/team_new" + TEAMS base.TplName = "org/team/teams" + TEAM_NEW base.TplName = "org/team/new" ) func Teams(ctx *middleware.Context) { - ctx.Data["Title"] = "Organization " + ctx.Params(":org") + " Teams" + org := ctx.Org.Organization + ctx.Data["Title"] = org.FullName + ctx.Data["PageIsOrgTeams"] = true - org, err := models.GetUserByName(ctx.Params(":org")) - if err != nil { - if err == models.ErrUserNotExist { - ctx.Handle(404, "org.Teams(GetUserByName)", err) - } else { - ctx.Handle(500, "org.Teams(GetUserByName)", err) - } - return - } - ctx.Data["Org"] = org - - if err = org.GetTeams(); err != nil { - ctx.Handle(500, "org.Teams(GetTeams)", err) + if err := org.GetTeams(); err != nil { + ctx.Handle(500, "GetTeams", err) return } for _, t := range org.Teams { - if err = t.GetMembers(); err != nil { - ctx.Handle(500, "org.Home(GetMembers)", err) + if err := t.GetMembers(); err != nil { + ctx.Handle(500, "GetMembers", err) return } } @@ -46,44 +37,39 @@ func Teams(ctx *middleware.Context) { ctx.HTML(200, TEAMS) } -func NewTeam(ctx *middleware.Context) { - org, err := models.GetUserByName(ctx.Params(":org")) - if err != nil { - if err == models.ErrUserNotExist { - ctx.Handle(404, "org.NewTeam(GetUserByName)", err) - } else { - ctx.Handle(500, "org.NewTeam(GetUserByName)", err) - } - return +func TeamsAction(ctx *middleware.Context) { + var err error + switch ctx.Params(":action") { + case "join": + err = models.AddTeamMember(ctx.Org.Organization.Id, ctx.Org.Team.Id, ctx.User.Id) + case "leave": + err = models.RemoveMember(ctx.Org.Organization.Id, ctx.Org.Team.Id, ctx.User.Id) } - ctx.Data["Org"] = org - // Check ownership of organization. - if !org.IsOrgOwner(ctx.User.Id) { - ctx.Error(403) + 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 + "/teams") +} +func NewTeam(ctx *middleware.Context) { + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamsNew"] = true + ctx.Data["Team"] = &models.Team{} ctx.HTML(200, TEAM_NEW) } func NewTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { - org, err := models.GetUserByName(ctx.Params(":org")) - if err != nil { - if err == models.ErrUserNotExist { - ctx.Handle(404, "org.NewTeamPost(GetUserByName)", err) - } else { - ctx.Handle(500, "org.NewTeamPost(GetUserByName)", err) - } - return - } - ctx.Data["Org"] = org - - // Check ownership of organization. - if !org.IsOrgOwner(ctx.User.Id) { - ctx.Error(403) - return - } + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["PageIsOrgTeamsNew"] = true + ctx.Data["Team"] = &models.Team{} if ctx.HasError() { ctx.HTML(200, TEAM_NEW) @@ -104,23 +90,29 @@ func NewTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { return } + org := ctx.Org.Organization + t := &models.Team{ OrgId: org.Id, Name: form.TeamName, Description: form.Description, Authorize: auth, } - if err = models.NewTeam(t); err != nil { - if err == models.ErrTeamAlreadyExist { + if err := models.NewTeam(t); err != nil { + switch err { + case models.ErrTeamNameIllegal: + ctx.Data["Err_TeamName"] = true + ctx.RenderWithErr(ctx.Tr("form.illegal_team_name"), TEAM_NEW, &form) + case models.ErrTeamAlreadyExist: ctx.Data["Err_TeamName"] = true - ctx.RenderWithErr("Team name has already been used", TEAM_NEW, &form) - } else { - ctx.Handle(500, "org.NewTeamPost(NewTeam)", err) + ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), TEAM_NEW, &form) + default: + ctx.Handle(500, "NewTeam", err) } return } - log.Trace("%s Team created: %s/%s", ctx.Req.RequestURI, org.Name, t.Name) - ctx.Redirect("/org/" + org.LowerName + "/teams/" + t.LowerName) + log.Trace("Team created: %s/%s", org.Name, t.Name) + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName) } func EditTeam(ctx *middleware.Context) { diff --git a/routers/user/auth.go b/routers/user/auth.go index 404b22f24..231ee6627 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -20,12 +20,11 @@ import ( ) const ( - SIGNIN base.TplName = "user/signin" - SIGNUP base.TplName = "user/signup" - DELETE base.TplName = "user/delete" - ACTIVATE base.TplName = "user/activate" - FORGOT_PASSWORD base.TplName = "user/forgot_passwd" - RESET_PASSWORD base.TplName = "user/reset_passwd" + SIGNIN base.TplName = "user/auth/signin" + SIGNUP base.TplName = "user/auth/signup" + ACTIVATE base.TplName = "user/auth/activate" + FORGOT_PASSWORD base.TplName = "user/auth/forgot_passwd" + RESET_PASSWORD base.TplName = "user/auth/reset_passwd" ) func SignIn(ctx *middleware.Context) { diff --git a/templates/.VERSION b/templates/.VERSION index 906c01dc8..48fe58b17 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.4.7.0815 Alpha \ No newline at end of file +0.4.7.0816 Alpha \ No newline at end of file diff --git a/templates/org/header.tmpl b/templates/org/base/header.tmpl similarity index 100% rename from templates/org/header.tmpl rename to templates/org/base/header.tmpl diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index d96624010..afd695ec7 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -4,7 +4,10 @@
-

{{.Org.FullName}}

+

+ {{.Org.FullName}} + {{if .IsOrganizationOwner}}{{end}} +

{{if .Org.Description}}

{{.Org.Description}}

{{end}}
diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl index 7e24f8514..828389d6f 100644 --- a/templates/org/settings/delete.tmpl +++ b/templates/org/settings/delete.tmpl @@ -1,5 +1,6 @@ {{template "ng/base/head" .}} {{template "ng/base/header" .}} +{{template "org/header" .}}
{{template "org/settings/nav" .}} diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 7548ad5ae..c5c6a7e22 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -1,5 +1,6 @@ {{template "ng/base/head" .}} {{template "ng/base/header" .}} +{{template "org/header" .}}
{{template "org/settings/nav" .}} diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl new file mode 100644 index 000000000..f02512e05 --- /dev/null +++ b/templates/org/team/new.tmpl @@ -0,0 +1,48 @@ +{{template "ng/base/head" .}} +{{template "ng/base/header" .}} +{{template "org/base/header" .}} +
+
+ {{.CsrfTokenHtml}} +
+

{{.i18n.Tr "org.create_new_team"}}

+
+
+ {{template "ng/base/alert" .}} +
+ + + + {{.i18n.Tr "org.team_name_helper"}} +
+
+ + + + {{.i18n.Tr "org.team_desc_helper"}} +
+
+

{{.i18n.Tr "org.team_permission_desc"}}

+ + {{.i18n.Tr "org.teams.read_access"}} + +

{{.i18n.Tr "org.teams.read_access_helper"}}

+ + {{.i18n.Tr "org.teams.write_access"}} + +

{{.i18n.Tr "org.teams.write_access_helper"}}

+ + {{.i18n.Tr "org.teams.admin_access"}} + +

{{.i18n.Tr "org.teams.admin_access_helper"}}

+
+
+
+ + + {{.i18n.Tr "cancel"}} +
+
+
+
+{{template "ng/base/footer" .}} \ No newline at end of file diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl new file mode 100644 index 000000000..3e0846d65 --- /dev/null +++ b/templates/org/team/teams.tmpl @@ -0,0 +1,42 @@ +{{template "ng/base/head" .}} +{{template "ng/base/header" .}} +{{template "org/base/header" .}} +
+
+ {{template "ng/base/alert" .}} +
+
+ {{if .IsAdminTeam}} + {{.i18n.Tr "org.create_new_team"}} + {{end}} +
+
+ {{range .Teams}} +
+
+
+ {{if .IsMember $.SignedUser.Id}} + {{$.i18n.Tr "org.teams.leave"}} + {{else}} + {{$.i18n.Tr "org.teams.join"}} + {{end}} + {{.Name}} +
+ {{if .NumMembers}} +
+ {{range .Members}} + + + + {{end}} +
+ {{end}} + +
+
+ {{end}} +
+
+{{template "ng/base/footer" .}} \ No newline at end of file diff --git a/templates/org/team_new.tmpl b/templates/org/team_new.tmpl deleted file mode 100644 index 0936ec29b..000000000 --- a/templates/org/team_new.tmpl +++ /dev/null @@ -1,79 +0,0 @@ -{{template "base/head" .}} -{{template "base/navbar" .}} -
-
-
- - -
-

{{.Org.FullName}}

-
-
-
-
- -
-
-
- {{.CsrfTokenHtml}} -

Create new team

- {{template "base/alert" .}} -
- -
- - You'll use this name to mention this team in conversations. -
-
- -
- -
- -
-
- -
- -
-
- -

This team will be able to view and clone its repositories.

-
-
- -

This team will be able to read its repositories, as well as push to them.

-
-
- -

This team will be able to push/pull to its repositories, as well as add other collaborators to them.

-
-
-
-
-
- -
- -
-
-
-
-
-{{template "base/footer" .}} diff --git a/templates/org/teams.tmpl b/templates/org/teams.tmpl deleted file mode 100644 index 90ee209c0..000000000 --- a/templates/org/teams.tmpl +++ /dev/null @@ -1,58 +0,0 @@ -{{template "base/head" .}} -{{template "base/navbar" .}} -
-
-
- - -
-

{{.Org.FullName}}

-
-
-
-
- -
-
-
-
-
- -
-
-
- {{range .Teams}} -
-
-

{{.Name}}

-
-

{{.NumMembers}} members · {{.NumRepos}} repositories

-

- {{range .Members}} - - - - {{end}} -

-
- -
-
- {{end}} -
-
-
-{{template "base/footer" .}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index b5a8ea4e4..6baa6d31e 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -3,7 +3,9 @@
{{.CsrfTokenHtml}} -

{{.i18n.Tr "new_repo"}}

+
+

{{.i18n.Tr "new_repo"}}

+
{{template "ng/base/alert" .}}
diff --git a/templates/user/activate.tmpl b/templates/user/auth/activate.tmpl similarity index 100% rename from templates/user/activate.tmpl rename to templates/user/auth/activate.tmpl diff --git a/templates/user/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl similarity index 100% rename from templates/user/forgot_passwd.tmpl rename to templates/user/auth/forgot_passwd.tmpl diff --git a/templates/user/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl similarity index 100% rename from templates/user/reset_passwd.tmpl rename to templates/user/auth/reset_passwd.tmpl diff --git a/templates/user/signin.tmpl b/templates/user/auth/signin.tmpl similarity index 100% rename from templates/user/signin.tmpl rename to templates/user/auth/signin.tmpl diff --git a/templates/user/signup.tmpl b/templates/user/auth/signup.tmpl similarity index 100% rename from templates/user/signup.tmpl rename to templates/user/auth/signup.tmpl diff --git a/templates/user/stars.tmpl b/templates/user/stars.tmpl deleted file mode 100644 index 253efd6ee..000000000 --- a/templates/user/stars.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{{template "base/head" .}} -{{template "base/navbar" .}} -
-
- -

Stars

-
-
-
- {{if .HasInfo}}
{{.InfoMsg}}
{{end}} -
-{{template "base/footer" .}} \ No newline at end of file