[API] Add more filters to issues search (#13514)
* Add time filter for issue search * Add limit option for paggination * Add Filter for: Created by User, Assigned to User, Mentioning User * update swagger * Add Tests for limit, before & since
This commit is contained in:
		
							parent
							
								
									78204a7a71
								
							
						
					
					
						commit
						f88a2eae97
					
				
					 4 changed files with 130 additions and 9 deletions
				
			
		| 
						 | 
					@ -9,6 +9,7 @@ import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
						"code.gitea.io/gitea/models"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
| 
						 | 
					@ -152,17 +153,27 @@ func TestAPISearchIssues(t *testing.T) {
 | 
				
			||||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
	var apiIssues []*api.Issue
 | 
						var apiIssues []*api.Issue
 | 
				
			||||||
	DecodeJSON(t, resp, &apiIssues)
 | 
						DecodeJSON(t, resp, &apiIssues)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	assert.Len(t, apiIssues, 10)
 | 
						assert.Len(t, apiIssues, 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	query := url.Values{}
 | 
						query := url.Values{"token": {token}}
 | 
				
			||||||
	query.Add("token", token)
 | 
					 | 
				
			||||||
	link.RawQuery = query.Encode()
 | 
						link.RawQuery = query.Encode()
 | 
				
			||||||
	req = NewRequest(t, "GET", link.String())
 | 
						req = NewRequest(t, "GET", link.String())
 | 
				
			||||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
	DecodeJSON(t, resp, &apiIssues)
 | 
						DecodeJSON(t, resp, &apiIssues)
 | 
				
			||||||
	assert.Len(t, apiIssues, 10)
 | 
						assert.Len(t, apiIssues, 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801
 | 
				
			||||||
 | 
						before := time.Unix(999307200, 0).Format(time.RFC3339)
 | 
				
			||||||
 | 
						query.Add("since", since)
 | 
				
			||||||
 | 
						query.Add("before", before)
 | 
				
			||||||
 | 
						link.RawQuery = query.Encode()
 | 
				
			||||||
 | 
						req = NewRequest(t, "GET", link.String())
 | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &apiIssues)
 | 
				
			||||||
 | 
						assert.Len(t, apiIssues, 8)
 | 
				
			||||||
 | 
						query.Del("since")
 | 
				
			||||||
 | 
						query.Del("before")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	query.Add("state", "closed")
 | 
						query.Add("state", "closed")
 | 
				
			||||||
	link.RawQuery = query.Encode()
 | 
						link.RawQuery = query.Encode()
 | 
				
			||||||
	req = NewRequest(t, "GET", link.String())
 | 
						req = NewRequest(t, "GET", link.String())
 | 
				
			||||||
| 
						 | 
					@ -175,14 +186,22 @@ func TestAPISearchIssues(t *testing.T) {
 | 
				
			||||||
	req = NewRequest(t, "GET", link.String())
 | 
						req = NewRequest(t, "GET", link.String())
 | 
				
			||||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
	DecodeJSON(t, resp, &apiIssues)
 | 
						DecodeJSON(t, resp, &apiIssues)
 | 
				
			||||||
 | 
						assert.EqualValues(t, "12", resp.Header().Get("X-Total-Count"))
 | 
				
			||||||
	assert.Len(t, apiIssues, 10) //there are more but 10 is page item limit
 | 
						assert.Len(t, apiIssues, 10) //there are more but 10 is page item limit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	query.Add("page", "2")
 | 
						query.Add("limit", "20")
 | 
				
			||||||
	link.RawQuery = query.Encode()
 | 
						link.RawQuery = query.Encode()
 | 
				
			||||||
	req = NewRequest(t, "GET", link.String())
 | 
						req = NewRequest(t, "GET", link.String())
 | 
				
			||||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
	DecodeJSON(t, resp, &apiIssues)
 | 
						DecodeJSON(t, resp, &apiIssues)
 | 
				
			||||||
	assert.Len(t, apiIssues, 2)
 | 
						assert.Len(t, apiIssues, 12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						query = url.Values{"assigned": {"true"}, "state": {"all"}}
 | 
				
			||||||
 | 
						link.RawQuery = query.Encode()
 | 
				
			||||||
 | 
						req = NewRequest(t, "GET", link.String())
 | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
						DecodeJSON(t, resp, &apiIssues)
 | 
				
			||||||
 | 
						assert.Len(t, apiIssues, 1)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestAPISearchIssuesWithLabels(t *testing.T) {
 | 
					func TestAPISearchIssuesWithLabels(t *testing.T) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1100,6 +1100,8 @@ type IssuesOptions struct {
 | 
				
			||||||
	ExcludedLabelNames []string
 | 
						ExcludedLabelNames []string
 | 
				
			||||||
	SortType           string
 | 
						SortType           string
 | 
				
			||||||
	IssueIDs           []int64
 | 
						IssueIDs           []int64
 | 
				
			||||||
 | 
						UpdatedAfterUnix   int64
 | 
				
			||||||
 | 
						UpdatedBeforeUnix  int64
 | 
				
			||||||
	// prioritize issues from this repo
 | 
						// prioritize issues from this repo
 | 
				
			||||||
	PriorityRepoID int64
 | 
						PriorityRepoID int64
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1178,6 +1180,13 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
 | 
				
			||||||
		sess.In("issue.milestone_id", opts.MilestoneIDs)
 | 
							sess.In("issue.milestone_id", opts.MilestoneIDs)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opts.UpdatedAfterUnix != 0 {
 | 
				
			||||||
 | 
							sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if opts.UpdatedBeforeUnix != 0 {
 | 
				
			||||||
 | 
							sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if opts.ProjectID > 0 {
 | 
						if opts.ProjectID > 0 {
 | 
				
			||||||
		sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
 | 
							sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
 | 
				
			||||||
			And("project_issue.project_id=?", opts.ProjectID)
 | 
								And("project_issue.project_id=?", opts.ProjectID)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,14 +55,48 @@ func SearchIssues(ctx *context.APIContext) {
 | 
				
			||||||
	//   in: query
 | 
						//   in: query
 | 
				
			||||||
	//   description: filter by type (issues / pulls) if set
 | 
						//   description: filter by type (issues / pulls) if set
 | 
				
			||||||
	//   type: string
 | 
						//   type: string
 | 
				
			||||||
 | 
						// - name: since
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   format: date-time
 | 
				
			||||||
 | 
						//   required: false
 | 
				
			||||||
 | 
						// - name: before
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   format: date-time
 | 
				
			||||||
 | 
						//   required: false
 | 
				
			||||||
 | 
						// - name: assigned
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: filter (issues / pulls) assigned to you, default is false
 | 
				
			||||||
 | 
						//   type: boolean
 | 
				
			||||||
 | 
						// - name: created
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: filter (issues / pulls) created by you, default is false
 | 
				
			||||||
 | 
						//   type: boolean
 | 
				
			||||||
 | 
						// - name: mentioned
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: filter (issues / pulls) mentioning you, default is false
 | 
				
			||||||
 | 
						//   type: boolean
 | 
				
			||||||
	// - name: page
 | 
						// - name: page
 | 
				
			||||||
	//   in: query
 | 
						//   in: query
 | 
				
			||||||
	//   description: page number of requested issues
 | 
						//   description: page number of results to return (1-based)
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// - name: limit
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page size of results
 | 
				
			||||||
	//   type: integer
 | 
						//   type: integer
 | 
				
			||||||
	// responses:
 | 
						// responses:
 | 
				
			||||||
	//   "200":
 | 
						//   "200":
 | 
				
			||||||
	//     "$ref": "#/responses/IssueList"
 | 
						//     "$ref": "#/responses/IssueList"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var isClosed util.OptionalBool
 | 
						var isClosed util.OptionalBool
 | 
				
			||||||
	switch ctx.Query("state") {
 | 
						switch ctx.Query("state") {
 | 
				
			||||||
	case "closed":
 | 
						case "closed":
 | 
				
			||||||
| 
						 | 
					@ -119,7 +153,6 @@ func SearchIssues(ctx *context.APIContext) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var issueIDs []int64
 | 
						var issueIDs []int64
 | 
				
			||||||
	var labelIDs []int64
 | 
						var labelIDs []int64
 | 
				
			||||||
	var err error
 | 
					 | 
				
			||||||
	if len(keyword) > 0 && len(repoIDs) > 0 {
 | 
						if len(keyword) > 0 && len(repoIDs) > 0 {
 | 
				
			||||||
		if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil {
 | 
							if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
 | 
								ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
 | 
				
			||||||
| 
						 | 
					@ -143,13 +176,22 @@ func SearchIssues(ctx *context.APIContext) {
 | 
				
			||||||
		includedLabelNames = strings.Split(labels, ",")
 | 
							includedLabelNames = strings.Split(labels, ",")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// this api is also used in UI,
 | 
				
			||||||
 | 
						// so the default limit is set to fit UI needs
 | 
				
			||||||
 | 
						limit := ctx.QueryInt("limit")
 | 
				
			||||||
 | 
						if limit == 0 {
 | 
				
			||||||
 | 
							limit = setting.UI.IssuePagingNum
 | 
				
			||||||
 | 
						} else if limit > setting.API.MaxResponseItems {
 | 
				
			||||||
 | 
							limit = setting.API.MaxResponseItems
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Only fetch the issues if we either don't have a keyword or the search returned issues
 | 
						// Only fetch the issues if we either don't have a keyword or the search returned issues
 | 
				
			||||||
	// This would otherwise return all issues if no issues were found by the search.
 | 
						// This would otherwise return all issues if no issues were found by the search.
 | 
				
			||||||
	if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
 | 
						if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
 | 
				
			||||||
		issuesOpt := &models.IssuesOptions{
 | 
							issuesOpt := &models.IssuesOptions{
 | 
				
			||||||
			ListOptions: models.ListOptions{
 | 
								ListOptions: models.ListOptions{
 | 
				
			||||||
				Page:     ctx.QueryInt("page"),
 | 
									Page:     ctx.QueryInt("page"),
 | 
				
			||||||
				PageSize: setting.UI.IssuePagingNum,
 | 
									PageSize: limit,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			RepoIDs:            repoIDs,
 | 
								RepoIDs:            repoIDs,
 | 
				
			||||||
			IsClosed:           isClosed,
 | 
								IsClosed:           isClosed,
 | 
				
			||||||
| 
						 | 
					@ -158,6 +200,19 @@ func SearchIssues(ctx *context.APIContext) {
 | 
				
			||||||
			SortType:           "priorityrepo",
 | 
								SortType:           "priorityrepo",
 | 
				
			||||||
			PriorityRepoID:     ctx.QueryInt64("priority_repo_id"),
 | 
								PriorityRepoID:     ctx.QueryInt64("priority_repo_id"),
 | 
				
			||||||
			IsPull:             isPull,
 | 
								IsPull:             isPull,
 | 
				
			||||||
 | 
								UpdatedBeforeUnix:  before,
 | 
				
			||||||
 | 
								UpdatedAfterUnix:   since,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Filter for: Created by User, Assigned to User, Mentioning User
 | 
				
			||||||
 | 
							if ctx.QueryBool("created") {
 | 
				
			||||||
 | 
								issuesOpt.PosterID = ctx.User.ID
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if ctx.QueryBool("assigned") {
 | 
				
			||||||
 | 
								issuesOpt.AssigneeID = ctx.User.ID
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if ctx.QueryBool("mentioned") {
 | 
				
			||||||
 | 
								issuesOpt.MentionedID = ctx.User.ID
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if issues, err = models.Issues(issuesOpt); err != nil {
 | 
							if issues, err = models.Issues(issuesOpt); err != nil {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1879,11 +1879,49 @@
 | 
				
			||||||
            "name": "type",
 | 
					            "name": "type",
 | 
				
			||||||
            "in": "query"
 | 
					            "in": "query"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "format": "date-time",
 | 
				
			||||||
 | 
					            "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format",
 | 
				
			||||||
 | 
					            "name": "since",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "format": "date-time",
 | 
				
			||||||
 | 
					            "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format",
 | 
				
			||||||
 | 
					            "name": "before",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "boolean",
 | 
				
			||||||
 | 
					            "description": "filter (issues / pulls) assigned to you, default is false",
 | 
				
			||||||
 | 
					            "name": "assigned",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "boolean",
 | 
				
			||||||
 | 
					            "description": "filter (issues / pulls) created by you, default is false",
 | 
				
			||||||
 | 
					            "name": "created",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "boolean",
 | 
				
			||||||
 | 
					            "description": "filter (issues / pulls) mentioning you, default is false",
 | 
				
			||||||
 | 
					            "name": "mentioned",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "type": "integer",
 | 
					            "type": "integer",
 | 
				
			||||||
            "description": "page number of requested issues",
 | 
					            "description": "page number of results to return (1-based)",
 | 
				
			||||||
            "name": "page",
 | 
					            "name": "page",
 | 
				
			||||||
            "in": "query"
 | 
					            "in": "query"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "type": "integer",
 | 
				
			||||||
 | 
					            "description": "page size of results",
 | 
				
			||||||
 | 
					            "name": "limit",
 | 
				
			||||||
 | 
					            "in": "query"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "responses": {
 | 
					        "responses": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue