[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
					
				| @ -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…
	
		Reference in New Issue
	
	Block a user