Fix assigned issues dashboard (#920)

* Fix assigned/created issues in dashboard. (#3560)

* Fix assigned/created issues in dashboard.

* Use GetUserIssueStats for getting all Dashboard stats.

* Use gofmt to format the file properly.

* Replace &Issue{} with new(Issue).

* Check if user has access to given repository.

* Remove unnecessary filtering of issues.

* Return 404 error if invalid repository is given.

* Use correct number of issues in paginater.

* fix issues on dashboard
This commit is contained in:
Lunny Xiao 2017-02-14 22:15:18 +08:00 committed by GitHub
parent 3a91ac51a9
commit 7a9a5c8a69
4 changed files with 181 additions and 123 deletions

View file

@ -1184,7 +1184,7 @@ func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error {
// IssueStats represents issue statistic information. // IssueStats represents issue statistic information.
type IssueStats struct { type IssueStats struct {
OpenCount, ClosedCount int64 OpenCount, ClosedCount int64
AllCount int64 YourRepositoriesCount int64
AssignCount int64 AssignCount int64
CreateCount int64 CreateCount int64
MentionCount int64 MentionCount int64
@ -1210,6 +1210,7 @@ func parseCountResult(results []map[string][]byte) int64 {
// IssueStatsOptions contains parameters accepted by GetIssueStats. // IssueStatsOptions contains parameters accepted by GetIssueStats.
type IssueStatsOptions struct { type IssueStatsOptions struct {
FilterMode int
RepoID int64 RepoID int64
Labels string Labels string
MilestoneID int64 MilestoneID int64
@ -1265,19 +1266,41 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
} }
var err error var err error
switch opts.FilterMode {
case FilterModeAll, FilterModeAssign:
stats.OpenCount, err = countSession(opts). stats.OpenCount, err = countSession(opts).
And("is_closed = ?", false). And("is_closed = ?", false).
Count(&Issue{}) Count(new(Issue))
if err != nil {
return nil, err
}
stats.ClosedCount, err = countSession(opts). stats.ClosedCount, err = countSession(opts).
And("is_closed = ?", true). And("is_closed = ?", true).
Count(&Issue{}) Count(new(Issue))
if err != nil { case FilterModeCreate:
return nil, err 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))
} }
return stats, nil return stats, err
} }
// GetUserIssueStats returns issue statistic information for dashboard by given conditions. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
@ -1298,29 +1321,39 @@ func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPul
return sess return sess
} }
stats.AssignCount, _ = countSession(false, isPull, repoID, repoIDs). stats.AssignCount, _ = countSession(false, isPull, repoID, nil).
And("assignee_id = ?", uid). And("assignee_id = ?", uid).
Count(&Issue{}) Count(new(Issue))
stats.CreateCount, _ = countSession(false, isPull, repoID, repoIDs). stats.CreateCount, _ = countSession(false, isPull, repoID, nil).
And("poster_id = ?", uid). And("poster_id = ?", uid).
Count(&Issue{}) Count(new(Issue))
openCountSession := countSession(false, isPull, repoID, repoIDs) stats.YourRepositoriesCount, _ = countSession(false, isPull, repoID, repoIDs).
closedCountSession := countSession(true, isPull, repoID, repoIDs) Count(new(Issue))
switch filterMode { switch filterMode {
case FilterModeAll:
stats.OpenCount, _ = countSession(false, isPull, repoID, repoIDs).
Count(new(Issue))
stats.ClosedCount, _ = countSession(true, isPull, repoID, repoIDs).
Count(new(Issue))
case FilterModeAssign: case FilterModeAssign:
openCountSession.And("assignee_id = ?", uid) stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
closedCountSession.And("assignee_id = ?", uid) And("assignee_id = ?", uid).
Count(new(Issue))
stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
And("assignee_id = ?", uid).
Count(new(Issue))
case FilterModeCreate: case FilterModeCreate:
openCountSession.And("poster_id = ?", uid) stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
closedCountSession.And("poster_id = ?", uid) And("poster_id = ?", uid).
Count(new(Issue))
stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
And("poster_id = ?", uid).
Count(new(Issue))
} }
stats.OpenCount, _ = openCountSession.Count(&Issue{})
stats.ClosedCount, _ = closedCountSession.Count(&Issue{})
return stats return stats
} }
@ -1347,8 +1380,8 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
closedCountSession.And("poster_id = ?", uid) closedCountSession.And("poster_id = ?", uid)
} }
openResult, _ := openCountSession.Count(&Issue{}) openResult, _ := openCountSession.Count(new(Issue))
closedResult, _ := closedCountSession.Count(&Issue{}) closedResult, _ := closedCountSession.Count(new(Issue))
return openResult, closedResult return openResult, closedResult
} }

View file

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url"
"strings" "strings"
"time" "time"
@ -108,37 +107,17 @@ func Issues(ctx *context.Context) {
viewType := ctx.Query("type") viewType := ctx.Query("type")
sortType := ctx.Query("sort") sortType := ctx.Query("sort")
types := []string{"assigned", "created_by", "mentioned"} types := []string{"all", "assigned", "created_by", "mentioned"}
if !com.IsSliceContainsStr(types, viewType) { if !com.IsSliceContainsStr(types, viewType) {
viewType = "all" viewType = "all"
} }
// Must sign in to see issues about you.
if viewType != "all" && !ctx.IsSigned {
ctx.SetCookie("redirect_to", "/"+url.QueryEscape(setting.AppSubURL+ctx.Req.RequestURI), 0, setting.AppSubURL)
ctx.Redirect(setting.AppSubURL + "/user/login")
return
}
var ( var (
assigneeID = ctx.QueryInt64("assignee") assigneeID = ctx.QueryInt64("assignee")
posterID int64 posterID int64
mentionedID int64 mentionedID int64
forceEmpty bool forceEmpty bool
) )
switch viewType {
case "assigned":
if assigneeID > 0 && ctx.User.ID != assigneeID {
// two different assignees, must be empty
forceEmpty = true
} else {
assigneeID = ctx.User.ID
}
case "created_by":
posterID = ctx.User.ID
case "mentioned":
mentionedID = ctx.User.ID
}
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
selectLabels := ctx.Query("labels") selectLabels := ctx.Query("labels")

View file

@ -183,34 +183,39 @@ func Issues(ctx *context.Context) {
viewType string viewType string
sortType = ctx.Query("sort") sortType = ctx.Query("sort")
filterMode = models.FilterModeAll filterMode = models.FilterModeAll
assigneeID int64
posterID int64
) )
if ctxUser.IsOrganization() { if ctxUser.IsOrganization() {
viewType = "all" viewType = "all"
} else { } else {
viewType = ctx.Query("type") viewType = ctx.Query("type")
types := []string{"assigned", "created_by"} types := []string{"all", "assigned", "created_by"}
if !com.IsSliceContainsStr(types, viewType) { if !com.IsSliceContainsStr(types, viewType) {
viewType = "all" viewType = "all"
} }
switch viewType { switch viewType {
case "all":
filterMode = models.FilterModeAll
case "assigned": case "assigned":
filterMode = models.FilterModeAssign filterMode = models.FilterModeAssign
assigneeID = ctxUser.ID
case "created_by": case "created_by":
filterMode = models.FilterModeCreate filterMode = models.FilterModeCreate
posterID = ctxUser.ID
} }
} }
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
repoID := ctx.QueryInt64("repo") repoID := ctx.QueryInt64("repo")
isShowClosed := ctx.Query("state") == "closed" isShowClosed := ctx.Query("state") == "closed"
// Get repositories. // Get repositories.
var err error var err error
var repos []*models.Repository var repos []*models.Repository
userRepoIDs := make([]int64, 0, len(repos))
if ctxUser.IsOrganization() { if ctxUser.IsOrganization() {
env, err := ctxUser.AccessibleReposEnv(ctx.User.ID) env, err := ctxUser.AccessibleReposEnv(ctx.User.ID)
if err != nil { if err != nil {
@ -230,9 +235,6 @@ func Issues(ctx *context.Context) {
repos = ctxUser.Repos repos = ctxUser.Repos
} }
allCount := 0
repoIDs := make([]int64, 0, len(repos))
showRepos := make([]*models.Repository, 0, len(repos))
for _, repo := range repos { for _, repo := range repos {
if (isPullList && repo.NumPulls == 0) || if (isPullList && repo.NumPulls == 0) ||
(!isPullList && (!isPullList &&
@ -240,85 +242,129 @@ func Issues(ctx *context.Context) {
continue continue
} }
repoIDs = append(repoIDs, repo.ID) userRepoIDs = append(userRepoIDs, repo.ID)
if isPullList {
allCount += repo.NumOpenPulls
repo.NumOpenIssues = repo.NumOpenPulls
repo.NumClosedIssues = repo.NumClosedPulls
} else {
allCount += repo.NumOpenIssues
} }
if filterMode != models.FilterModeAll { var issues []*models.Issue
// Calculate repository issue count with filter mode. switch filterMode {
numOpen, numClosed := repo.IssueStats(ctxUser.ID, filterMode, isPullList) case models.FilterModeAll:
repo.NumOpenIssues, repo.NumClosedIssues = int(numOpen), int(numClosed) // Get all issues from repositories from this user.
issues, err = models.Issues(&models.IssuesOptions{
RepoIDs: userRepoIDs,
RepoID: repoID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
})
case models.FilterModeAssign:
// Get all issues assigned to this user.
issues, err = models.Issues(&models.IssuesOptions{
RepoID: repoID,
AssigneeID: ctxUser.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
})
case models.FilterModeCreate:
// Get all issues created by this user.
issues, err = models.Issues(&models.IssuesOptions{
RepoID: repoID,
PosterID: ctxUser.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
})
case models.FilterModeMention:
// Get all issues created by this user.
issues, err = models.Issues(&models.IssuesOptions{
RepoID: repoID,
MentionedID: ctxUser.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
})
} }
if repo.ID == repoID || if err != nil {
(isShowClosed && repo.NumClosedIssues > 0) || ctx.Handle(500, "Issues", err)
(!isShowClosed && repo.NumOpenIssues > 0) { return
}
showRepos := make([]*models.Repository, 0, len(issues))
showReposSet := make(map[int64]bool)
if repoID > 0 {
repo, err := models.GetRepositoryByID(repoID)
if err != nil {
ctx.Handle(500, "GetRepositoryByID", fmt.Errorf("[#%d]%v", repoID, err))
return
}
if err = repo.GetOwner(); err != nil {
ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", repoID, err))
return
}
// Check if user has access to given repository.
if !repo.IsOwnedBy(ctxUser.ID) && !repo.HasAccess(ctxUser) {
ctx.Handle(404, "Issues", fmt.Errorf("#%d", repoID))
return
}
showReposSet[repoID] = true
showRepos = append(showRepos, repo) showRepos = append(showRepos, repo)
} }
}
ctx.Data["Repos"] = showRepos for _, issue := range issues {
if len(repoIDs) == 0 { // Get Repository data.
repoIDs = []int64{-1} issue.Repo, err = models.GetRepositoryByID(issue.RepoID)
if err != nil {
ctx.Handle(500, "GetRepositoryByID", fmt.Errorf("[#%d]%v", issue.RepoID, err))
return
} }
issueStats := models.GetUserIssueStats(repoID, ctxUser.ID, repoIDs, filterMode, isPullList) // Get Owner data.
issueStats.AllCount = int64(allCount) if err = issue.Repo.GetOwner(); err != nil {
ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", issue.RepoID, err))
page := ctx.QueryInt("page") return
if page <= 1 {
page = 1
} }
// Append repo to list of shown repos
if filterMode == models.FilterModeAll {
// Use a map to make sure we don't add the same Repository twice.
_, ok := showReposSet[issue.RepoID]
if !ok {
showReposSet[issue.RepoID] = true
// Append to list of shown Repositories.
showRepos = append(showRepos, issue.Repo)
}
}
}
issueStats := models.GetUserIssueStats(repoID, ctxUser.ID, userRepoIDs, filterMode, isPullList)
var total int var total int
if !isShowClosed { if !isShowClosed {
total = int(issueStats.OpenCount) total = int(issueStats.OpenCount)
} else { } else {
total = int(issueStats.ClosedCount) total = int(issueStats.ClosedCount)
} }
ctx.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5)
// Get issues.
issues, err := models.Issues(&models.IssuesOptions{
AssigneeID: assigneeID,
RepoID: repoID,
PosterID: posterID,
RepoIDs: repoIDs,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
})
if err != nil {
ctx.Handle(500, "Issues", err)
return
}
// Get posters and repository.
for i := range issues {
issues[i].Repo, err = models.GetRepositoryByID(issues[i].RepoID)
if err != nil {
ctx.Handle(500, "GetRepositoryByID", fmt.Errorf("[#%d]%v", issues[i].ID, err))
return
}
if err = issues[i].Repo.GetOwner(); err != nil {
ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", issues[i].ID, err))
return
}
}
ctx.Data["Issues"] = issues ctx.Data["Issues"] = issues
ctx.Data["Repos"] = showRepos
ctx.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5)
ctx.Data["IssueStats"] = issueStats ctx.Data["IssueStats"] = issueStats
ctx.Data["ViewType"] = viewType ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType ctx.Data["SortType"] = sortType
ctx.Data["RepoID"] = repoID ctx.Data["RepoID"] = repoID
ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["IsShowClosed"] = isShowClosed
if isShowClosed { if isShowClosed {
ctx.Data["State"] = "closed" ctx.Data["State"] = "closed"
} else { } else {

View file

@ -5,9 +5,9 @@
<div class="ui grid"> <div class="ui grid">
<div class="four wide column"> <div class="four wide column">
<div class="ui secondary vertical filter menu"> <div class="ui secondary vertical filter menu">
<a class="{{if eq .ViewType "all"}}ui basic blue button{{end}} item" href="{{.Link}}?repo={{.RepoID}}&sort={{$.SortType}}&state={{.State}}"> <a class="{{if eq .ViewType "your_repositories"}}ui basic blue button{{end}} item" href="{{.Link}}?type=your_repositories&repo={{.RepoID}}&sort={{$.SortType}}&state={{.State}}">
{{.i18n.Tr "home.issues.in_your_repos"}} {{.i18n.Tr "home.issues.in_your_repos"}}
<strong class="ui right">{{.IssueStats.AllCount}}</strong> <strong class="ui right">{{.IssueStats.YourRepositoriesCount}}</strong>
</a> </a>
{{if not .ContextUser.IsOrganization}} {{if not .ContextUser.IsOrganization}}
<a class="{{if eq .ViewType "assigned"}}ui basic blue button{{end}} item" href="{{.Link}}?type=assigned&repo={{.RepoID}}&sort={{$.SortType}}&state={{.State}}"> <a class="{{if eq .ViewType "assigned"}}ui basic blue button{{end}} item" href="{{.Link}}?type=assigned&repo={{.RepoID}}&sort={{$.SortType}}&state={{.State}}">
@ -22,7 +22,7 @@
<div class="ui divider"></div> <div class="ui divider"></div>
{{range .Repos}} {{range .Repos}}
<a class="{{if eq $.RepoID .ID}}ui basic blue button{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}{{if not (eq $.RepoID .ID)}}&repo={{.ID}}{{end}}&sort={{$.SortType}}&state={{$.State}}"> <a class="{{if eq $.RepoID .ID}}ui basic blue button{{end}} repo name item" href="{{$.Link}}?type={{$.ViewType}}{{if not (eq $.RepoID .ID)}}&repo={{.ID}}{{end}}&sort={{$.SortType}}&state={{$.State}}">
<span class="text truncate">{{$.ContextUser.Name}}/{{.Name}}</span> <span class="text truncate">{{.FullName}}</span>
<div class="floating ui {{if $.IsShowClosed}}red{{else}}green{{end}} label">{{if $.IsShowClosed}}{{.NumClosedIssues}}{{else}}{{.NumOpenIssues}}{{end}}</div> <div class="floating ui {{if $.IsShowClosed}}red{{else}}green{{end}} label">{{if $.IsShowClosed}}{{.NumClosedIssues}}{{else}}{{.NumOpenIssues}}{{end}}</div>
</a> </a>
{{end}} {{end}}
@ -61,7 +61,7 @@
{{range .Issues}} {{range .Issues}}
{{ $timeStr:= TimeSince .Created $.Lang }} {{ $timeStr:= TimeSince .Created $.Lang }}
<li class="item"> <li class="item">
<div class="ui label">{{if not $.RepoID}}{{.Repo.Name}}{{end}}#{{.Index}}</div> <div class="ui label">{{if not $.RepoID}}{{.Repo.FullName}}{{end}}#{{.Index}}</div>
<a class="title has-emoji" href="{{AppSubUrl}}/{{.Repo.Owner.Name}}/{{.Repo.Name}}/issues/{{.Index}}">{{.Title}}</a> <a class="title has-emoji" href="{{AppSubUrl}}/{{.Repo.Owner.Name}}/{{.Repo.Name}}/issues/{{.Index}}">{{.Title}}</a>
{{range .Labels}} {{range .Labels}}