* Api endpoint for searching teams. Signed-off-by: dasv <david.svantesson@qrtech.se> * Move API to /orgs/:org/teams/search Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Regenerate swagger Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix search is Get Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add test for search team API. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Update routers/api/v1/org/team.go grammar Co-Authored-By: Richard Mahn <richmahn@users.noreply.github.com> * Fix review comments Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix some issues in repo collaboration team search, after changes in this PR. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Remove teamUser which is not used and replace with actual user id. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Remove unused search variable UserIsAdmin. * Add paging to team search. * Re-genereate swagger Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix review comments Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * fix * Regenerate swaggertags/v1.10.0-rc1
@@ -107,3 +107,32 @@ func checkTeamBean(t *testing.T, id int64, name, description string, permission | |||
assert.NoError(t, team.GetUnits(), "GetUnits") | |||
checkTeamResponse(t, convert.ToTeam(team), name, description, permission, units) | |||
} | |||
type TeamSearchResults struct { | |||
OK bool `json:"ok"` | |||
Data []*api.Team `json:"data"` | |||
} | |||
func TestAPITeamSearch(t *testing.T) { | |||
prepareTestEnv(t) | |||
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | |||
org := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) | |||
var results TeamSearchResults | |||
session := loginUser(t, user.Name) | |||
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "_team") | |||
resp := session.MakeRequest(t, req, http.StatusOK) | |||
DecodeJSON(t, resp, &results) | |||
assert.NotEmpty(t, results.Data) | |||
assert.Equal(t, 1, len(results.Data)) | |||
assert.Equal(t, "test_team", results.Data[0].Name) | |||
// no access if not organization member | |||
user5 := models.AssertExistsAndLoadBean(t, &models.User{ID: 5}).(*models.User) | |||
session = loginUser(t, user5.Name) | |||
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "team") | |||
resp = session.MakeRequest(t, req, http.StatusForbidden) | |||
} |
@@ -15,6 +15,7 @@ import ( | |||
"code.gitea.io/gitea/modules/setting" | |||
"github.com/go-xorm/xorm" | |||
"xorm.io/builder" | |||
) | |||
const ownerTeamName = "Owners" | |||
@@ -34,6 +35,67 @@ type Team struct { | |||
Units []*TeamUnit `xorm:"-"` | |||
} | |||
// SearchTeamOptions holds the search options | |||
type SearchTeamOptions struct { | |||
UserID int64 | |||
Keyword string | |||
OrgID int64 | |||
IncludeDesc bool | |||
PageSize int | |||
Page int | |||
} | |||
// SearchTeam search for teams. Caller is responsible to check permissions. | |||
func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) { | |||
if opts.Page <= 0 { | |||
opts.Page = 1 | |||
} | |||
if opts.PageSize == 0 { | |||
// Default limit | |||
opts.PageSize = 10 | |||
} | |||
var cond = builder.NewCond() | |||
if len(opts.Keyword) > 0 { | |||
lowerKeyword := strings.ToLower(opts.Keyword) | |||
var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword} | |||
if opts.IncludeDesc { | |||
keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword}) | |||
} | |||
cond = cond.And(keywordCond) | |||
} | |||
cond = cond.And(builder.Eq{"org_id": opts.OrgID}) | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
count, err := sess. | |||
Where(cond). | |||
Count(new(Team)) | |||
if err != nil { | |||
return nil, 0, err | |||
} | |||
sess = sess.Where(cond) | |||
if opts.PageSize == -1 { | |||
opts.PageSize = int(count) | |||
} else { | |||
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) | |||
} | |||
teams := make([]*Team, 0, opts.PageSize) | |||
if err = sess. | |||
OrderBy("lower_name"). | |||
Find(&teams); err != nil { | |||
return nil, 0, err | |||
} | |||
return teams, count, nil | |||
} | |||
// ColorFormat provides a basic color format for a Team | |||
func (t *Team) ColorFormat(s fmt.State) { | |||
log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v", |
@@ -1766,11 +1766,11 @@ function searchTeams() { | |||
$searchTeamBox.search({ | |||
minCharacters: 2, | |||
apiSettings: { | |||
url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams', | |||
url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams/search?q={query}', | |||
headers: {"X-Csrf-Token": csrf}, | |||
onResponse: function(response) { | |||
const items = []; | |||
$.each(response, function (_i, item) { | |||
$.each(response.data, function (_i, item) { | |||
const title = item.name + ' (' + item.permission + ' access)'; | |||
items.push({ | |||
title: title, |
@@ -802,8 +802,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
Put(reqToken(), reqOrgMembership(), org.PublicizeMember). | |||
Delete(reqToken(), reqOrgMembership(), org.ConcealMember) | |||
}) | |||
m.Combo("/teams", reqToken(), reqOrgMembership()).Get(org.ListTeams). | |||
Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam) | |||
m.Group("/teams", func() { | |||
m.Combo("", reqToken()).Get(org.ListTeams). | |||
Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam) | |||
m.Get("/search", org.SearchTeam) | |||
}, reqOrgMembership()) | |||
m.Group("/hooks", func() { | |||
m.Combo("").Get(org.ListHooks). | |||
Post(bind(api.CreateHookOption{}), org.CreateHook) |
@@ -6,8 +6,11 @@ | |||
package org | |||
import ( | |||
"strings" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/log" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/routers/api/v1/convert" | |||
"code.gitea.io/gitea/routers/api/v1/user" | |||
@@ -504,3 +507,83 @@ func RemoveTeamRepository(ctx *context.APIContext) { | |||
} | |||
ctx.Status(204) | |||
} | |||
// SearchTeam api for searching teams | |||
func SearchTeam(ctx *context.APIContext) { | |||
// swagger:operation GET /orgs/{org}/teams/search organization teamSearch | |||
// --- | |||
// summary: Search for teams within an organization | |||
// produces: | |||
// - application/json | |||
// parameters: | |||
// - name: org | |||
// in: path | |||
// description: name of the organization | |||
// type: string | |||
// required: true | |||
// - name: q | |||
// in: query | |||
// description: keywords to search | |||
// type: string | |||
// - name: include_desc | |||
// in: query | |||
// description: include search within team description (defaults to true) | |||
// type: boolean | |||
// - name: limit | |||
// in: query | |||
// description: limit size of results | |||
// type: integer | |||
// - name: page | |||
// in: query | |||
// description: page number of results to return (1-based) | |||
// type: integer | |||
// responses: | |||
// "200": | |||
// description: "SearchResults of a successful search" | |||
// schema: | |||
// type: object | |||
// properties: | |||
// ok: | |||
// type: boolean | |||
// data: | |||
// type: array | |||
// items: | |||
// "$ref": "#/definitions/Team" | |||
opts := &models.SearchTeamOptions{ | |||
UserID: ctx.User.ID, | |||
Keyword: strings.TrimSpace(ctx.Query("q")), | |||
OrgID: ctx.Org.Organization.ID, | |||
IncludeDesc: (ctx.Query("include_desc") == "" || ctx.QueryBool("include_desc")), | |||
PageSize: ctx.QueryInt("limit"), | |||
Page: ctx.QueryInt("page"), | |||
} | |||
teams, _, err := models.SearchTeam(opts) | |||
if err != nil { | |||
log.Error("SearchTeam failed: %v", err) | |||
ctx.JSON(500, map[string]interface{}{ | |||
"ok": false, | |||
"error": "SearchTeam internal failure", | |||
}) | |||
return | |||
} | |||
apiTeams := make([]*api.Team, len(teams)) | |||
for i := range teams { | |||
if err := teams[i].GetUnits(); err != nil { | |||
log.Error("Team GetUnits failed: %v", err) | |||
ctx.JSON(500, map[string]interface{}{ | |||
"ok": false, | |||
"error": "SearchTeam failed to get units", | |||
}) | |||
return | |||
} | |||
apiTeams[i] = convert.ToTeam(teams[i]) | |||
} | |||
ctx.JSON(200, map[string]interface{}{ | |||
"ok": true, | |||
"data": apiTeams, | |||
}) | |||
} |
@@ -95,7 +95,7 @@ | |||
<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 id="search-team-box" class="ui search" data-org="{{.OrgName}}"> | |||
<div class="ui input"> | |||
<input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required> | |||
</div> |
@@ -1047,6 +1047,70 @@ | |||
} | |||
} | |||
}, | |||
"/orgs/{org}/teams/search": { | |||
"get": { | |||
"produces": [ | |||
"application/json" | |||
], | |||
"tags": [ | |||
"organization" | |||
], | |||
"summary": "Search for teams within an organization", | |||
"operationId": "teamSearch", | |||
"parameters": [ | |||
{ | |||
"type": "string", | |||
"description": "name of the organization", | |||
"name": "org", | |||
"in": "path", | |||
"required": true | |||
}, | |||
{ | |||
"type": "string", | |||
"description": "keywords to search", | |||
"name": "q", | |||
"in": "query" | |||
}, | |||
{ | |||
"type": "boolean", | |||
"description": "include search within team description (defaults to true)", | |||
"name": "include_desc", | |||
"in": "query" | |||
}, | |||
{ | |||
"type": "integer", | |||
"description": "limit size of results", | |||
"name": "limit", | |||
"in": "query" | |||
}, | |||
{ | |||
"type": "integer", | |||
"description": "page number of results to return (1-based)", | |||
"name": "page", | |||
"in": "query" | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"description": "SearchResults of a successful search", | |||
"schema": { | |||
"type": "object", | |||
"properties": { | |||
"data": { | |||
"type": "array", | |||
"items": { | |||
"$ref": "#/definitions/Team" | |||
} | |||
}, | |||
"ok": { | |||
"type": "boolean" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"/repos/migrate": { | |||
"post": { | |||
"consumes": [ |