mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 13:28:25 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						
						Lunny Xiao
					
				
			
			
				
	
			
			
			
						parent
						
							d3bc3dd4d1
						
					
				
				
					commit
					36bcd4cd6b
				
			@@ -107,3 +107,32 @@ func checkTeamBean(t *testing.T, id int64, name, description string, permission
 | 
				
			|||||||
	assert.NoError(t, team.GetUnits(), "GetUnits")
 | 
						assert.NoError(t, team.GetUnits(), "GetUnits")
 | 
				
			||||||
	checkTeamResponse(t, convert.ToTeam(team), name, description, permission, units)
 | 
						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"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/go-xorm/xorm"
 | 
						"github.com/go-xorm/xorm"
 | 
				
			||||||
 | 
						"xorm.io/builder"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ownerTeamName = "Owners"
 | 
					const ownerTeamName = "Owners"
 | 
				
			||||||
@@ -34,6 +35,67 @@ type Team struct {
 | 
				
			|||||||
	Units       []*TeamUnit `xorm:"-"`
 | 
						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
 | 
					// ColorFormat provides a basic color format for a Team
 | 
				
			||||||
func (t *Team) ColorFormat(s fmt.State) {
 | 
					func (t *Team) ColorFormat(s fmt.State) {
 | 
				
			||||||
	log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v",
 | 
						log.ColorFprintf(s, "%d:%s (OrgID: %d) %-v",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1766,11 +1766,11 @@ function searchTeams() {
 | 
				
			|||||||
    $searchTeamBox.search({
 | 
					    $searchTeamBox.search({
 | 
				
			||||||
        minCharacters: 2,
 | 
					        minCharacters: 2,
 | 
				
			||||||
        apiSettings: {
 | 
					        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},
 | 
					            headers: {"X-Csrf-Token": csrf},
 | 
				
			||||||
            onResponse: function(response) {
 | 
					            onResponse: function(response) {
 | 
				
			||||||
                const items = [];
 | 
					                const items = [];
 | 
				
			||||||
                $.each(response, function (_i, item) {
 | 
					                $.each(response.data, function (_i, item) {
 | 
				
			||||||
                    const title = item.name + ' (' + item.permission + ' access)';
 | 
					                    const title = item.name + ' (' + item.permission + ' access)';
 | 
				
			||||||
                    items.push({
 | 
					                    items.push({
 | 
				
			||||||
                        title: title,
 | 
					                        title: title,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -802,8 +802,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
				
			|||||||
					Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
 | 
										Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
 | 
				
			||||||
					Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
 | 
										Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			m.Combo("/teams", reqToken(), reqOrgMembership()).Get(org.ListTeams).
 | 
								m.Group("/teams", func() {
 | 
				
			||||||
				Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
 | 
									m.Combo("", reqToken()).Get(org.ListTeams).
 | 
				
			||||||
 | 
										Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
 | 
				
			||||||
 | 
									m.Get("/search", org.SearchTeam)
 | 
				
			||||||
 | 
								}, reqOrgMembership())
 | 
				
			||||||
			m.Group("/hooks", func() {
 | 
								m.Group("/hooks", func() {
 | 
				
			||||||
				m.Combo("").Get(org.ListHooks).
 | 
									m.Combo("").Get(org.ListHooks).
 | 
				
			||||||
					Post(bind(api.CreateHookOption{}), org.CreateHook)
 | 
										Post(bind(api.CreateHookOption{}), org.CreateHook)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,11 @@
 | 
				
			|||||||
package org
 | 
					package org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/api/v1/convert"
 | 
						"code.gitea.io/gitea/routers/api/v1/convert"
 | 
				
			||||||
	"code.gitea.io/gitea/routers/api/v1/user"
 | 
						"code.gitea.io/gitea/routers/api/v1/user"
 | 
				
			||||||
@@ -504,3 +507,83 @@ func RemoveTeamRepository(ctx *context.APIContext) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	ctx.Status(204)
 | 
						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">
 | 
									<form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post">
 | 
				
			||||||
					{{.CsrfTokenHtml}}
 | 
										{{.CsrfTokenHtml}}
 | 
				
			||||||
					<div class="inline field ui left">
 | 
										<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">
 | 
												<div class="ui input">
 | 
				
			||||||
								<input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required>
 | 
													<input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required>
 | 
				
			||||||
							</div>
 | 
												</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": {
 | 
					    "/repos/migrate": {
 | 
				
			||||||
      "post": {
 | 
					      "post": {
 | 
				
			||||||
        "consumes": [
 | 
					        "consumes": [
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user