Skip to content

Commit 36bcd4c

Browse files
davidsvantessonlunny
authored andcommitted
API endpoint for searching teams. (#8108)
* Api endpoint for searching teams. Signed-off-by: dasv <[email protected]> * Move API to /orgs/:org/teams/search Signed-off-by: David Svantesson <[email protected]> * Regenerate swagger Signed-off-by: David Svantesson <[email protected]> * Fix search is Get Signed-off-by: David Svantesson <[email protected]> * Add test for search team API. Signed-off-by: David Svantesson <[email protected]> * Update routers/api/v1/org/team.go grammar Co-Authored-By: Richard Mahn <[email protected]> * Fix review comments Signed-off-by: David Svantesson <[email protected]> * Fix some issues in repo collaboration team search, after changes in this PR. Signed-off-by: David Svantesson <[email protected]> * Remove teamUser which is not used and replace with actual user id. Signed-off-by: David Svantesson <[email protected]> * Remove unused search variable UserIsAdmin. * Add paging to team search. * Re-genereate swagger Signed-off-by: David Svantesson <[email protected]> * Fix review comments Signed-off-by: David Svantesson <[email protected]> * fix * Regenerate swagger
1 parent d3bc3dd commit 36bcd4c

File tree

7 files changed

+246
-5
lines changed

7 files changed

+246
-5
lines changed

integrations/api_team_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,32 @@ func checkTeamBean(t *testing.T, id int64, name, description string, permission
107107
assert.NoError(t, team.GetUnits(), "GetUnits")
108108
checkTeamResponse(t, convert.ToTeam(team), name, description, permission, units)
109109
}
110+
111+
type TeamSearchResults struct {
112+
OK bool `json:"ok"`
113+
Data []*api.Team `json:"data"`
114+
}
115+
116+
func TestAPITeamSearch(t *testing.T) {
117+
prepareTestEnv(t)
118+
119+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
120+
org := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)
121+
122+
var results TeamSearchResults
123+
124+
session := loginUser(t, user.Name)
125+
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "_team")
126+
resp := session.MakeRequest(t, req, http.StatusOK)
127+
DecodeJSON(t, resp, &results)
128+
assert.NotEmpty(t, results.Data)
129+
assert.Equal(t, 1, len(results.Data))
130+
assert.Equal(t, "test_team", results.Data[0].Name)
131+
132+
// no access if not organization member
133+
user5 := models.AssertExistsAndLoadBean(t, &models.User{ID: 5}).(*models.User)
134+
session = loginUser(t, user5.Name)
135+
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s", org.Name, "team")
136+
resp = session.MakeRequest(t, req, http.StatusForbidden)
137+
138+
}

models/org_team.go

+62
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"code.gitea.io/gitea/modules/setting"
1616

1717
"github.com/go-xorm/xorm"
18+
"xorm.io/builder"
1819
)
1920

2021
const ownerTeamName = "Owners"
@@ -34,6 +35,67 @@ type Team struct {
3435
Units []*TeamUnit `xorm:"-"`
3536
}
3637

