* Add teams to repo on collaboration page. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add option for repository admins to change teams access to repo. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add comment for functions Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Make RepoAdminChangeTeamAccess default false in xorm and make it default checked in template instead. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Make proper language strings and fix error redirection. * Add unit tests for adding and deleting team from repository. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add database migration Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix redirect Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix locale string mismatch. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Move team access mode text logic to template. * Move collaborator access mode text logic to template.tags/v1.10.0-rc1
@@ -1370,6 +1370,23 @@ func (err ErrTeamAlreadyExist) Error() string { | |||
return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name) | |||
} | |||
// ErrTeamNotExist represents a "TeamNotExist" error | |||
type ErrTeamNotExist struct { | |||
OrgID int64 | |||
TeamID int64 | |||
Name string | |||
} | |||
// IsErrTeamNotExist checks if an error is a ErrTeamNotExist. | |||
func IsErrTeamNotExist(err error) bool { | |||
_, ok := err.(ErrTeamNotExist) | |||
return ok | |||
} | |||
func (err ErrTeamNotExist) Error() string { | |||
return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name) | |||
} | |||
// | |||
// Two-factor authentication | |||
// |
@@ -508,4 +508,15 @@ | |||
num_stars: 0 | |||
num_forks: 0 | |||
num_issues: 0 | |||
is_mirror: false | |||
- | |||
id: 43 | |||
owner_id: 26 | |||
lower_name: repo26 | |||
name: repo26 | |||
is_private: true | |||
num_stars: 0 | |||
num_forks: 0 | |||
num_issues: 0 | |||
is_mirror: false |
@@ -87,3 +87,12 @@ | |||
authorize: 1 # owner | |||
num_repos: 0 | |||
num_members: 1 | |||
- | |||
id: 11 | |||
org_id: 26 | |||
lower_name: team11 | |||
name: team11 | |||
authorize: 1 # read | |||
num_repos: 0 | |||
num_members: 0 |
@@ -410,3 +410,21 @@ | |||
num_repos: 0 | |||
num_members: 1 | |||
num_teams: 1 | |||
- | |||
id: 26 | |||
lower_name: org26 | |||
name: org26 | |||
full_name: "Org26" | |||
email: org26@example.com | |||
email_notifications_preference: onmention | |||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password | |||
type: 1 # organization | |||
salt: ZogKvWdyEx | |||
is_admin: false | |||
avatar: avatar26 | |||
avatar_email: org26@example.com | |||
num_repos: 1 | |||
num_members: 0 | |||
num_teams: 1 | |||
repo_admin_change_team_access: true |
@@ -248,6 +248,8 @@ var migrations = []Migration{ | |||
NewMigration("add table columns for cross referencing issues", addCrossReferenceColumns), | |||
// v96 -> v97 | |||
NewMigration("delete orphaned attachments", deleteOrphanedAttachments), | |||
// v97 -> v98 | |||
NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser), | |||
} | |||
// Migrate database to current version |
@@ -0,0 +1,15 @@ | |||
// Copyright 2019 The Gitea 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 migrations | |||
import "github.com/go-xorm/xorm" | |||
func addRepoAdminChangeTeamAccessColumnForUser(x *xorm.Engine) error { | |||
type User struct { | |||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"` | |||
} | |||
return x.Sync2(new(User)) | |||
} |
@@ -6,7 +6,6 @@ | |||
package models | |||
import ( | |||
"errors" | |||
"fmt" | |||
"os" | |||
"strings" | |||
@@ -20,11 +19,6 @@ import ( | |||
"xorm.io/builder" | |||
) | |||
var ( | |||
// ErrTeamNotExist team does not exist | |||
ErrTeamNotExist = errors.New("Team does not exist") | |||
) | |||
// IsOwnedBy returns true if given user is in the owner team. | |||
func (org *User) IsOwnedBy(uid int64) (bool, error) { | |||
return IsOrganizationOwner(org.ID, uid) | |||
@@ -304,7 +298,7 @@ type OrgUser struct { | |||
func isOrganizationOwner(e Engine, orgID, uid int64) (bool, error) { | |||
ownerTeam, err := getOwnerTeam(e, orgID) | |||
if err != nil { | |||
if err == ErrTeamNotExist { | |||
if IsErrTeamNotExist(err) { | |||
log.Error("Organization does not have owner team: %d", orgID) | |||
return false, nil | |||
} |
@@ -352,7 +352,7 @@ func getTeam(e Engine, orgID int64, name string) (*Team, error) { | |||
if err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, ErrTeamNotExist | |||
return nil, ErrTeamNotExist{orgID, 0, name} | |||
} | |||
return t, nil | |||
} | |||
@@ -373,7 +373,7 @@ func getTeamByID(e Engine, teamID int64) (*Team, error) { | |||
if err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, ErrTeamNotExist | |||
return nil, ErrTeamNotExist{0, teamID, ""} | |||
} | |||
return t, nil | |||
} |
@@ -64,11 +64,11 @@ func TestUser_GetTeam(t *testing.T) { | |||
assert.Equal(t, "team1", team.LowerName) | |||
_, err = org.GetTeam("does not exist") | |||
assert.Equal(t, ErrTeamNotExist, err) | |||
assert.True(t, IsErrTeamNotExist(err)) | |||
nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||
_, err = nonOrg.GetTeam("team") | |||
assert.Equal(t, ErrTeamNotExist, err) | |||
assert.True(t, IsErrTeamNotExist(err)) | |||
} | |||
func TestUser_GetOwnerTeam(t *testing.T) { | |||
@@ -80,7 +80,7 @@ func TestUser_GetOwnerTeam(t *testing.T) { | |||
nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||
_, err = nonOrg.GetOwnerTeam() | |||
assert.Equal(t, ErrTeamNotExist, err) | |||
assert.True(t, IsErrTeamNotExist(err)) | |||
} | |||
func TestUser_GetTeams(t *testing.T) { |
@@ -16,20 +16,6 @@ type Collaboration struct { | |||
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` | |||
} | |||
// ModeI18nKey returns the collaboration mode I18n Key | |||
func (c *Collaboration) ModeI18nKey() string { | |||
switch c.Mode { | |||
case AccessModeRead: | |||
return "repo.settings.collaboration.read" | |||
case AccessModeWrite: | |||
return "repo.settings.collaboration.write" | |||
case AccessModeAdmin: | |||
return "repo.settings.collaboration.admin" | |||
default: | |||
return "repo.settings.collaboration.undefined" | |||
} | |||
} | |||
// AddCollaborator adds new collaboration to a repository with default access mode. | |||
func (repo *Repository) AddCollaborator(u *User) error { | |||
collaboration := &Collaboration{ | |||
@@ -183,3 +169,17 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) { | |||
return sess.Commit() | |||
} | |||
func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) { | |||
return teams, e. | |||
Join("INNER", "team_repo", "team_repo.team_id = team.id"). | |||
Where("team.org_id = ?", repo.OwnerID). | |||
And("team_repo.repo_id=?", repo.ID). | |||
OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END"). | |||
Find(&teams) | |||
} | |||
// GetRepoTeams gets the list of teams that has access to the repository | |||
func (repo *Repository) GetRepoTeams() ([]*Team, error) { | |||
return repo.getRepoTeams(x) | |||
} |
@@ -10,17 +10,6 @@ import ( | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestCollaboration_ModeI18nKey(t *testing.T) { | |||
assert.Equal(t, "repo.settings.collaboration.read", | |||
(&Collaboration{Mode: AccessModeRead}).ModeI18nKey()) | |||
assert.Equal(t, "repo.settings.collaboration.write", | |||
(&Collaboration{Mode: AccessModeWrite}).ModeI18nKey()) | |||
assert.Equal(t, "repo.settings.collaboration.admin", | |||
(&Collaboration{Mode: AccessModeAdmin}).ModeI18nKey()) | |||
assert.Equal(t, "repo.settings.collaboration.undefined", | |||
(&Collaboration{Mode: AccessModeNone}).ModeI18nKey()) | |||
} | |||
func TestRepository_AddCollaborator(t *testing.T) { | |||
assert.NoError(t, PrepareTestDatabase()) | |||
@@ -147,12 +147,13 @@ type User struct { | |||
NumRepos int | |||
// For organization | |||
NumTeams int | |||
NumMembers int | |||
Teams []*Team `xorm:"-"` | |||
Members UserList `xorm:"-"` | |||
MembersIsPublic map[int64]bool `xorm:"-"` | |||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` | |||
NumTeams int | |||
NumMembers int | |||
Teams []*Team `xorm:"-"` | |||
Members UserList `xorm:"-"` | |||
MembersIsPublic map[int64]bool `xorm:"-"` | |||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` | |||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"` | |||
// Preferences | |||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` |
@@ -140,7 +140,10 @@ func TestSearchUsers(t *testing.T) { | |||
testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2}, | |||
[]int64{19, 25}) | |||
testOrgSuccess(&SearchUserOptions{Page: 4, PageSize: 2}, | |||
testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 4, PageSize: 2}, | |||
[]int64{26}) | |||
testOrgSuccess(&SearchUserOptions{Page: 5, PageSize: 2}, | |||
[]int64{}) | |||
// test users |
@@ -43,7 +43,7 @@ func (users UserList) loadOrganizationOwners(e Engine, orgID int64) (map[int64]* | |||
} | |||
ownerTeam, err := getOwnerTeam(e, orgID) | |||
if err != nil { | |||
if err == ErrTeamNotExist { | |||
if IsErrTeamNotExist(err) { | |||
log.Error("Organization does not have owner team: %d", orgID) | |||
return nil, nil | |||
} |
@@ -33,13 +33,14 @@ func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) bind | |||
// UpdateOrgSettingForm form for updating organization settings | |||
type UpdateOrgSettingForm struct { | |||
Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"` | |||
FullName string `binding:"MaxSize(100)"` | |||
Description string `binding:"MaxSize(255)"` | |||
Website string `binding:"ValidUrl;MaxSize(255)"` | |||
Location string `binding:"MaxSize(50)"` | |||
Visibility structs.VisibleType | |||
MaxRepoCreation int | |||
Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"` | |||
FullName string `binding:"MaxSize(100)"` | |||
Description string `binding:"MaxSize(255)"` | |||
Website string `binding:"ValidUrl;MaxSize(255)"` | |||
Location string `binding:"MaxSize(50)"` | |||
Visibility structs.VisibleType | |||
MaxRepoCreation int | |||
RepoAdminChangeTeamAccess bool | |||
} | |||
// Validate validates the fields |
@@ -6,14 +6,15 @@ package structs | |||
// Organization represents an organization | |||
type Organization struct { | |||
ID int64 `json:"id"` | |||
UserName string `json:"username"` | |||
FullName string `json:"full_name"` | |||
AvatarURL string `json:"avatar_url"` | |||
Description string `json:"description"` | |||
Website string `json:"website"` | |||
Location string `json:"location"` | |||
Visibility string `json:"visibility"` | |||
ID int64 `json:"id"` | |||
UserName string `json:"username"` | |||
FullName string `json:"full_name"` | |||
AvatarURL string `json:"avatar_url"` | |||
Description string `json:"description"` | |||
Website string `json:"website"` | |||
Location string `json:"location"` | |||
Visibility string `json:"visibility"` | |||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` | |||
} | |||
// CreateOrgOption options for creating an organization | |||
@@ -26,7 +27,8 @@ type CreateOrgOption struct { | |||
Location string `json:"location"` | |||
// possible values are `public` (default), `limited` or `private` | |||
// enum: public,limited,private | |||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` | |||
} | |||
// EditOrgOption options for editing an organization | |||
@@ -37,5 +39,6 @@ type EditOrgOption struct { | |||
Location string `json:"location"` | |||
// possible values are `public`, `limited` or `private` | |||
// enum: public,limited,private | |||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` | |||
} |
@@ -319,6 +319,7 @@ enterred_invalid_repo_name = The repository name you entered is incorrect. | |||
enterred_invalid_owner_name = The new owner name is not valid. | |||
enterred_invalid_password = The password you entered is incorrect. | |||
user_not_exist = The user does not exist. | |||
team_not_exist = The team does not exist. | |||
last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner in any given team. | |||
cannot_add_org_to_team = An organization cannot be added as a team member. | |||
@@ -1136,6 +1137,7 @@ settings.collaboration = Collaborators | |||
settings.collaboration.admin = Administrator | |||
settings.collaboration.write = Write | |||
settings.collaboration.read = Read | |||
settings.collaboration.owner = Owner | |||
settings.collaboration.undefined = Undefined | |||
settings.hooks = Webhooks | |||
settings.githooks = Git Hooks | |||
@@ -1217,6 +1219,11 @@ settings.collaborator_deletion_desc = Removing a collaborator will revoke their | |||
settings.remove_collaborator_success = The collaborator has been removed. | |||
settings.search_user_placeholder = Search user… | |||
settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator. | |||
settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner | |||
settings.team_not_in_organization = The team is not in the same organization as the repository | |||
settings.add_team_duplicate = Team already has the repository | |||
settings.add_team_success = The team now have access to the repository. | |||
settings.remove_team_success = The team's access to the repository has been removed. | |||
settings.add_webhook = Add Webhook | |||
settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character. | |||
settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>. | |||
@@ -1475,6 +1482,8 @@ settings.options = Organization | |||
settings.full_name = Full Name | |||
settings.website = Website | |||
settings.location = Location | |||
settings.permission = Permissions | |||
settings.repoadminchangeteam = Repository admin can add and remove access for teams | |||
settings.visibility = Visibility | |||
settings.visibility.public = Public | |||
settings.visibility.limited = Limited (Visible to logged in users only) |
@@ -747,6 +747,8 @@ footer .ui.left,footer .ui.right{line-height:40px} | |||
.repository.settings.collaboration .collaborator.list>.item:not(:last-child){border-bottom:1px solid #ddd} | |||
.repository.settings.collaboration #repo-collab-form #search-user-box .results{left:7px} | |||
.repository.settings.collaboration #repo-collab-form .ui.button{margin-left:5px;margin-top:-3px} | |||
.repository.settings.collaboration #repo-collab-team-form #search-team-box .results{left:7px} | |||
.repository.settings.collaboration #repo-collab-team-form .ui.button{margin-left:5px;margin-top:-3px} | |||
.repository.settings.branches .protected-branches .selection.dropdown{width:300px} | |||
.repository.settings.branches .protected-branches .item{border:1px solid #eaeaea;padding:10px 15px} | |||
.repository.settings.branches .protected-branches .item:not(:last-child){border-bottom:0} | |||
@@ -783,6 +785,7 @@ footer .ui.left,footer .ui.right{line-height:40px} | |||
.user-cards .list .item .meta{margin-top:5px} | |||
#search-repo-box .results .result .image,#search-user-box .results .result .image{float:left;margin-right:8px;width:2em;height:2em} | |||
#search-repo-box .results .result .content,#search-user-box .results .result .content{margin:6px 0} | |||
#search-team-box .results .result .content{margin:6px 0} | |||
#issue-filters.hide{display:none} | |||
#issue-actions{margin-top:-1rem!important} | |||
#issue-actions.hide{display:none} |
@@ -1761,6 +1761,30 @@ function searchUsers() { | |||
}); | |||
} | |||
function searchTeams() { | |||
const $searchTeamBox = $('#search-team-box'); | |||
$searchTeamBox.search({ | |||
minCharacters: 2, | |||
apiSettings: { | |||
url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams', | |||
headers: {"X-Csrf-Token": csrf}, | |||
onResponse: function(response) { | |||
const items = []; | |||
$.each(response, function (_i, item) { | |||
const title = item.name + ' (' + item.permission + ' access)'; | |||
items.push({ | |||
title: title, | |||
}) | |||
}); | |||
return { results: items } | |||
} | |||
}, | |||
searchFields: ['name', 'description'], | |||
showNoResults: false | |||
}); | |||
} | |||
function searchRepositories() { | |||
const $searchRepoBox = $('#search-repo-box'); | |||
$searchRepoBox.search({ | |||
@@ -2171,6 +2195,7 @@ $(document).ready(function () { | |||
buttonsClickOnEnter(); | |||
searchUsers(); | |||
searchTeams(); | |||
searchRepositories(); | |||
initCommentForm(); |
@@ -1736,6 +1736,19 @@ | |||
margin-top: -3px; | |||
} | |||
} | |||
#repo-collab-team-form { | |||
#search-team-box { | |||
.results { | |||
left: 7px; | |||
} | |||
} | |||
.ui.button { | |||
margin-left: 5px; | |||
margin-top: -3px; | |||
} | |||
} | |||
} | |||
&.branches { | |||
@@ -1936,6 +1949,16 @@ | |||
} | |||
} | |||
#search-team-box { | |||
.results { | |||
.result { | |||
.content { | |||
margin: 6px 0; | |||
} | |||
} | |||
} | |||
} | |||
#issue-filters.hide { | |||
display: none; | |||
} |
@@ -206,14 +206,15 @@ func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { | |||
// ToOrganization convert models.User to api.Organization | |||
func ToOrganization(org *models.User) *api.Organization { | |||
return &api.Organization{ | |||
ID: org.ID, | |||
AvatarURL: org.AvatarLink(), | |||
UserName: org.Name, | |||
FullName: org.FullName, | |||
Description: org.Description, | |||
Website: org.Website, | |||
Location: org.Location, | |||
Visibility: org.Visibility.String(), | |||
ID: org.ID, | |||
AvatarURL: org.AvatarLink(), | |||
UserName: org.Name, | |||
FullName: org.FullName, | |||
Description: org.Description, | |||
Website: org.Website, | |||
Location: org.Location, | |||
Visibility: org.Visibility.String(), | |||
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess, | |||
} | |||
} | |||
@@ -95,14 +95,15 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { | |||
} | |||
org := &models.User{ | |||
Name: form.UserName, | |||
FullName: form.FullName, | |||
Description: form.Description, | |||
Website: form.Website, | |||
Location: form.Location, | |||
IsActive: true, | |||
Type: models.UserTypeOrganization, | |||
Visibility: visibility, | |||
Name: form.UserName, | |||
FullName: form.FullName, | |||
Description: form.Description, | |||
Website: form.Website, | |||
Location: form.Location, | |||
IsActive: true, | |||
Type: models.UserTypeOrganization, | |||
Visibility: visibility, | |||
RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess, | |||
} | |||
if err := models.CreateOrganization(org, ctx.User); err != nil { | |||
if models.IsErrUserAlreadyExist(err) || |
@@ -83,6 +83,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) { | |||
org.Website = form.Website | |||
org.Location = form.Location | |||
org.Visibility = form.Visibility | |||
org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess | |||
if err := models.UpdateUser(org); err != nil { | |||
ctx.ServerError("UpdateUser", err) | |||
return |
@@ -490,6 +490,18 @@ func Collaboration(ctx *context.Context) { | |||
} | |||
ctx.Data["Collaborators"] = users | |||
teams, err := ctx.Repo.Repository.GetRepoTeams() | |||
if err != nil { | |||
ctx.ServerError("GetRepoTeams", err) | |||
return | |||
} | |||
ctx.Data["Teams"] = teams | |||
ctx.Data["Repo"] = ctx.Repo.Repository | |||
ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID | |||
ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName | |||
ctx.Data["Org"] = ctx.Repo.Repository.Owner | |||
ctx.Data["Units"] = models.Units | |||
ctx.HTML(200, tplCollaboration) | |||
} | |||
@@ -566,6 +578,77 @@ func DeleteCollaboration(ctx *context.Context) { | |||
}) | |||
} | |||
// AddTeamPost response for adding a team to a repository | |||
func AddTeamPost(ctx *context.Context) { | |||
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { | |||
ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
return | |||
} | |||
name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team"))) | |||
if len(name) == 0 || ctx.Repo.Owner.LowerName == name { | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
return | |||
} | |||
team, err := ctx.Repo.Owner.GetTeam(name) | |||
if err != nil { | |||
if models.IsErrTeamNotExist(err) { | |||
ctx.Flash.Error(ctx.Tr("form.team_not_exist")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
} else { | |||
ctx.ServerError("GetTeam", err) | |||
} | |||
return | |||
} | |||
if team.OrgID != ctx.Repo.Repository.OwnerID { | |||
ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
return | |||
} | |||
if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) { | |||
ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
return | |||
} | |||
if err = team.AddRepository(ctx.Repo.Repository); err != nil { | |||
ctx.ServerError("team.AddRepository", err) | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
} | |||
// DeleteTeam response for deleting a team from a repository | |||
func DeleteTeam(ctx *context.Context) { | |||
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { | |||
ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
return | |||
} | |||
team, err := models.GetTeamByID(ctx.QueryInt64("id")) | |||
if err != nil { | |||
ctx.ServerError("GetTeamByID", err) | |||
return | |||
} | |||
if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil { | |||
ctx.ServerError("team.RemoveRepositorys", err) | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success")) | |||
ctx.JSON(200, map[string]interface{}{ | |||
"redirect": ctx.Repo.RepoLink + "/settings/collaboration", | |||
}) | |||
} | |||
// parseOwnerAndRepo get repos by owner | |||
func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) { | |||
owner, err := models.GetUserByName(ctx.Params(":username")) |
@@ -185,3 +185,196 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) { | |||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||
assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||
} | |||
func TestAddTeamPost(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "org26/repo43") | |||
ctx.Req.Form.Set("team", "team11") | |||
org := &models.User{ | |||
LowerName: "org26", | |||
Type: models.UserTypeOrganization, | |||
} | |||
team := &models.Team{ | |||
ID: 11, | |||
OrgID: 26, | |||
} | |||
re := &models.Repository{ | |||
ID: 43, | |||
Owner: org, | |||
OwnerID: 26, | |||
} | |||
repo := &context.Repository{ | |||
Owner: &models.User{ | |||
ID: 26, | |||
LowerName: "org26", | |||
RepoAdminChangeTeamAccess: true, | |||
}, | |||
Repository: re, | |||
} | |||
ctx.Repo = repo | |||
AddTeamPost(ctx) | |||
assert.True(t, team.HasRepository(re.ID)) | |||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||
assert.Empty(t, ctx.Flash.ErrorMsg) | |||
} | |||
func TestAddTeamPost_NotAllowed(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "org26/repo43") | |||
ctx.Req.Form.Set("team", "team11") | |||
org := &models.User{ | |||
LowerName: "org26", | |||
Type: models.UserTypeOrganization, | |||
} | |||
team := &models.Team{ | |||
ID: 11, | |||
OrgID: 26, | |||
} | |||
re := &models.Repository{ | |||
ID: 43, | |||
Owner: org, | |||
OwnerID: 26, | |||
} | |||
repo := &context.Repository{ | |||
Owner: &models.User{ | |||
ID: 26, | |||
LowerName: "org26", | |||
RepoAdminChangeTeamAccess: false, | |||
}, | |||
Repository: re, | |||
} | |||
ctx.Repo = repo | |||
AddTeamPost(ctx) | |||
assert.False(t, team.HasRepository(re.ID)) | |||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||
assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||
} | |||
func TestAddTeamPost_AddTeamTwice(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "org26/repo43") | |||
ctx.Req.Form.Set("team", "team11") | |||
org := &models.User{ | |||
LowerName: "org26", | |||
Type: models.UserTypeOrganization, | |||
} | |||
team := &models.Team{ | |||
ID: 11, | |||
OrgID: 26, | |||
} | |||
re := &models.Repository{ | |||
ID: 43, | |||
Owner: org, | |||
OwnerID: 26, | |||
} | |||
repo := &context.Repository{ | |||
Owner: &models.User{ | |||
ID: 26, | |||
LowerName: "org26", | |||
RepoAdminChangeTeamAccess: true, | |||
}, | |||
Repository: re, | |||
} | |||
ctx.Repo = repo | |||
AddTeamPost(ctx) | |||
AddTeamPost(ctx) | |||
assert.True(t, team.HasRepository(re.ID)) | |||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||
assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||
} | |||
func TestAddTeamPost_NonExistentTeam(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "org26/repo43") | |||
ctx.Req.Form.Set("team", "team-non-existent") | |||
org := &models.User{ | |||
LowerName: "org26", | |||
Type: models.UserTypeOrganization, | |||
} | |||
re := &models.Repository{ | |||
ID: 43, | |||
Owner: org, | |||
OwnerID: 26, | |||
} | |||
repo := &context.Repository{ | |||
Owner: &models.User{ | |||
ID: 26, | |||
LowerName: "org26", | |||
RepoAdminChangeTeamAccess: true, | |||
}, | |||
Repository: re, | |||
} | |||
ctx.Repo = repo | |||
AddTeamPost(ctx) | |||
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||
assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||
} | |||
func TestDeleteTeam(t *testing.T) { | |||
models.PrepareTestEnv(t) | |||
ctx := test.MockContext(t, "org3/team1/repo3") | |||
ctx.Req.Form.Set("id", "2") | |||
org := &models.User{ | |||
LowerName: "org3", | |||
Type: models.UserTypeOrganization, | |||
} | |||
team := &models.Team{ | |||
ID: 2, | |||
OrgID: 3, | |||
} | |||
re := &models.Repository{ | |||
ID: 3, | |||
Owner: org, | |||
OwnerID: 3, | |||
} | |||
repo := &context.Repository{ | |||
Owner: &models.User{ | |||
ID: 3, | |||
LowerName: "org3", | |||
RepoAdminChangeTeamAccess: true, | |||
}, | |||
Repository: re, | |||
} | |||
ctx.Repo = repo | |||
DeleteTeam(ctx) | |||
assert.False(t, team.HasRepository(re.ID)) | |||
} |
@@ -629,6 +629,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) | |||
m.Post("/access_mode", repo.ChangeCollaborationAccessMode) | |||
m.Post("/delete", repo.DeleteCollaboration) | |||
m.Group("/team", func() { | |||
m.Post("", repo.AddTeamPost) | |||
m.Post("/delete", repo.DeleteTeam) | |||
}) | |||
}) | |||
m.Group("/branches", func() { | |||
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) |
@@ -32,6 +32,17 @@ | |||
</div> | |||
</div> | |||
</div> | |||
<div class="field" id="permission_box"> | |||
<label>{{.i18n.Tr "org.settings.permission"}}</label> | |||
<div class="field"> | |||
<div class="ui checkbox"> | |||
<input class="hidden" type="checkbox" name="repo_admin_change_team_access" checked/> | |||
<label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<label></label> | |||
<button class="ui green button"> |
@@ -56,6 +56,16 @@ | |||
</div> | |||
</div> | |||
<div class="field" id="permission_box"> | |||
<label>{{.i18n.Tr "org.settings.permission"}}</label> | |||
<div class="field"> | |||
<div class="ui checkbox"> | |||
<input class="hidden" type="checkbox" name="repo_admin_change_team_access" checked/> | |||
<label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label> | |||
</div> | |||
</div> | |||
</div> | |||
{{if .SignedUser.IsAdmin}} | |||
<div class="ui divider"></div> | |||
@@ -20,7 +20,7 @@ | |||
<div class="ui eight wide column"> | |||
<span class="octicon octicon-shield"></span> | |||
<div class="ui inline dropdown"> | |||
<div class="text">{{$.i18n.Tr .Collaboration.ModeI18nKey}}</div> | |||
<div class="text">{{if eq .Collaboration.Mode 1}}{{$.i18n.Tr "repo.settings.collaboration.read"}}{{else if eq .Collaboration.Mode 2}}{{$.i18n.Tr "repo.settings.collaboration.write"}}{{else if eq .Collaboration.Mode 3}}{{$.i18n.Tr "repo.settings.collaboration.admin"}}{{else}}{{$.i18n.Tr "repo.settings.collaboration.undefined"}}{{end}}</div> | |||
<i class="dropdown icon"></i> | |||
<div class="access-mode menu" data-url="{{$.Link}}/access_mode" data-uid="{{.ID}}"> | |||
<div class="item" data-text="{{$.i18n.Tr "repo.settings.collaboration.admin"}}" data-value="3">{{$.i18n.Tr "repo.settings.collaboration.admin"}}</div> | |||
@@ -51,6 +51,63 @@ | |||
<button class="ui green button">{{.i18n.Tr "repo.settings.add_collaborator"}}</button> | |||
</form> | |||
</div> | |||
<h4 class="ui top attached header"> | |||
Teams | |||
</h4> | |||
{{ $allowedToChangeTeams := ( or (.Org.RepoAdminChangeTeamAccess) (.Permission.IsOwner)) }} | |||
{{if .Teams}} | |||
<div class="ui attached segment collaborator list"> | |||
{{range $t, $team := .Teams}} | |||
<div class="item ui grid"> | |||
<div class="ui five wide column"> | |||
<a href="{{AppSubUrl}}/org/{{$.OrgName}}/teams/{{.LowerName}}"> | |||
{{.Name}} | |||
</a> | |||
</div> | |||
<div class="ui eight wide column poping up" data-content="Team's permission is set on the team setting page and can't be changed per repository"> | |||
<span class="octicon octicon-shield"></span> | |||
<div class="ui inline dropdown"> | |||
<div class="text">{{if eq .Authorize 1}}{{$.i18n.Tr "repo.settings.collaboration.read"}}{{else if eq .Authorize 2}}{{$.i18n.Tr "repo.settings.collaboration.write"}}{{else if eq .Authorize 3}}{{$.i18n.Tr "repo.settings.collaboration.admin"}}{{else if eq .Authorize 4}}{{$.i18n.Tr "repo.settings.collaboration.owner"}}{{else}}{{$.i18n.Tr "repo.settings.collaboration.undefined"}}{{end}}</div> | |||
</div> | |||
{{ if or (eq .Authorize 1) (eq .Authorize 2) }} | |||
{{ $first := true }} | |||
<div class="description"> | |||
Sections: {{range $u, $unit := $.Units}}{{if and ($.Repo.UnitEnabled $unit.Type) ($team.UnitEnabled $unit.Type)}}{{if $first}}{{ $first = false }}{{else}}, {{end}}{{$.i18n.Tr $unit.NameKey}}{{end}}{{end}} {{if $first}}None{{end}} | |||
</div> | |||
{{end}} | |||
</div> | |||
{{if $allowedToChangeTeams}} | |||
{{ $globalRepoAccess := (eq .LowerName "owners") }} | |||
<div class="ui two wide column {{if $globalRepoAccess}}poping up{{end}}" {{if $globalRepoAccess}}data-content="This team has access to all repositories and can't be removed."{{end}}> | |||
<button class="ui red tiny button inline text-thin delete-button {{if $globalRepoAccess}}disabled{{end}}" data-url="{{$.Link}}/team/delete" data-id="{{.ID}}"> | |||
{{$.i18n.Tr "repo.settings.delete_collaborator"}} | |||
</button> | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
<div class="ui bottom attached segment"> | |||
{{if $allowedToChangeTeams}} | |||
<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post"> | |||
{{.CsrfTokenHtml}} | |||
<div class="inline field ui left"> | |||
<div id="search-team-box" class="ui search" data-org="{{.OrgID}}"> | |||
<div class="ui input"> | |||
<input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required> | |||
</div> | |||
</div> | |||
</div> | |||
<button class="ui green button">Add Team</button> | |||
</form> | |||
{{else}} | |||
<div class="item"> | |||
Changing team access for repository has been restricted to organization owner | |||
</div> | |||
{{end}} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -7718,6 +7718,10 @@ | |||
"type": "string", | |||
"x-go-name": "Location" | |||
}, | |||
"repo_admin_change_team_access": { | |||
"type": "boolean", | |||
"x-go-name": "RepoAdminChangeTeamAccess" | |||
}, | |||
"username": { | |||
"type": "string", | |||
"x-go-name": "UserName" | |||
@@ -8262,6 +8266,10 @@ | |||
"type": "string", | |||
"x-go-name": "Location" | |||
}, | |||
"repo_admin_change_team_access": { | |||
"type": "boolean", | |||
"x-go-name": "RepoAdminChangeTeamAccess" | |||
}, | |||
"visibility": { | |||
"description": "possible values are `public`, `limited` or `private`", | |||
"type": "string", | |||
@@ -9271,6 +9279,10 @@ | |||
"type": "string", | |||
"x-go-name": "Location" | |||
}, | |||
"repo_admin_change_team_access": { | |||
"type": "boolean", | |||
"x-go-name": "RepoAdminChangeTeamAccess" | |||
}, | |||
"username": { | |||
"type": "string", | |||
"x-go-name": "UserName" |