[API] ListIssues add more filters (#16174)
* [API] ListIssues add more filters: optional filter repo issues by: - since - before - created_by - assigned_by - mentioned_by * Add Tests * Update routers/api/v1/repo/issue.go Co-authored-by: Lanre Adelowo <adelowomailbox@gmail.com> * Apply suggestions from code review Co-authored-by: Lanre Adelowo <adelowomailbox@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		
							parent
							
								
									ffbf35b7e9
								
							
						
					
					
						commit
						0e081ff0ce
					
				
					 4 changed files with 134 additions and 15 deletions
				
			
		|  | @ -25,9 +25,10 @@ func TestAPIListIssues(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	session := loginUser(t, owner.Name) | 	session := loginUser(t, owner.Name) | ||||||
| 	token := getTokenForLoggedInUser(t, session) | 	token := getTokenForLoggedInUser(t, session) | ||||||
| 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&token=%s", | 	link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name)) | ||||||
| 		owner.Name, repo.Name, token) | 
 | ||||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | 	link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode() | ||||||
|  | 	resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||||
| 	var apiIssues []*api.Issue | 	var apiIssues []*api.Issue | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
| 	assert.Len(t, apiIssues, models.GetCount(t, &models.Issue{RepoID: repo.ID})) | 	assert.Len(t, apiIssues, models.GetCount(t, &models.Issue{RepoID: repo.ID})) | ||||||
|  | @ -36,15 +37,34 @@ func TestAPIListIssues(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// test milestone filter
 | 	// test milestone filter
 | ||||||
| 	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&type=all&milestones=ignore,milestone1,3,4&token=%s", | 	link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode() | ||||||
| 		owner.Name, repo.Name, token) | 	resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) |  | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
| 	if assert.Len(t, apiIssues, 2) { | 	if assert.Len(t, apiIssues, 2) { | ||||||
| 		assert.EqualValues(t, 3, apiIssues[0].Milestone.ID) | 		assert.EqualValues(t, 3, apiIssues[0].Milestone.ID) | ||||||
| 		assert.EqualValues(t, 1, apiIssues[1].Milestone.ID) | 		assert.EqualValues(t, 1, apiIssues[1].Milestone.ID) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "created_by": {"user2"}}.Encode() | ||||||
|  | 	resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||||
|  | 	DecodeJSON(t, resp, &apiIssues) | ||||||
|  | 	if assert.Len(t, apiIssues, 1) { | ||||||
|  | 		assert.EqualValues(t, 5, apiIssues[0].ID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "assigned_by": {"user1"}}.Encode() | ||||||
|  | 	resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||||
|  | 	DecodeJSON(t, resp, &apiIssues) | ||||||
|  | 	if assert.Len(t, apiIssues, 1) { | ||||||
|  | 		assert.EqualValues(t, 1, apiIssues[0].ID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "mentioned_by": {"user4"}}.Encode() | ||||||
|  | 	resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||||
|  | 	DecodeJSON(t, resp, &apiIssues) | ||||||
|  | 	if assert.Len(t, apiIssues, 1) { | ||||||
|  | 		assert.EqualValues(t, 1, apiIssues[0].ID) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestAPICreateIssue(t *testing.T) { | func TestAPICreateIssue(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -17,4 +17,4 @@ | ||||||
|   uid: 4 |   uid: 4 | ||||||
|   issue_id: 1 |   issue_id: 1 | ||||||
|   is_read: false |   is_read: false | ||||||
|   is_mentioned: false |   is_mentioned: true | ||||||
|  |  | ||||||
|  | @ -266,6 +266,30 @@ func ListIssues(ctx *context.APIContext) { | ||||||
| 	//   in: query
 | 	//   in: query
 | ||||||
| 	//   description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
 | 	//   description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
 | ||||||
| 	//   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: created_by
 | ||||||
|  | 	//   in: query
 | ||||||
|  | 	//   description: filter (issues / pulls) created to
 | ||||||
|  | 	//   type: string
 | ||||||
|  | 	// - name: assigned_by
 | ||||||
|  | 	//   in: query
 | ||||||
|  | 	//   description: filter (issues / pulls) assigned to
 | ||||||
|  | 	//   type: string
 | ||||||
|  | 	// - name: mentioned_by
 | ||||||
|  | 	//   in: query
 | ||||||
|  | 	//   description: filter (issues / pulls) mentioning to
 | ||||||
|  | 	//   type: string
 | ||||||
| 	// - name: page
 | 	// - name: page
 | ||||||
| 	//   in: query
 | 	//   in: query
 | ||||||
| 	//   description: page number of results to return (1-based)
 | 	//   description: page number of results to return (1-based)
 | ||||||
|  | @ -277,6 +301,11 @@ func ListIssues(ctx *context.APIContext) { | ||||||
| 	// 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") { | ||||||
|  | @ -297,7 +326,6 @@ func ListIssues(ctx *context.APIContext) { | ||||||
| 	} | 	} | ||||||
| 	var issueIDs []int64 | 	var issueIDs []int64 | ||||||
| 	var labelIDs []int64 | 	var labelIDs []int64 | ||||||
| 	var err error |  | ||||||
| 	if len(keyword) > 0 { | 	if len(keyword) > 0 { | ||||||
| 		issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword) | 		issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -356,17 +384,36 @@ func ListIssues(ctx *context.APIContext) { | ||||||
| 		isPull = util.OptionalBoolNone | 		isPull = util.OptionalBoolNone | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// FIXME: we should be more efficient here
 | ||||||
|  | 	createdByID := getUserIDForFilter(ctx, "created_by") | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	assignedByID := getUserIDForFilter(ctx, "assigned_by") | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	mentionedByID := getUserIDForFilter(ctx, "mentioned_by") | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// 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:  listOptions, | 			ListOptions:       listOptions, | ||||||
| 			RepoIDs:      []int64{ctx.Repo.Repository.ID}, | 			RepoIDs:           []int64{ctx.Repo.Repository.ID}, | ||||||
| 			IsClosed:     isClosed, | 			IsClosed:          isClosed, | ||||||
| 			IssueIDs:     issueIDs, | 			IssueIDs:          issueIDs, | ||||||
| 			LabelIDs:     labelIDs, | 			LabelIDs:          labelIDs, | ||||||
| 			MilestoneIDs: mileIDs, | 			MilestoneIDs:      mileIDs, | ||||||
| 			IsPull:       isPull, | 			IsPull:            isPull, | ||||||
|  | 			UpdatedBeforeUnix: before, | ||||||
|  | 			UpdatedAfterUnix:  since, | ||||||
|  | 			PosterID:          createdByID, | ||||||
|  | 			AssigneeID:        assignedByID, | ||||||
|  | 			MentionedID:       mentionedByID, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if issues, err = models.Issues(issuesOpt); err != nil { | 		if issues, err = models.Issues(issuesOpt); err != nil { | ||||||
|  | @ -389,6 +436,26 @@ func ListIssues(ctx *context.APIContext) { | ||||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) | 	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 { | ||||||
|  | 	userName := ctx.Query(queryName) | ||||||
|  | 	if len(userName) == 0 { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	user, err := models.GetUserByName(userName) | ||||||
|  | 	if models.IsErrUserNotExist(err) { | ||||||
|  | 		ctx.NotFound(err) | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.InternalServerError(err) | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return user.ID | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // GetIssue get an issue of a repository
 | // GetIssue get an issue of a repository
 | ||||||
| func GetIssue(ctx *context.APIContext) { | func GetIssue(ctx *context.APIContext) { | ||||||
| 	// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
 | 	// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
 | ||||||
|  |  | ||||||
|  | @ -4234,6 +4234,38 @@ | ||||||
|             "name": "milestones", |             "name": "milestones", | ||||||
|             "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": "string", | ||||||
|  |             "description": "filter (issues / pulls) created to", | ||||||
|  |             "name": "created_by", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "filter (issues / pulls) assigned to", | ||||||
|  |             "name": "assigned_by", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "string", | ||||||
|  |             "description": "filter (issues / pulls) mentioning to", | ||||||
|  |             "name": "mentioned_by", | ||||||
|  |             "in": "query" | ||||||
|  |           }, | ||||||
|           { |           { | ||||||
|             "type": "integer", |             "type": "integer", | ||||||
|             "description": "page number of results to return (1-based)", |             "description": "page number of results to return (1-based)", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue