diff --git a/cmd/web.go b/cmd/web.go index 064a0358f..0b3b84ab6 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -48,7 +48,7 @@ and it takes care of all the other things for you`, Flags: []cli.Flag{}, } -// checkVersion checks if binary matches the version of temolate files. +// checkVersion checks if binary matches the version of templates files. func checkVersion() { data, err := ioutil.ReadFile(path.Join(setting.StaticRootPath, "templates/.VERSION")) if err != nil { @@ -235,7 +235,7 @@ func runWeb(*cli.Context) { r.Get("/members/action/:action", org.MembersAction) r.Get("/teams", org.Teams) - r.Get("/teams/:team", org.SingleTeam) + r.Get("/teams/:team", org.TeamMembers) r.Get("/teams/:team/action/:action", org.TeamsAction) }, middleware.OrgAssignment(true, true)) @@ -243,6 +243,8 @@ func runWeb(*cli.Context) { r.Get("/teams/new", org.NewTeam) r.Post("/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost) r.Get("/teams/:team/edit", org.EditTeam) + r.Post("/teams/:team/edit", bindIgnErr(auth.CreateTeamForm{}), org.EditTeamPost) + r.Post("/teams/:team/delete", org.DeleteTeam) m.Group("/settings", func(r *macaron.Router) { r.Get("", org.Settings) diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index b9e7966c2..9437bfd19 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -283,6 +283,13 @@ teams.no_desc = This team has no description teams.settings = Settings teams.owners_permission_desc = Owners have full access to all repositories and have admin rights to the organization. teams.members = Team Members +teams.update_settings = Update Settings +teams.delete_team = Delete This Team +teams.add_team_member = Add Team Member +teams.delete_team_success = Given team has been successfully deleted. +teams.read_permission_desc = This team grants Read access: members can view and clone the team's repositories. +teams.write_permission_desc = This team grants Write access: members can read from and push to the team's repositories. +teams.admin_permission_desc = This team grants Admin access: members can read from, push to, and add collaborators to the team's repositories. [action] create_repo = created repository %s diff --git a/conf/locale/locale_zh-CN.ini b/conf/locale/locale_zh-CN.ini index 7e5ac6486..dbc94f3a0 100644 --- a/conf/locale/locale_zh-CN.ini +++ b/conf/locale/locale_zh-CN.ini @@ -283,6 +283,13 @@ teams.no_desc = 该团队暂无描述 teams.settings = 团队设置 teams.owners_permission_desc = 管理员团队对 所有仓库 具有操作权限,且对组织具有 管理员权限。 teams.members = 团队成员 +teams.update_settings = 更新团队设置 +teams.delete_team = 删除当前团队 +teams.add_team_member = 添加团队成员 +teams.delete_team_success = 指定团队已经被成功删除! +teams.read_permission_desc = 该团队拥有对所属仓库的 读取 权限,团队成员可以进行查看和克隆等只读操作。 +teams.write_permission_desc = 该团队拥有对所属仓库的 读取写入 的权限。 +teams.admin_permission_desc = 该团队拥有一定的 管理 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。 [action] create_repo = 创建了仓库 %s diff --git a/gogs.go b/gogs.go index 1e493b92b..5c93ae26d 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.4.7.0823 Alpha" +const APP_VER = "0.4.7.0824 Alpha" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/org.go b/models/org.go index cd4163bab..27228382d 100644 --- a/models/org.go +++ b/models/org.go @@ -6,11 +6,13 @@ package models import ( "errors" + "fmt" "os" "path" "strings" "github.com/Unknwon/com" + "github.com/go-xorm/xorm" "github.com/gogits/gogs/modules/base" ) @@ -134,10 +136,10 @@ func CreateOrganization(org, owner *User) (*User, error) { // Add initial creator to organization and owner team. ou := &OrgUser{ - Uid: owner.Id, - OrgId: org.Id, - IsOwner: true, - NumTeam: 1, + Uid: owner.Id, + OrgId: org.Id, + IsOwner: true, + NumTeams: 1, } if _, err = sess.Insert(ou); err != nil { sess.Rollback() @@ -199,7 +201,7 @@ type OrgUser struct { OrgId int64 `xorm:"INDEX UNIQUE(s)"` IsPublic bool IsOwner bool - NumTeam int + NumTeams int } // IsOrganizationOwner returns true if given user is in the owner team. @@ -255,17 +257,17 @@ func AddOrgUser(orgId, uid int64) error { return nil } - ou := &OrgUser{ - Uid: uid, - OrgId: orgId, - } - sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err } + ou := &OrgUser{ + Uid: uid, + OrgId: orgId, + } + if _, err := sess.Insert(ou); err != nil { sess.Rollback() return err @@ -288,12 +290,17 @@ func RemoveOrgUser(orgId, uid int64) error { return nil } + u, err := GetUserById(uid) + if err != nil { + return err + } + org, err := GetUserById(orgId) + if err != nil { + return err + } + // 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 @@ -317,6 +324,33 @@ func RemoveOrgUser(orgId, uid int64) error { return err } + // Delete all repository accesses. + if err = org.GetRepositories(); err != nil { + sess.Rollback() + return err + } + access := &Access{ + UserName: u.LowerName, + } + for _, repo := range org.Repos { + access.RepoName = path.Join(org.LowerName, repo.LowerName) + if _, err = sess.Delete(access); err != nil { + sess.Rollback() + return err + } + } + + // Delete member in his/her teams. + ts, err := GetUserTeams(org.Id, u.Id) + if err != nil { + return err + } + for _, t := range ts { + if err = removeTeamMemberWithSess(org.Id, t.Id, u.Id, sess); err != nil { + return err + } + } + return sess.Commit() } @@ -352,6 +386,11 @@ type Team struct { NumMembers int } +// IsOwnerTeam returns true if team is owner team. +func (t *Team) IsOwnerTeam() bool { + return t.Name == OWNER_TEAM +} + // 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) @@ -362,7 +401,10 @@ func (t *Team) GetRepositories() error { idStrs := strings.Split(t.RepoIds, "|") t.Repos = make([]*Repository, 0, len(idStrs)) for _, str := range idStrs { - id := com.StrTo(str).MustInt64() + if len(str) == 0 { + continue + } + id := com.StrTo(str[1:]).MustInt64() if id == 0 { continue } @@ -459,15 +501,177 @@ func GetTeamById(teamId int64) (*Team, error) { return t, nil } +// GetHighestAuthorize returns highest repository authorize level for given user and team. +func GetHighestAuthorize(orgId, uid, teamId, repoId int64) (AuthorizeType, error) { + ts, err := GetUserTeams(orgId, uid) + if err != nil { + return 0, err + } + + var auth AuthorizeType = 0 + for _, t := range ts { + // Not current team and has given repository. + if t.Id != teamId && strings.Contains(t.RepoIds, "$"+com.ToStr(repoId)+"|") { + // Fast return. + if t.Authorize == ORG_WRITABLE { + return ORG_WRITABLE, nil + } + if t.Authorize > auth { + auth = t.Authorize + } + } + } + return auth, nil +} + // UpdateTeam updates information of team. -func UpdateTeam(t *Team) error { +func UpdateTeam(t *Team, authChanged bool) (err error) { + if !IsLegalName(t.Name) { + return ErrTeamNameIllegal + } + if len(t.Description) > 255 { t.Description = t.Description[:255] } + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + // Update access for team members if needed. + if authChanged && !t.IsOwnerTeam() { + if err = t.GetRepositories(); err != nil { + return err + } else if err = t.GetMembers(); err != nil { + return err + } + + // Get organization. + org, err := GetUserById(t.OrgId) + if err != nil { + return err + } + + mode := READABLE + if t.Authorize > ORG_READABLE { + mode = WRITABLE + } + access := &Access{ + Mode: mode, + } + + for _, repo := range t.Repos { + access.RepoName = path.Join(org.LowerName, repo.LowerName) + for _, u := range t.Members { + // ORG_WRITABLE is the highest authorize level for now. + // Skip checking others if current team has this level. + if t.Authorize < ORG_WRITABLE { + auth, err := GetHighestAuthorize(org.Id, u.Id, t.Id, repo.Id) + if err != nil { + sess.Rollback() + return err + } + if auth >= t.Authorize { + continue // Other team has higher or same authorize level. + } + } + + access.UserName = u.LowerName + if _, err = sess.Update(access); err != nil { + sess.Rollback() + return err + } + } + } + } + t.LowerName = strings.ToLower(t.Name) - _, err := x.Id(t.Id).AllCols().Update(t) - return err + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } + return sess.Commit() +} + +// DeleteTeam deletes given team. +// It's caller's responsibility to assign organization ID. +func DeleteTeam(t *Team) error { + if err := t.GetRepositories(); err != nil { + return err + } else if err = t.GetMembers(); err != nil { + return err + } + + // Get organization. + org, err := GetUserById(t.OrgId) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + // Delete all accesses. + mode := READABLE + if t.Authorize > ORG_READABLE { + mode = WRITABLE + } + access := new(Access) + + for _, repo := range t.Repos { + access.RepoName = path.Join(org.LowerName, repo.LowerName) + for _, u := range t.Members { + access.UserName = u.LowerName + access.Mode = mode + auth, err := GetHighestAuthorize(org.Id, u.Id, t.Id, repo.Id) + if err != nil { + sess.Rollback() + return err + } + + if auth == 0 { + if _, err = sess.Delete(access); err != nil { + sess.Rollback() + return err + } + } else if auth < t.Authorize { + // Downgrade authorize level. + mode := READABLE + if auth > ORG_READABLE { + mode = WRITABLE + } + access.Mode = mode + if _, err = sess.Update(access); err != nil { + sess.Rollback() + return err + } + } + } + } + + // Delete team-user. + if _, err = sess.Where("org_id=?", org.Id).Where("team_id=?", t.Id).Delete(new(TeamUser)); err != nil { + sess.Rollback() + return err + } + + // Delete team. + if _, err = sess.Id(t.Id).Delete(new(Team)); 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() } // ___________ ____ ___ @@ -509,12 +713,37 @@ func GetTeamMembers(orgId, teamId int64) ([]*User, error) { return us, nil } +// GetUserTeams returns all teams that user belongs to in given origanization. +func GetUserTeams(orgId, uid int64) ([]*Team, error) { + tus := make([]*TeamUser, 0, 5) + if err := x.Where("uid=?", uid).And("org_id=?", orgId).Find(&tus); err != nil { + return nil, err + } + + ts := make([]*Team, len(tus)) + for i, tu := range tus { + t := new(Team) + has, err := x.Id(tu.TeamId).Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTeamNotExist + } + ts[i] = t + } + return ts, 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) { + if IsTeamMember(orgId, teamId, uid) { return nil } + if err := AddOrgUser(orgId, uid); err != nil { + return err + } + // Get team and its repositories. t, err := GetTeamById(teamId) if err != nil { @@ -569,18 +798,49 @@ func AddTeamMember(orgId, teamId, uid int64) error { // Give access to team repositories. for _, repo := range t.Repos { - access.RepoName = path.Join(org.LowerName, repo.LowerName) - if _, err = sess.Insert(access); err != nil { + auth, err := GetHighestAuthorize(orgId, uid, teamId, repo.Id) + if err != nil { sess.Rollback() return err } + + access.Id = 0 + access.RepoName = path.Join(org.LowerName, repo.LowerName) + // Equal 0 means given access doesn't exist. + if auth == 0 { + if _, err = sess.Insert(access); err != nil { + sess.Rollback() + return err + } + } else if auth < t.Authorize { + if _, err = sess.Update(access); err != nil { + sess.Rollback() + return err + } + } + } + fmt.Println("kao") + + // We make sure it exists before. + ou := new(OrgUser) + _, err = sess.Where("uid=?", uid).And("org_id=?", orgId).Get(ou) + if err != nil { + sess.Rollback() + return err + } + ou.NumTeams++ + if t.IsOwnerTeam() { + ou.IsOwner = true + } + if _, err = sess.Id(ou.Id).AllCols().Update(ou); err != nil { + sess.Rollback() + return err } return sess.Commit() } -// RemoveTeamMember removes member from given team of given organization. -func RemoveTeamMember(orgId, teamId, uid int64) error { +func removeTeamMemberWithSess(orgId, teamId, uid int64, sess *xorm.Session) error { if !IsTeamMember(orgId, teamId, uid) { return nil } @@ -590,6 +850,12 @@ func RemoveTeamMember(orgId, teamId, uid int64) error { if err != nil { return err } + + // Check if the user to delete is the last member in owner team. + if t.IsOwnerTeam() && t.NumMembers == 1 { + return ErrLastOrgOwner + } + t.NumMembers-- if err = t.GetRepositories(); err != nil { @@ -608,22 +874,12 @@ func RemoveTeamMember(orgId, teamId, uid int64) error { return err } - sess := x.NewSession() - defer sess.Close() - if err := sess.Begin(); err != nil { - return err - } - tu := &TeamUser{ Uid: uid, OrgId: orgId, TeamId: teamId, } - access := &Access{ - UserName: u.LowerName, - } - if _, err := sess.Delete(tu); err != nil { sess.Rollback() return err @@ -633,13 +889,63 @@ func RemoveTeamMember(orgId, teamId, uid int64) error { } // Delete access to team repositories. + access := &Access{ + UserName: u.LowerName, + } + for _, repo := range t.Repos { - access.RepoName = path.Join(org.LowerName, repo.LowerName) - if _, err = sess.Delete(access); err != nil { + auth, err := GetHighestAuthorize(orgId, uid, teamId, repo.Id) + if err != nil { + sess.Rollback() + return err + } + + // Delete access if this is the last team user belongs to. + if auth == 0 { + access.RepoName = path.Join(org.LowerName, repo.LowerName) + _, err = sess.Delete(access) + } else if auth < t.Authorize { + // Downgrade authorize level. + mode := READABLE + if auth > ORG_READABLE { + mode = WRITABLE + } + access.Mode = mode + _, err = sess.Update(access) + } + if err != nil { sess.Rollback() return err } } + // This must exist. + ou := new(OrgUser) + _, err = sess.Where("uid=?", uid).And("org_id=?", org.Id).Get(ou) + if err != nil { + sess.Rollback() + return err + } + ou.NumTeams-- + if t.IsOwnerTeam() { + ou.IsOwner = false + } + if _, err = sess.Id(ou.Id).AllCols().Update(ou); err != nil { + sess.Rollback() + return err + } + return nil +} + +// RemoveTeamMember removes member from given team of given organization. +func RemoveTeamMember(orgId, teamId, uid int64) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if err := removeTeamMemberWithSess(orgId, teamId, uid, sess); err != nil { + return err + } return sess.Commit() } diff --git a/models/repo.go b/models/repo.go index 9666192df..f5d1ca834 100644 --- a/models/repo.go +++ b/models/repo.go @@ -525,6 +525,7 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror return nil, err } for _, u := range us { + access.Id = 0 access.UserName = u.LowerName if _, err = sess.Insert(access); err != nil { sess.Rollback() @@ -707,6 +708,10 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) (err error) { // ChangeRepositoryName changes all corresponding setting from old repository name to new one. func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) { + if !IsLegalName(newRepoName) { + return ErrRepoNameIllegal + } + // Update accesses. accesses := make([]Access, 0, 10) if err = x.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil { diff --git a/models/user.go b/models/user.go index 757c290b9..e8db1ad15 100644 --- a/models/user.go +++ b/models/user.go @@ -54,7 +54,8 @@ type User struct { LoginSource int64 `xorm:"not null default 0"` LoginName string Type UserType - Orgs []*User `xorm:"-"` + Orgs []*User `xorm:"-"` + Repos []*Repository `xorm:"-"` NumFollowers int NumFollowings int NumStars int @@ -143,6 +144,12 @@ func (u *User) GetOrganizationCount() (int64, error) { return x.Where("uid=?", u.Id).Count(new(OrgUser)) } +// GetRepositories returns all repositories that user owns, including private repositories. +func (u *User) GetRepositories() (err error) { + u.Repos, err = GetRepositories(u.Id, true) + return err +} + // GetOrganizations returns all organizations that user belongs to. func (u *User) GetOrganizations() error { ous, err := GetOrgUsersByUserId(u.Id) diff --git a/modules/middleware/context.go b/modules/middleware/context.go index 80975e999..3ef1b1d62 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -46,6 +46,7 @@ type Context struct { IsBranch bool IsTag bool IsCommit bool + IsAdmin bool // Current user is admin level. HasAccess bool Repository *models.Repository Owner *models.User diff --git a/modules/middleware/org.go b/modules/middleware/org.go index c85221a5a..ee4460b27 100644 --- a/modules/middleware/org.go +++ b/modules/middleware/org.go @@ -8,6 +8,7 @@ import ( "github.com/Unknwon/macaron" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/log" ) func OrgAssignment(redirect bool, args ...bool) macaron.Handler { @@ -35,6 +36,7 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { if err == models.ErrUserNotExist { ctx.Handle(404, "GetUserByName", err) } else if redirect { + log.Error(4, "GetUserByName", err) ctx.Redirect("/") } else { ctx.Handle(500, "GetUserByName", err) @@ -52,17 +54,14 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { } else { if org.IsOrgMember(ctx.User.Id) { ctx.Org.IsMember = true - // TODO: ctx.Org.IsAdminTeam } } } if (requireMember && !ctx.Org.IsMember) || - (requireOwner && !ctx.Org.IsOwner) || - (requireAdminTeam && !ctx.Org.IsAdminTeam) { + (requireOwner && !ctx.Org.IsOwner) { ctx.Handle(404, "OrgAssignment", err) return } - ctx.Data["IsAdminTeam"] = ctx.Org.IsAdminTeam ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Org.OrgLink = "/org/" + org.Name @@ -76,6 +75,7 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { if err == models.ErrTeamNotExist { ctx.Handle(404, "GetTeam", err) } else if redirect { + log.Error(4, "GetTeam", err) ctx.Redirect("/") } else { ctx.Handle(500, "GetTeam", err) @@ -83,6 +83,12 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { return } ctx.Data["Team"] = ctx.Org.Team + ctx.Org.IsAdminTeam = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.Authorize == models.ORG_ADMIN + } + ctx.Data["IsAdminTeam"] = ctx.Org.IsAdminTeam + if requireAdminTeam && !ctx.Org.IsAdminTeam { + ctx.Handle(404, "OrgAssignment", err) + return } } } diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index 3db1932af..68a9a2d7f 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -59,6 +59,7 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { if err == models.ErrUserNotExist { ctx.Handle(404, "GetUserByName", err) } else if redirect { + log.Error(4, "GetUserByName", err) ctx.Redirect("/") } else { ctx.Handle(500, "GetUserByName", err) @@ -84,7 +85,7 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Repo.IsTrueOwner = true } - // get repository + // Get repository. repo, err := models.GetRepositoryByName(u.Id, repoName) if err != nil { if err == models.ErrRepoNotExist { @@ -102,8 +103,22 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { } // Check if the mirror repository owner(mirror repository doesn't have access). - if ctx.IsSigned && !ctx.Repo.IsOwner && repo.OwnerId == ctx.User.Id { - ctx.Repo.IsOwner = true + if ctx.IsSigned && !ctx.Repo.IsOwner { + if repo.OwnerId == ctx.User.Id { + ctx.Repo.IsOwner = true + } + // Check if current user has admin permission to repository. + if u.IsOrganization() { + auth, err := models.GetHighestAuthorize(u.Id, ctx.User.Id, 0, repo.Id) + if err != nil { + ctx.Handle(500, "GetHighestAuthorize", err) + return + } + if auth == models.ORG_ADMIN { + ctx.Repo.IsOwner = true + ctx.Repo.IsAdmin = true + } + } } // Check access. @@ -281,7 +296,7 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { func RequireTrueOwner() macaron.Handler { return func(ctx *Context) { - if !ctx.Repo.IsTrueOwner { + if !ctx.Repo.IsTrueOwner && !ctx.Repo.IsAdmin { if !ctx.IsSigned { ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) ctx.Redirect("/user/login") diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css index 9aa7206bd..48468c7e8 100644 --- a/public/ng/css/gogs.css +++ b/public/ng/css/gogs.css @@ -1298,27 +1298,33 @@ The register and sign-in page style .repo-setting-zone { padding: 30px; } +#team-members-list, #repo-collab-list { list-style: none; padding: 10px 0 5px 0; } +#team-members-list li.collab, #repo-collab-list li.collab { clear: both; height: 50px; padding: 0 15px 0 15px; } +#team-members-list a.member, #repo-collab-list a.member { color: #444; height: 50px; line-height: 50px; } +#team-members-list a.member:hover, #repo-collab-list a.member:hover { color: #4183C4; } +#team-members-list .avatar, #repo-collab-list .avatar { margin-right: 1em; width: 40px; } +#team-members-list .remove-collab, #repo-collab-list .remove-collab { color: #DD4B39; } @@ -1871,3 +1877,14 @@ textarea#issue-add-content { #org-team-card .panel-footer { padding: 10px 20px; } +#team-members-list .panel-body .search { + padding: 4px 0 10px 10px; + border-bottom: 1px solid #dddddd; +} +#team-members-list li.collab { + padding-top: 10px !important; + border-bottom: 1px solid #dddddd; +} +#team-members-list li.collab:last-child { + border-bottom: 0; +} diff --git a/public/ng/js/gogs.js b/public/ng/js/gogs.js index 5e6a6a6b9..52000f366 100644 --- a/public/ng/js/gogs.js +++ b/public/ng/js/gogs.js @@ -351,6 +351,41 @@ function initInvite() { }); } +function initOrgTeamCreate() { + // Delete team. + $('#org-team-delete').click(function (e) { + if (!confirm('This team is going to be deleted, do you want to continue?')) { + e.preventDefault(); + return true; + } + var $form = $('#team-create-form') + $form.attr('action', $form.data('delete-url')); + }); +} + +function initTeamMembersList() { + // Add team member. + var $ul = $('#org-team-members-list'); + $('#org-team-members-add').on('keyup', function () { + var $this = $(this); + if (!$this.val()) { + $ul.toggleHide(); + return; + } + Gogs.searchUsers($this.val(), $ul); + }).on('focus', function () { + if (!$(this).val()) { + $ul.toggleHide(); + } else { + $ul.toggleShow(); + } + }).next().next().find('ul').on("click", 'li', function () { + $('#org-team-members-add').val($(this).text()); + $ul.toggleHide(); + }); + +} + $(document).ready(function () { initCore(); if ($('#user-profile-setting').length) { @@ -368,6 +403,12 @@ $(document).ready(function () { if ($('#invite-box').length) { initInvite(); } + if ($('#team-create-form').length) { + initOrgTeamCreate(); + } + if ($('#team-members-list').length) { + initTeamMembersList(); + } Tabs('#dashboard-sidebar-menu'); diff --git a/public/ng/less/gogs/organization.less b/public/ng/less/gogs/organization.less index 58039d90b..a62dcbb3e 100644 --- a/public/ng/less/gogs/organization.less +++ b/public/ng/less/gogs/organization.less @@ -196,4 +196,19 @@ .panel-footer { padding: 10px 20px; } +} +#team-members-list { + .panel-body .search { + padding: 4px 0 10px 10px; + border-bottom: 1px solid #dddddd; + } +} +#team-members-list { + li.collab { + padding-top: 10px !important; + border-bottom: 1px solid #dddddd; + &:last-child { + border-bottom: 0; + } + } } \ No newline at end of file diff --git a/public/ng/less/gogs/repository.less b/public/ng/less/gogs/repository.less index 8f9a97fa0..2f9728985 100644 --- a/public/ng/less/gogs/repository.less +++ b/public/ng/less/gogs/repository.less @@ -426,6 +426,7 @@ border-top-right-radius: .25em; .repo-setting-zone { padding: 30px; } +#team-members-list, #repo-collab-list { list-style: none; padding: 10px 0 5px 0; diff --git a/routers/org/members.go b/routers/org/members.go index 1e249e8be..823daec94 100644 --- a/routers/org/members.go +++ b/routers/org/members.go @@ -82,7 +82,12 @@ func MembersAction(ctx *middleware.Context) { }) return } - ctx.Redirect(ctx.Org.OrgLink + "/members") + + if ctx.Params(":action") != "leave" { + ctx.Redirect(ctx.Org.OrgLink + "/members") + } else { + ctx.Redirect("/") + } } func Invitation(ctx *middleware.Context) { diff --git a/routers/org/teams.go b/routers/org/teams.go index 8eb86c49f..4c986d4aa 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -5,6 +5,8 @@ package org import ( + "github.com/Unknwon/com" + "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" @@ -39,23 +41,71 @@ func Teams(ctx *middleware.Context) { } func TeamsAction(ctx *middleware.Context) { + uid := com.StrTo(ctx.Query("uid")).MustInt64() + if uid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/teams") + return + } + + page := ctx.Query("page") var err error switch ctx.Params(":action") { case "join": + if !ctx.Org.IsOwner { + ctx.Error(404) + return + } err = ctx.Org.Team.AddMember(ctx.User.Id) case "leave": err = ctx.Org.Team.RemoveMember(ctx.User.Id) + case "remove": + if !ctx.Org.IsOwner { + ctx.Error(404) + return + } + err = ctx.Org.Team.RemoveMember(uid) + page = "team" + case "add": + if !ctx.Org.IsOwner { + ctx.Error(404) + return + } + uname := ctx.Query("uname") + var u *models.User + 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 + "/teams/" + ctx.Org.Team.LowerName) + } else { + ctx.Handle(500, " GetUserByName", err) + } + return + } + + err = ctx.Org.Team.AddMember(u.Id) + page = "team" } 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 + if err == models.ErrLastOrgOwner { + ctx.Flash.Error(ctx.Tr("form.last_org_owner")) + } else { + log.Error(4, "Action(%s): %v", ctx.Params(":action"), err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + } + + switch page { + case "team": + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName) + default: + ctx.Redirect(ctx.Org.OrgLink + "/teams") } - ctx.Redirect(ctx.Org.OrgLink + "/teams") } func NewTeam(ctx *middleware.Context) { @@ -116,13 +166,76 @@ func NewTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName) } -func EditTeam(ctx *middleware.Context) { - ctx.Data["Title"] = "Organization " + ctx.Params(":org") + " Edit Team" - ctx.HTML(200, "org/edit_team") -} - -func SingleTeam(ctx *middleware.Context) { +func TeamMembers(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Org.Team.Name ctx.Data["PageIsOrgTeams"] = true + if err := ctx.Org.Team.GetMembers(); err != nil { + ctx.Handle(500, "GetMembers", err) + return + } ctx.HTML(200, TEAM_MEMBERS) } + +func EditTeam(ctx *middleware.Context) { + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["team_name"] = ctx.Org.Team.Name + ctx.Data["desc"] = ctx.Org.Team.Description + ctx.HTML(200, TEAM_NEW) +} + +func EditTeamPost(ctx *middleware.Context, form auth.CreateTeamForm) { + t := ctx.Org.Team + ctx.Data["Title"] = ctx.Org.Organization.FullName + ctx.Data["PageIsOrgTeams"] = true + ctx.Data["team_name"] = t.Name + ctx.Data["desc"] = t.Description + + if ctx.HasError() { + ctx.HTML(200, TEAM_NEW) + return + } + + isAuthChanged := false + if !t.IsOwnerTeam() { + // Validate permission level. + var auth models.AuthorizeType + switch form.Permission { + case "read": + auth = models.ORG_READABLE + case "write": + auth = models.ORG_WRITABLE + case "admin": + auth = models.ORG_ADMIN + default: + ctx.Error(401) + return + } + + t.Name = form.TeamName + if t.Authorize != auth { + isAuthChanged = true + t.Authorize = auth + } + } + t.Description = form.Description + if err := models.UpdateTeam(t, isAuthChanged); err != nil { + if err == models.ErrTeamNameIllegal { + ctx.Data["Err_TeamName"] = true + ctx.RenderWithErr(ctx.Tr("form.illegal_team_name"), TEAM_NEW, &form) + } else { + ctx.Handle(500, "UpdateTeam", err) + } + return + } + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName) +} + +func DeleteTeam(ctx *middleware.Context) { + if err := models.DeleteTeam(ctx.Org.Team); err != nil { + ctx.Handle(500, "DeleteTeam", err) + return + } + ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success")) + ctx.Redirect(ctx.Org.OrgLink + "/teams") +} diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 10a6f72db..866c0cd93 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -56,7 +56,12 @@ func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) { ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), SETTINGS_OPTIONS, nil) return } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { - ctx.Handle(500, "ChangeRepositoryName", err) + if err == models.ErrRepoNameIllegal { + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("form.illegal_repo_name"), SETTINGS_OPTIONS, nil) + } else { + ctx.Handle(500, "ChangeRepositoryName", err) + } return } log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName) @@ -185,9 +190,24 @@ func SettingsCollaboration(ctx *middleware.Context) { // Delete collaborator. remove := strings.ToLower(ctx.Query("remove")) if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { - if err := models.DeleteAccess(&models.Access{UserName: remove, RepoName: repoLink}); err != nil { - ctx.Handle(500, "DeleteAccess", err) - return + needDelete := true + if ctx.User.IsOrganization() { + // Check if user belongs to a team that has access to this repository. + auth, err := models.GetHighestAuthorize(ctx.Repo.Owner.Id, ctx.User.Id, 0, ctx.Repo.Repository.Id) + if err != nil { + ctx.Handle(500, "GetHighestAuthorize", err) + return + } + if auth > 0 { + needDelete = false + } + } + + if needDelete { + if err := models.DeleteAccess(&models.Access{UserName: remove, RepoName: repoLink}); err != nil { + ctx.Handle(500, "DeleteAccess", err) + return + } } ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") diff --git a/templates/.VERSION b/templates/.VERSION index f152becda..3e68f858b 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.4.7.0823 Alpha \ No newline at end of file +0.4.7.0824 Alpha \ No newline at end of file diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index afd695ec7..6f3078de5 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -18,6 +18,7 @@
+ {{$isMember := .Org.IsOrgMember $.SignedUser.Id}}
{{if .IsOrganizationOwner}} @@ -26,6 +27,7 @@
{{range .Repos}} + {{if or $isMember (not .IsPrivate)}}
  • {{.NumStars}}
  • @@ -35,6 +37,7 @@

    {{.Description}}

    {{$.i18n.Tr "org.repo_updated"}} {{TimeSince .Updated $.i18n.Lang}}

+ {{end}} {{end}}
@@ -42,12 +45,16 @@
+ {{if $isMember}} {{.Org.NumMembers}} + {{end}} {{.i18n.Tr "org.people"}}
{{range .Members}} - + {{if or $isMember (.IsPublicMember $.Org.Id)}} + + {{end}} {{end}}
{{if .IsOrganizationOwner}} @@ -56,6 +63,7 @@
{{end}}
+ {{if $isMember}}
@@ -76,9 +84,9 @@ - {{end}}
+ {{end}}
diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index 3ab92bbfc..1c530982f 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -6,7 +6,7 @@ {{template "ng/base/alert" .}}
- {{if .IsAdminTeam}} + {{if .IsOrganizationOwner}} {{.i18n.Tr "org.invite_someone"}} {{end}}
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index 8faeb9ef3..d3176be15 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -2,7 +2,8 @@ {{template "ng/base/header" .}} {{template "org/base/header" .}}
-
+
+ {{template "ng/base/alert" .}} {{template "org/team/sidebar" .}}
@@ -10,6 +11,32 @@
{{.i18n.Tr "org.teams.members"}}
+
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index f02512e05..ce8c15236 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -2,16 +2,21 @@ {{template "ng/base/header" .}} {{template "org/base/header" .}}
-
+ {{.CsrfTokenHtml}}
-

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

+

+ {{if .PageIsOrgTeamsNew}}{{.i18n.Tr "org.create_new_team"}}{{else}}{{.i18n.Tr "org.teams.settings"}}{{end}} +

{{template "ng/base/alert" .}}
- + {{if eq .Team.LowerName "owners"}} + + {{end}} + {{.i18n.Tr "org.team_name_helper"}}
@@ -21,6 +26,7 @@ {{.i18n.Tr "org.team_desc_helper"}}
+ {{if not (eq .Team.LowerName "owners")}}

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

@@ -37,10 +43,19 @@

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


+ {{end}}
- - {{.i18n.Tr "cancel"}} + {{if .PageIsOrgTeamsNew}} + + {{.i18n.Tr "cancel"}} + {{else}} + + {{if not (eq .Team.LowerName "owners")}} +       + + {{end}} + {{end}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 1ab24e9aa..b760e002f 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -1,9 +1,9 @@
{{if .Team.IsMember $.SignedUser.Id}} - {{$.i18n.Tr "org.teams.leave"}} - {{else}} - {{$.i18n.Tr "org.teams.join"}} + {{$.i18n.Tr "org.teams.leave"}} + {{else if .IsOrganizationOwner}} + {{$.i18n.Tr "org.teams.join"}} {{end}} {{.Team.Name}}
@@ -11,16 +11,24 @@

{{if .Team.Description}}{{.Team.Description}}{{else}}{{.i18n.Tr "org.teams.no_desc"}}{{end}}


{{if eq .Team.LowerName "owners"}} {{.i18n.Tr "org.teams.owners_permission_desc" | Str2html}} + {{else if (eq .Team.Authorize 1)}} + {{.i18n.Tr "org.teams.read_permission_desc" | Str2html}} + {{else if (eq .Team.Authorize 2)}} + {{.i18n.Tr "org.teams.write_permission_desc" | Str2html}} + {{else if (eq .Team.Authorize 3)}} + {{.i18n.Tr "org.teams.admin_permission_desc" | Str2html}} {{end}}

+ {{if .IsOrganizationOwner}} + {{end}}
\ No newline at end of file diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 3e0846d65..9c47cb5a4 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -6,7 +6,7 @@ {{template "ng/base/alert" .}}
- {{if .IsAdminTeam}} + {{if .IsOrganizationOwner}} {{.i18n.Tr "org.create_new_team"}} {{end}}
@@ -16,9 +16,9 @@
{{if .IsMember $.SignedUser.Id}} - {{$.i18n.Tr "org.teams.leave"}} - {{else}} - {{$.i18n.Tr "org.teams.join"}} + {{$.i18n.Tr "org.teams.leave"}} + {{else if $.IsOrganizationOwner}} + {{$.i18n.Tr "org.teams.join"}} {{end}} {{.Name}}