From 8fcda0442e60209d7e6783c95d96f20ff04dbdd6 Mon Sep 17 00:00:00 2001
From: Ethan Koenig <etk39@cornell.edu>
Date: Wed, 14 Jun 2017 23:09:03 -0400
Subject: [PATCH] Fix search by issue type (#1914)

* Fix search by issue type
---
 integrations/issue_test.go     | 50 ++++++++++++++++++++++++++++++++
 models/issue.go                | 52 +++++++++-------------------------
 models/unit_tests.go           | 40 +++++++++++++++++++++-----
 routers/repo/issue.go          | 12 +++++++-
 templates/repo/issue/list.tmpl | 26 +++++++++--------
 5 files changed, 121 insertions(+), 59 deletions(-)

diff --git a/integrations/issue_test.go b/integrations/issue_test.go
index 3b486dc50..f94f11a86 100644
--- a/integrations/issue_test.go
+++ b/integrations/issue_test.go
@@ -6,11 +6,30 @@ package integrations
 
 import (
 	"net/http"
+	"strconv"
+	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/PuerkitoBio/goquery"
 	"github.com/stretchr/testify/assert"
 )
 
+func getIssuesSelection(htmlDoc *HtmlDoc) *goquery.Selection {
+	return htmlDoc.doc.Find(".issue.list").Find("li").Find(".title")
+}
+
+func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *models.Issue {
+	href, exists := issueSelection.Attr("href")
+	assert.True(t, exists)
+	indexStr := href[strings.LastIndexByte(href, '/')+1:]
+	index, err := strconv.Atoi(indexStr)
+	assert.NoError(t, err, "Invalid issue href: %s", href)
+	return models.AssertExistsAndLoadBean(t, &models.Issue{RepoID: repoID, Index: int64(index)}).(*models.Issue)
+}
+
 func TestNoLoginViewIssues(t *testing.T) {
 	prepareTestEnv(t)
 
@@ -19,6 +38,37 @@ func TestNoLoginViewIssues(t *testing.T) {
 	assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
 }
 
+func TestNoLoginViewIssuesSortByType(t *testing.T) {
+	prepareTestEnv(t)
+
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+	repo.Owner = models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
+
+	session := loginUser(t, user.Name, "password")
+	req := NewRequest(t, "GET", repo.RelLink()+"/issues?type=created_by")
+	resp := session.MakeRequest(t, req)
+	assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
+
+	htmlDoc, err := NewHtmlParser(resp.Body)
+	assert.NoError(t, err)
+	issuesSelection := getIssuesSelection(htmlDoc)
+	expectedNumIssues := models.GetCount(t,
+		&models.Issue{RepoID: repo.ID, PosterID: user.ID},
+		models.Cond("is_closed=?", false),
+		models.Cond("is_pull=?", false),
+	)
+	if expectedNumIssues > setting.UI.IssuePagingNum {
+		expectedNumIssues = setting.UI.IssuePagingNum
+	}
+	assert.EqualValues(t, expectedNumIssues, issuesSelection.Length())
+
+	issuesSelection.Each(func(_ int, selection *goquery.Selection) {
+		issue := getIssue(t, repo.ID, selection)
+		assert.EqualValues(t, user.ID, issue.PosterID)
+	})
+}
+
 func TestNoLoginViewIssue(t *testing.T) {
 	prepareTestEnv(t)
 
diff --git a/models/issue.go b/models/issue.go
index 8340595fc..d4d0a5a2f 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1223,7 +1223,6 @@ func parseCountResult(results []map[string][]byte) int64 {
 
 // IssueStatsOptions contains parameters accepted by GetIssueStats.
 type IssueStatsOptions struct {
-	FilterMode  int
 	RepoID      int64
 	Labels      string
 	MilestoneID int64
@@ -1241,7 +1240,7 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
 	countSession := func(opts *IssueStatsOptions) *xorm.Session {
 		sess := x.
 			Where("issue.repo_id = ?", opts.RepoID).
-			And("is_pull = ?", opts.IsPull)
+			And("issue.is_pull = ?", opts.IsPull)
 
 		if len(opts.IssueIDs) > 0 {
 			sess.In("issue.id", opts.IssueIDs)
@@ -1252,8 +1251,8 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
 			if err != nil {
 				log.Warn("Malformed Labels argument: %s", opts.Labels)
 			} else if len(labelIDs) > 0 {
-				sess.Join("INNER", "issue_label", "issue.id = issue_id").
-					In("label_id", labelIDs)
+				sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
+					In("issue_label.label_id", labelIDs)
 			}
 		}
 
@@ -1262,11 +1261,11 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
 		}
 
 		if opts.AssigneeID > 0 {
-			sess.And("assignee_id = ?", opts.AssigneeID)
+			sess.And("issue.assignee_id = ?", opts.AssigneeID)
 		}
 
 		if opts.PosterID > 0 {
-			sess.And("poster_id = ?", opts.PosterID)
+			sess.And("issue.poster_id = ?", opts.PosterID)
 		}
 
 		if opts.MentionedID > 0 {
@@ -1279,40 +1278,15 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
 	}
 
 	var err error
-	switch opts.FilterMode {
-	case FilterModeAll, FilterModeAssign:
-		stats.OpenCount, err = countSession(opts).
-			And("is_closed = ?", false).
-			Count(new(Issue))
-
-		stats.ClosedCount, err = countSession(opts).
-			And("is_closed = ?", true).
-			Count(new(Issue))
-	case FilterModeCreate:
-		stats.OpenCount, err = countSession(opts).
-			And("poster_id = ?", opts.PosterID).
-			And("is_closed = ?", false).
-			Count(new(Issue))
-
-		stats.ClosedCount, err = countSession(opts).
-			And("poster_id = ?", opts.PosterID).
-			And("is_closed = ?", true).
-			Count(new(Issue))
-	case FilterModeMention:
-		stats.OpenCount, err = countSession(opts).
-			Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
-			And("issue_user.uid = ?", opts.PosterID).
-			And("issue_user.is_mentioned = ?", true).
-			And("issue.is_closed = ?", false).
-			Count(new(Issue))
-
-		stats.ClosedCount, err = countSession(opts).
-			Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
-			And("issue_user.uid = ?", opts.PosterID).
-			And("issue_user.is_mentioned = ?", true).
-			And("issue.is_closed = ?", true).
-			Count(new(Issue))
+	stats.OpenCount, err = countSession(opts).
+		And("issue.is_closed = ?", false).
+		Count(new(Issue))
+	if err != nil {
+		return stats, err
 	}
+	stats.ClosedCount, err = countSession(opts).
+		And("issue.is_closed = ?", true).
+		Count(new(Issue))
 	return stats, err
 }
 
diff --git a/models/unit_tests.go b/models/unit_tests.go
index a3459e49f..16eb09968 100644
--- a/models/unit_tests.go
+++ b/models/unit_tests.go
@@ -37,13 +37,31 @@ func PrepareTestDatabase() error {
 	return LoadFixtures()
 }
 
+type testCond struct {
+	query interface{}
+	args  []interface{}
+}
+
+// Cond create a condition with arguments for a test
+func Cond(query interface{}, args ...interface{}) interface{} {
+	return &testCond{query: query, args: args}
+}
+
+func whereConditions(sess *xorm.Session, conditions []interface{}) {
+	for _, condition := range conditions {
+		switch cond := condition.(type) {
+		case *testCond:
+			sess.Where(cond.query, cond.args...)
+		default:
+			sess.Where(cond)
+		}
+	}
+}
+
 func loadBeanIfExists(bean interface{}, conditions ...interface{}) (bool, error) {
 	sess := x.NewSession()
 	defer sess.Close()
-
-	for _, cond := range conditions {
-		sess = sess.Where(cond)
-	}
+	whereConditions(sess, conditions)
 	return sess.Get(bean)
 }
 
@@ -65,6 +83,16 @@ func AssertExistsAndLoadBean(t *testing.T, bean interface{}, conditions ...inter
 	return bean
 }
 
+// GetCount get the count of a bean
+func GetCount(t *testing.T, bean interface{}, conditions ...interface{}) int {
+	sess := x.NewSession()
+	defer sess.Close()
+	whereConditions(sess, conditions)
+	count, err := sess.Count(bean)
+	assert.NoError(t, err)
+	return int(count)
+}
+
 // AssertNotExistsBean assert that a bean does not exist in the test database
 func AssertNotExistsBean(t *testing.T, bean interface{}, conditions ...interface{}) {
 	exists, err := loadBeanIfExists(bean, conditions...)
@@ -80,7 +108,5 @@ func AssertSuccessfulInsert(t *testing.T, beans ...interface{}) {
 
 // AssertCount assert the count of a bean
 func AssertCount(t *testing.T, bean interface{}, expected interface{}) {
-	actual, err := x.Count(bean)
-	assert.NoError(t, err)
-	assert.EqualValues(t, expected, actual)
+	assert.EqualValues(t, expected, GetCount(t, bean))
 }
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index c54cd2843..b94eddc4e 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -120,6 +120,15 @@ func Issues(ctx *context.Context) {
 		forceEmpty  bool
 	)
 
+	if ctx.IsSigned {
+		switch viewType {
+		case "created_by":
+			posterID = ctx.User.ID
+		case "mentioned":
+			mentionedID = ctx.User.ID
+		}
+	}
+
 	repo := ctx.Repo.Repository
 	selectLabels := ctx.Query("labels")
 	milestoneID := ctx.QueryInt64("milestone")
@@ -150,11 +159,12 @@ func Issues(ctx *context.Context) {
 			MilestoneID: milestoneID,
 			AssigneeID:  assigneeID,
 			MentionedID: mentionedID,
+			PosterID:    posterID,
 			IsPull:      isPullList,
 			IssueIDs:    issueIDs,
 		})
 		if err != nil {
-			ctx.Error(500, "GetSearchIssueStats")
+			ctx.Handle(500, "GetIssueStats", err)
 			return
 		}
 	}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 863ae6a74..8de52fb23 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -68,19 +68,21 @@
 					</div>
 				</div>
 
-				<!-- Type -->
-				<div class="ui dropdown type jump item">
-					<span class="text">
-						{{.i18n.Tr "repo.issues.filter_type"}}
-						<i class="dropdown icon"></i>
-					</span>
-					<div class="menu">
-						<a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a>
-						<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
-						<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a>
-						<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a>
+				{{if .IsSigned}}
+					<!-- Type -->
+					<div class="ui dropdown type jump item">
+						<span class="text">
+							{{.i18n.Tr "repo.issues.filter_type"}}
+							<i class="dropdown icon"></i>
+						</span>
+						<div class="menu">
+							<a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a>
+							<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{.SignedUser.ID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
+							<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a>
+							<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a>
+						</div>
 					</div>
-				</div>
+				{{end}}
 
 				<!-- Sort -->
 				<div class="ui dropdown type jump item">