From 6fa19a8458e993972be2355e0575a76823a75735 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=B2=E3=81=AE=E3=81=97=E3=81=B0=20/=20hinoshiba?=
 <s.k.noe@hinoshiba.com>
Date: Fri, 25 Sep 2020 08:30:40 +0900
Subject: [PATCH] Fixed count of filtered issues when api request. (#12275)

* Improved total count of issue when filtered.

* Fixed size of slice when  selected 1 repository.

* Improved function of error check.

* improved comment

* Added parameter of return header.

Co-authored-by: 6543 <6543@obermui.de>

* Updated corresponded to the current vendored of "xorm.io/xorm".

* Dedublicated it by store the Options Struct into a variable.

* format code

* Update routers/api/v1/repo/issue.go

Co-authored-by: 6543 <6543@obermui.de>

* Update routers/api/v1/repo/issue.go

Co-authored-by: 6543 <6543@obermui.de>

* Updated number of range.

Co-authored-by: 6543 <6543@obermui.de>

* Updated number of range.

Co-authored-by: 6543 <6543@obermui.de>

* Removed total value.

* make fmt

* Improved value of sql.

Co-authored-by: zeripath <art27@cantab.net>

* Improved value of sql.

* improved message

* improved message

* improved message

* fixed message

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
---
 models/issue.go              | 23 ++++++++++-
 routers/api/v1/repo/issue.go | 74 +++++++++++++++++++++---------------
 2 files changed, 66 insertions(+), 31 deletions(-)

diff --git a/models/issue.go b/models/issue.go
index ede318095..82bd21455 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1266,7 +1266,7 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
 	opts.setupSession(sess)
 	sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID)
 
-	issues := make([]*Issue, 0, setting.UI.IssuePagingNum)
+	issues := make([]*Issue, 0, opts.ListOptions.PageSize)
 	if err := sess.Find(&issues); err != nil {
 		return nil, fmt.Errorf("Find: %v", err)
 	}
@@ -1279,6 +1279,27 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
 	return issues, nil
 }
 
+// CountIssues number return of issues by given conditions.
+func CountIssues(opts *IssuesOptions) (int64, error) {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	countsSlice := make([]*struct {
+		RepoID int64
+		Count  int64
+	}, 0, 1)
+
+	sess.Select("COUNT(issue.id) AS count").Table("issue")
+	opts.setupSession(sess)
+	if err := sess.Find(&countsSlice); err != nil {
+		return 0, fmt.Errorf("Find: %v", err)
+	}
+	if len(countsSlice) < 1 {
+		return 0, fmt.Errorf("there is less than one result sql record")
+	}
+	return countsSlice[0].Count, nil
+}
+
 // GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
 // but skips joining with `user` for performance reasons.
 // User permissions must be verified elsewhere if required.
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 1653b8594..0dbf2741a 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -93,7 +93,6 @@ func SearchIssues(ctx *context.APIContext) {
 		opts.AllLimited = true
 	}
 
-	issueCount := 0
 	for page := 1; ; page++ {
 		opts.Page = page
 		repos, count, err := models.SearchRepositoryByName(opts)
@@ -107,19 +106,12 @@ func SearchIssues(ctx *context.APIContext) {
 		}
 		log.Trace("Processing next %d repos of %d", len(repos), count)
 		for _, repo := range repos {
-			switch isClosed {
-			case util.OptionalBoolTrue:
-				issueCount += repo.NumClosedIssues
-			case util.OptionalBoolFalse:
-				issueCount += repo.NumOpenIssues
-			case util.OptionalBoolNone:
-				issueCount += repo.NumIssues
-			}
 			repoIDs = append(repoIDs, repo.ID)
 		}
 	}
 
 	var issues []*models.Issue
+	var filteredCount int64
 
 	keyword := strings.Trim(ctx.Query("q"), " ")
 	if strings.IndexByte(keyword, 0) >= 0 {
@@ -129,7 +121,10 @@ func SearchIssues(ctx *context.APIContext) {
 	var labelIDs []int64
 	var err error
 	if len(keyword) > 0 && len(repoIDs) > 0 {
-		issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword)
+		if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil {
+			ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
+			return
+		}
 	}
 
 	var isPull util.OptionalBool
@@ -151,12 +146,11 @@ func SearchIssues(ctx *context.APIContext) {
 	// 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 {
-		issues, err = models.Issues(&models.IssuesOptions{
+		issuesOpt := &models.IssuesOptions{
 			ListOptions: models.ListOptions{
 				Page:     ctx.QueryInt("page"),
 				PageSize: setting.UI.IssuePagingNum,
 			},
-
 			RepoIDs:            repoIDs,
 			IsClosed:           isClosed,
 			IssueIDs:           issueIDs,
@@ -164,16 +158,24 @@ func SearchIssues(ctx *context.APIContext) {
 			SortType:           "priorityrepo",
 			PriorityRepoID:     ctx.QueryInt64("priority_repo_id"),
 			IsPull:             isPull,
-		})
+		}
+
+		if issues, err = models.Issues(issuesOpt); err != nil {
+			ctx.Error(http.StatusInternalServerError, "Issues", err)
+			return
+		}
+
+		issuesOpt.ListOptions = models.ListOptions{
+			Page: -1,
+		}
+		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+			ctx.Error(http.StatusInternalServerError, "CountIssues", err)
+			return
+		}
 	}
 
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "Issues", err)
-		return
-	}
-
-	ctx.SetLinkHeader(issueCount, setting.UI.IssuePagingNum)
-	ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", issueCount))
+	ctx.SetLinkHeader(int(filteredCount), setting.UI.IssuePagingNum)
+	ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", filteredCount))
 	ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link")
 	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
 }
@@ -241,6 +243,7 @@ func ListIssues(ctx *context.APIContext) {
 	}
 
 	var issues []*models.Issue
+	var filteredCount int64
 
 	keyword := strings.Trim(ctx.Query("q"), " ")
 	if strings.IndexByte(keyword, 0) >= 0 {
@@ -251,6 +254,10 @@ func ListIssues(ctx *context.APIContext) {
 	var err error
 	if len(keyword) > 0 {
 		issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
+			return
+		}
 	}
 
 	if splitted := strings.Split(ctx.Query("labels"), ","); len(splitted) > 0 {
@@ -306,7 +313,7 @@ func ListIssues(ctx *context.APIContext) {
 	// 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 {
-		issues, err = models.Issues(&models.IssuesOptions{
+		issuesOpt := &models.IssuesOptions{
 			ListOptions:  listOptions,
 			RepoIDs:      []int64{ctx.Repo.Repository.ID},
 			IsClosed:     isClosed,
@@ -314,18 +321,25 @@ func ListIssues(ctx *context.APIContext) {
 			LabelIDs:     labelIDs,
 			MilestoneIDs: mileIDs,
 			IsPull:       isPull,
-		})
+		}
+
+		if issues, err = models.Issues(issuesOpt); err != nil {
+			ctx.Error(http.StatusInternalServerError, "Issues", err)
+			return
+		}
+
+		issuesOpt.ListOptions = models.ListOptions{
+			Page: -1,
+		}
+		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+			ctx.Error(http.StatusInternalServerError, "CountIssues", err)
+			return
+		}
 	}
 
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "Issues", err)
-		return
-	}
-
-	ctx.SetLinkHeader(ctx.Repo.Repository.NumIssues, listOptions.PageSize)
-	ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", ctx.Repo.Repository.NumIssues))
+	ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize)
+	ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", filteredCount))
 	ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link")
-
 	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
 }