API endpoint for searching teams. (#8108)
* 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 swagger
This commit is contained in:
parent
d3bc3dd4d1
commit
36bcd4cd6b
7 changed files with 246 additions and 5 deletions
|
@ -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": [
|
||||
|
|
Reference in a new issue