[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:
6543 2021-06-17 00:33:37 +02:00 committed by GitHub
parent ffbf35b7e9
commit 0e081ff0ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 134 additions and 15 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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)",