mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18: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")
 | 
			
		||||
	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": [
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user