From 0e081ff0ce61227d5f34f1d7f8213d9f407f1f3d Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 17 Jun 2021 00:33:37 +0200 Subject: [PATCH] [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 * Apply suggestions from code review Co-authored-by: Lanre Adelowo Co-authored-by: techknowlogick --- integrations/api_issue_test.go | 32 ++++++++++--- models/fixtures/issue_user.yml | 2 +- routers/api/v1/repo/issue.go | 83 ++++++++++++++++++++++++++++++---- templates/swagger/v1_json.tmpl | 32 +++++++++++++ 4 files changed, 134 insertions(+), 15 deletions(-) diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go index 109135b634..604e6d6381 100644 --- a/integrations/api_issue_test.go +++ b/integrations/api_issue_test.go @@ -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) { diff --git a/models/fixtures/issue_user.yml b/models/fixtures/issue_user.yml index 8039b1e40f..64824316ea 100644 --- a/models/fixtures/issue_user.yml +++ b/models/fixtures/issue_user.yml @@ -17,4 +17,4 @@ uid: 4 issue_id: 1 is_read: false - is_mentioned: false + is_mentioned: true diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 6b46dc0fef..5932765ab8 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -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 diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8ad9ae5a43..8ea5edb6fc 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -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)",