* 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.11.0-dev
| @@ -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": [ | |||