38+
// SearchTeamOptions holds the search options
39+
type SearchTeamOptions struct {
40+
UserID int64
41+
Keyword string
42+
OrgID int64
43+
IncludeDesc bool
44+
PageSize int
45+
Page int
46+
}
47+
48+
// SearchTeam search for teams. Caller is responsible to check permissions.
49+
func SearchTeam(opts *SearchTeamOptions) ([]*Team, int64, error) {
50+
if opts.Page <= 0 {
51+
opts.Page = 1
52+
}
53+
if opts.PageSize == 0 {
54+
// Default limit
55+
opts.PageSize = 10
56+
}
57+
58+
var cond = builder.NewCond()
59+
60+
if len(opts.Keyword) > 0 {
61+
lowerKeyword := strings.ToLower(opts.Keyword)
62+
var keywordCond builder.Cond = builder.Like{"lower_name", lowerKeyword}
63+
if opts.IncludeDesc {
64+
keywordCond = keywordCond.Or(builder.Like{"LOWER(description)", lowerKeyword})
65+
}
66+
cond = cond.And(keywordCond)
67+
}
68+
69+
cond = cond.And(builder.Eq{"org_id": opts.OrgID})
70+
71+
sess := x.NewSession()
72+
defer sess.Close()
73+
74+
count, err := sess.
75+
Where(cond).
76+
Count(new(Team))
77+
78+
if err != nil {
79+
return nil, 0, err
80+
}
81+
82+
sess = sess.Where(cond)
83+
if opts.PageSize == -1 {
84+
opts.PageSize = int(count)
85+
} else {
86+
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
87+
}
88+
89+
teams := make([]*Team, 0, opts.PageSize)
90+
if err = sess.
91+
OrderBy("lower_name").
92+
Find(&teams); err != nil {
93+
return nil, 0, err
94+
}
95+
96+
return teams, count, nil
97+
}
98+
3799
// ColorFormat provides a basic color format for a Team
38100
func (t *Team) ColorFormat(s fmt.State) {
39101
log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v",

public/js/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1766,11 +1766,11 @@ function searchTeams() {
17661766
$searchTeamBox.search({
17671767
minCharacters: 2,
17681768
apiSettings: {
1769-
url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams',
1769+
url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams/search?q={query}',
17701770
headers: {"X-Csrf-Token": csrf},
17711771
onResponse: function(response) {
17721772
const items = [];
1773-
$.each(response, function (_i, item) {
1773+
$.each(response.data, function (_i, item) {
17741774
const title = item.name + ' (' + item.permission + ' access)';
17751775
items.push({
17761776
title: title,

routers/api/v1/api.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -802,8 +802,11 @@ func RegisterRoutes(m *macaron.Macaron) {
802802
Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
803803
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
804804
})
805-
m.Combo("/teams", reqToken(), reqOrgMembership()).Get(org.ListTeams).
806-
Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
805+
m.Group("/teams", func() {
806+
m.Combo("", reqToken()).Get(org.ListTeams).
807+
Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
808+
m.Get("/search", org.SearchTeam)
809+
}, reqOrgMembership())
807810
m.Group("/hooks", func() {
808811
m.Combo("").Get(org.ListHooks).
809812
Post(bind(api.CreateHookOption{}), org.CreateHook)

routers/api/v1/org/team.go

+83
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
package org
77

88
import (
9+
"strings"
10+
911
"code.gitea.io/gitea/models"
1012
"code.gitea.io/gitea/modules/context"
13+
"code.gitea.io/gitea/modules/log"
1114
api "code.gitea.io/gitea/modules/structs"
1215
"code.gitea.io/gitea/routers/api/v1/convert"
1316
"code.gitea.io/gitea/routers/api/v1/user"
@@ -504,3 +507,83 @@ func RemoveTeamRepository(ctx *context.APIContext) {
504507
}
505508
ctx.Status(204)
506509
}
510+
511+
// SearchTeam api for searching teams
512+
func SearchTeam(ctx *context.APIContext) {
513+
// swagger:operation GET /orgs/{org}/teams/search organization teamSearch
514+
// ---
515+
// summary: Search for teams within an organization
516+
// produces:
517+
// - application/json
518+
// parameters:
519+
// - name: org
520+
// in: path
521+
// description: name of the organization
522+
// type: string
523+
// required: true
524+
// - name: q
525+
// in: query
526+
// description: keywords to search
527+
// type: string
528+
// - name: include_desc
529+
// in: query
530+
// description: include search within team description (defaults to true)
531+
// type: boolean
532+
// - name: limit
533+
// in: query
534+
// description: limit size of results
535+
// type: integer
536+
// - name: page
537+
// in: query
538+
// description: page number of results to return (1-based)
539+
// type: integer
540+
// responses:
541+
// "200":
542+
// description: "SearchResults of a successful search"
543+
// schema:
544+
// type: object
545+
// properties:
546+
// ok:
547+
// type: boolean
548+
// data:
549+
// type: array
550+
// items:
551+
// "$ref": "#/definitions/Team"
552+
opts := &models.SearchTeamOptions{
553+
UserID: ctx.User.ID,
554+
Keyword: strings.TrimSpace(ctx.Query("q")),
555+
OrgID: ctx.Org.Organization.ID,
556+
IncludeDesc: (ctx.Query("include_desc") == "" || ctx.QueryBool("include_desc")),
557+
PageSize: ctx.QueryInt("limit"),
558+
Page: ctx.QueryInt("page"),
559+
}
560+
561+
teams, _, err := models.SearchTeam(opts)
562+
if err != nil {
563+
log.Error("SearchTeam failed: %v", err)
564+
ctx.JSON(500, map[string]interface{}{
565+
"ok": false,
566+
"error": "SearchTeam internal failure",
567+
})
568+
return
569+
}
570+
571+
apiTeams := make([]*api.Team, len(teams))
572+
for i := range teams {
573+
if err := teams[i].GetUnits(); err != nil {
574+
log.Error("Team GetUnits failed: %v", err)
575+
ctx.JSON(500, map[string]interface{}{
576+
"ok": false,
577+
"error": "SearchTeam failed to get units",
578+
})
579+
return
580+
}
581+
apiTeams[i] = convert.ToTeam(teams[i])
582+
}
583+
584+
ctx.JSON(200, map[string]interface{}{
585+
"ok": true,
586+
"data": apiTeams,
587+
})
588+
589+
}

templates/repo/settings/collaboration.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
9696
{{.CsrfTokenHtml}}
9797
<div class="inline field ui left">
98-
<div id="search-team-box" class="ui search" data-org="{{.OrgID}}">
98+
<div id="search-team-box" class="ui search" data-org="{{.OrgName}}">
9999
<div class="ui input">
100100
<input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required>
101101
</div>

templates/swagger/v1_json.tmpl

+64
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,70 @@
10471047
}
10481048
}
10491049
},
1050+
"/orgs/{org}/teams/search": {
1051+
"get": {
1052+
"produces": [
1053+
"application/json"
1054+
],
1055+
"tags": [
1056+
"organization"
1057+
],
1058+
"summary": "Search for teams within an organization",
1059+
"operationId": "teamSearch",
1060+
"parameters": [
1061+
{
1062+
"type": "string",
1063+
"description": "name of the organization",
1064+
"name": "org",
1065+
"in": "path",
1066+
"required": true
1067+
},
1068+
{
1069+
"type": "string",
1070+
"description": "keywords to search",
1071+
"name": "q",
1072+
"in": "query"
1073+
},
1074+
{
1075+
"type": "boolean",
1076+
"description": "include search within team description (defaults to true)",
1077+
"name": "include_desc",
1078+
"in": "query"
1079+
},
1080+
{
1081+
"type": "integer",
1082+
"description": "limit size of results",
1083+
"name": "limit",
1084+
"in": "query"
1085+
},
1086+
{
1087+
"type": "integer",
1088+
"description": "page number of results to return (1-based)",
1089+
"name": "page",
1090+
"in": "query"
1091+
}
1092+
],
1093+
"responses": {
1094+
"200": {
1095+
"description": "SearchResults of a successful search",
1096+
"schema": {
1097+
"type": "object",
1098+
"properties": {
1099+
"data": {
1100+
"type": "array",
1101+
"items": {
1102+
"$ref": "#/definitions/Team"
1103+
}
1104+
},
1105+
"ok": {
1106+
"type": "boolean"
1107+
}
1108+
}
1109+
}
1110+
}
1111+
}
1112+
}
1113+
},
10501114
"/repos/migrate": {
10511115
"post": {
10521116
"consumes": [

0 commit comments

Comments
 (0)