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). | ||||
| 			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": [ | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue