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 <adelowomailbox@gmail.com>

* Apply suggestions from code review

Co-authored-by: Lanre Adelowo <adelowomailbox@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
---
 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)",