* [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>tags/v1.15.0-rc1
@@ -25,9 +25,10 @@ func TestAPIListIssues(t *testing.T) { | |||
session := loginUser(t, owner.Name) | |||
token := getTokenForLoggedInUser(t, session) | |||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&token=%s", | |||
owner.Name, repo.Name, token) | |||
resp := session.MakeRequest(t, req, http.StatusOK) | |||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name)) | |||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode() | |||
resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | |||
var apiIssues []*api.Issue | |||
DecodeJSON(t, resp, &apiIssues) | |||
assert.Len(t, apiIssues, models.GetCount(t, &models.Issue{RepoID: repo.ID})) | |||
@@ -36,15 +37,34 @@ func TestAPIListIssues(t *testing.T) { | |||
} | |||
// test milestone filter | |||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&type=all&milestones=ignore,milestone1,3,4&token=%s", | |||
owner.Name, repo.Name, token) | |||
resp = session.MakeRequest(t, req, http.StatusOK) | |||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode() | |||
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | |||
DecodeJSON(t, resp, &apiIssues) | |||
if assert.Len(t, apiIssues, 2) { | |||
assert.EqualValues(t, 3, apiIssues[0].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) { |
@@ -17,4 +17,4 @@ | |||
uid: 4 | |||
issue_id: 1 | |||
is_read: false | |||
is_mentioned: false | |||
is_mentioned: true |
@@ -266,6 +266,30 @@ func ListIssues(ctx *context.APIContext) { | |||
// 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 | |||
// 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 | |||
// in: query | |||
// description: page number of results to return (1-based) | |||
@@ -277,6 +301,11 @@ func ListIssues(ctx *context.APIContext) { | |||
// responses: | |||
// "200": | |||
// "$ref": "#/responses/IssueList" | |||
before, since, err := utils.GetQueryBeforeSince(ctx) | |||
if err != nil { | |||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | |||
return | |||
} | |||
var isClosed util.OptionalBool | |||
switch ctx.Query("state") { | |||
@@ -297,7 +326,6 @@ func ListIssues(ctx *context.APIContext) { | |||
} | |||
var issueIDs []int64 | |||
var labelIDs []int64 | |||
var err error | |||
if len(keyword) > 0 { | |||
issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword) | |||
if err != nil { | |||
@@ -356,17 +384,36 @@ func ListIssues(ctx *context.APIContext) { | |||
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 | |||
// This would otherwise return all issues if no issues were found by the search. | |||
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { | |||
issuesOpt := &models.IssuesOptions{ | |||
ListOptions: listOptions, | |||
RepoIDs: []int64{ctx.Repo.Repository.ID}, | |||
IsClosed: isClosed, | |||
IssueIDs: issueIDs, | |||
LabelIDs: labelIDs, | |||
MilestoneIDs: mileIDs, | |||
IsPull: isPull, | |||
ListOptions: listOptions, | |||
RepoIDs: []int64{ctx.Repo.Repository.ID}, | |||
IsClosed: isClosed, | |||
IssueIDs: issueIDs, | |||
LabelIDs: labelIDs, | |||
MilestoneIDs: mileIDs, | |||
IsPull: isPull, | |||
UpdatedBeforeUnix: before, | |||
UpdatedAfterUnix: since, | |||
PosterID: createdByID, | |||
AssigneeID: assignedByID, | |||
MentionedID: mentionedByID, | |||
} | |||
if issues, err = models.Issues(issuesOpt); err != nil { | |||
@@ -389,6 +436,26 @@ func ListIssues(ctx *context.APIContext) { | |||
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 | |||
func GetIssue(ctx *context.APIContext) { | |||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue |
@@ -4234,6 +4234,38 @@ | |||
"name": "milestones", | |||
"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", | |||
"description": "page number of results to return (1-based)", |