summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Svantesson <davidsvantesson@gmail.com>2019-10-01 07:32:28 +0200
committerLunny Xiao <xiaolunwen@gmail.com>2019-10-01 13:32:28 +0800
commit36bcd4cd6b6d1131e3f812a825558fbfe5dcca20 (patch)
tree1606d302648ca6f2f6633d21aeb5d06a568b14d8
parentd3bc3dd4d16aef053699d5bfc45039db060d373e (diff)
downloadgitea-36bcd4cd6b6d1131e3f812a825558fbfe5dcca20.tar.gz
gitea-36bcd4cd6b6d1131e3f812a825558fbfe5dcca20.zip
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
-rw-r--r--integrations/api_team_test.go29
-rw-r--r--models/org_team.go62
-rw-r--r--public/js/index.js4
-rw-r--r--routers/api/v1/api.go7
-rw-r--r--routers/api/v1/org/team.go83
-rw-r--r--templates/repo/settings/collaboration.tmpl2
-rw-r--r--templates/swagger/v1_json.tmpl64
7 files changed, 246 insertions, 5 deletions
diff --git a/integrations/api_team_test.go b/integrations/api_team_test.go
index a7c22d6ba1..38e202f239 100644
--- a/integrations/api_team_test.go
+++ b/integrations/api_team_test.go
@@ -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)
+
+}
diff --git a/models/org_team.go b/models/org_team.go
index 90a089417d..fc5d5834ef 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -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",
diff --git a/public/js/index.js b/public/js/index.js
index ad5e3912de..8a85ad9157 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -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,
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c57edf6a99..04ff91fbbf 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -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)
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index 7b8fd12fba..d01f051626 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -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,
+ })
+
+}
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index 61feb4ec18..c0b444dce3 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -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>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index a5fef2f5e6..fcc26f5c54 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -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": [