diff options
author | David Svantesson <davidsvantesson@gmail.com> | 2019-10-01 07:32:28 +0200 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2019-10-01 13:32:28 +0800 |
commit | 36bcd4cd6b6d1131e3f812a825558fbfe5dcca20 (patch) | |
tree | 1606d302648ca6f2f6633d21aeb5d06a568b14d8 | |
parent | d3bc3dd4d16aef053699d5bfc45039db060d373e (diff) | |
download | gitea-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.go | 29 | ||||
-rw-r--r-- | models/org_team.go | 62 | ||||
-rw-r--r-- | public/js/index.js | 4 | ||||
-rw-r--r-- | routers/api/v1/api.go | 7 | ||||
-rw-r--r-- | routers/api/v1/org/team.go | 83 | ||||
-rw-r--r-- | templates/repo/settings/collaboration.tmpl | 2 | ||||
-rw-r--r-- | templates/swagger/v1_json.tmpl | 64 |
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": [ |