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:
parent
3a91ac51a9
commit
7a9a5c8a69
4 changed files with 181 additions and 123 deletions
|
@ -1184,7 +1184,7 @@ func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error {
|
|||
// IssueStats represents issue statistic information.
|
||||
type IssueStats struct {
|
||||
OpenCount, ClosedCount int64
|
||||
AllCount int64
|
||||
YourRepositoriesCount int64
|
||||
AssignCount int64
|
||||
CreateCount int64
|
||||
MentionCount int64
|
||||
|
@ -1210,6 +1210,7 @@ func parseCountResult(results []map[string][]byte) int64 {
|
|||
|
||||
// IssueStatsOptions contains parameters accepted by GetIssueStats.
|
||||
type IssueStatsOptions struct {
|
||||
FilterMode int
|
||||
RepoID int64
|
||||
Labels string
|
||||
MilestoneID int64
|
||||
|
@ -1265,19 +1266,41 @@ 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(&Issue{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Count(new(Issue))
|
||||
|
||||
stats.ClosedCount, err = countSession(opts).
|
||||
And("is_closed = ?", true).
|
||||
Count(&Issue{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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))
|
||||
}
|
||||
return stats, nil
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
stats.AssignCount, _ = countSession(false, isPull, repoID, repoIDs).
|
||||
stats.AssignCount, _ = countSession(false, isPull, repoID, nil).
|
||||
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).
|
||||
Count(&Issue{})
|
||||
Count(new(Issue))
|
||||
|
||||
openCountSession := countSession(false, isPull, repoID, repoIDs)
|
||||
closedCountSession := countSession(true, isPull, repoID, repoIDs)
|
||||
stats.YourRepositoriesCount, _ = countSession(false, isPull, repoID, repoIDs).
|
||||
Count(new(Issue))
|
||||
|
||||
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:
|
||||
openCountSession.And("assignee_id = ?", uid)
|
||||
closedCountSession.And("assignee_id = ?", uid)
|
||||
stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
|
||||
And("assignee_id = ?", uid).
|
||||
Count(new(Issue))
|
||||
stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
|
||||
And("assignee_id = ?", uid).
|
||||
Count(new(Issue))
|
||||
case FilterModeCreate:
|
||||
openCountSession.And("poster_id = ?", uid)
|
||||
closedCountSession.And("poster_id = ?", uid)
|
||||
stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -1347,8 +1380,8 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
|
|||
closedCountSession.And("poster_id = ?", uid)
|
||||
}
|
||||
|
||||
openResult, _ := openCountSession.Count(&Issue{})
|
||||
closedResult, _ := closedCountSession.Count(&Issue{})
|
||||
openResult, _ := openCountSession.Count(new(Issue))
|
||||
closedResult, _ := closedCountSession.Count(new(Issue))
|
||||
|
||||
return openResult, closedResult
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -108,37 +107,17 @@ func Issues(ctx *context.Context) {
|
|||
|
||||
viewType := ctx.Query("type")
|
||||
sortType := ctx.Query("sort")
|
||||
types := []string{"assigned", "created_by", "mentioned"}
|
||||
types := []string{"all", "assigned", "created_by", "mentioned"}
|
||||
if !com.IsSliceContainsStr(types, viewType) {
|
||||
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 (
|
||||
assigneeID = ctx.QueryInt64("assignee")
|
||||
posterID int64
|
||||
mentionedID int64
|
||||
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
|
||||
selectLabels := ctx.Query("labels")
|
||||
|
|
|
@ -183,34 +183,39 @@ func Issues(ctx *context.Context) {
|
|||
viewType string
|
||||
sortType = ctx.Query("sort")
|
||||
filterMode = models.FilterModeAll
|
||||
assigneeID int64
|
||||
posterID int64
|
||||
)
|
||||
|
||||
if ctxUser.IsOrganization() {
|
||||
viewType = "all"
|
||||
} else {
|
||||
viewType = ctx.Query("type")
|
||||
types := []string{"assigned", "created_by"}
|
||||
types := []string{"all", "assigned", "created_by"}
|
||||
if !com.IsSliceContainsStr(types, viewType) {
|
||||
viewType = "all"
|
||||
}
|
||||
|
||||
switch viewType {
|
||||
case "all":
|
||||
filterMode = models.FilterModeAll
|
||||
case "assigned":
|
||||
filterMode = models.FilterModeAssign
|
||||
assigneeID = ctxUser.ID
|
||||
case "created_by":
|
||||
filterMode = models.FilterModeCreate
|
||||
posterID = ctxUser.ID
|
||||
}
|
||||
}
|
||||
|
||||
page := ctx.QueryInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
repoID := ctx.QueryInt64("repo")
|
||||
isShowClosed := ctx.Query("state") == "closed"
|
||||
|
||||
// Get repositories.
|
||||
var err error
|
||||
var repos []*models.Repository
|
||||
userRepoIDs := make([]int64, 0, len(repos))
|
||||
if ctxUser.IsOrganization() {
|
||||
env, err := ctxUser.AccessibleReposEnv(ctx.User.ID)
|
||||
if err != nil {
|
||||
|
@ -230,9 +235,6 @@ func Issues(ctx *context.Context) {
|
|||
repos = ctxUser.Repos
|
||||
}
|
||||
|
||||
allCount := 0
|
||||
repoIDs := make([]int64, 0, len(repos))
|
||||
showRepos := make([]*models.Repository, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
if (isPullList && repo.NumPulls == 0) ||
|
||||
(!isPullList &&
|
||||
|
@ -240,85 +242,129 @@ func Issues(ctx *context.Context) {
|
|||
continue
|
||||
}
|
||||
|
||||
repoIDs = append(repoIDs, repo.ID)
|
||||
|
||||
if isPullList {
|
||||
allCount += repo.NumOpenPulls
|
||||
repo.NumOpenIssues = repo.NumOpenPulls
|
||||
repo.NumClosedIssues = repo.NumClosedPulls
|
||||
} else {
|
||||
allCount += repo.NumOpenIssues
|
||||
userRepoIDs = append(userRepoIDs, repo.ID)
|
||||
}
|
||||
|
||||
if filterMode != models.FilterModeAll {
|
||||
// Calculate repository issue count with filter mode.
|
||||
numOpen, numClosed := repo.IssueStats(ctxUser.ID, filterMode, isPullList)
|
||||
repo.NumOpenIssues, repo.NumClosedIssues = int(numOpen), int(numClosed)
|
||||
var issues []*models.Issue
|
||||
switch filterMode {
|
||||
case models.FilterModeAll:
|
||||
// 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 ||
|
||||
(isShowClosed && repo.NumClosedIssues > 0) ||
|
||||
(!isShowClosed && repo.NumOpenIssues > 0) {
|
||||
if err != nil {
|
||||
ctx.Handle(500, "Issues", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
ctx.Data["Repos"] = showRepos
|
||||
if len(repoIDs) == 0 {
|
||||
repoIDs = []int64{-1}
|
||||
|
||||
for _, issue := range issues {
|
||||
// Get Repository data.
|
||||
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)
|
||||
issueStats.AllCount = int64(allCount)
|
||||
|
||||
page := ctx.QueryInt("page")
|
||||
if page <= 1 {
|
||||
page = 1
|
||||
// Get Owner data.
|
||||
if err = issue.Repo.GetOwner(); err != nil {
|
||||
ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", issue.RepoID, err))
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
if !isShowClosed {
|
||||
total = int(issueStats.OpenCount)
|
||||
} else {
|
||||
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["Repos"] = showRepos
|
||||
ctx.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5)
|
||||
ctx.Data["IssueStats"] = issueStats
|
||||
ctx.Data["ViewType"] = viewType
|
||||
ctx.Data["SortType"] = sortType
|
||||
ctx.Data["RepoID"] = repoID
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
|
||||
if isShowClosed {
|
||||
ctx.Data["State"] = "closed"
|
||||
} else {
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<div class="ui grid">
|
||||
<div class="four wide column">
|
||||
<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"}}
|
||||
<strong class="ui right">{{.IssueStats.AllCount}}</strong>
|
||||
<strong class="ui right">{{.IssueStats.YourRepositoriesCount}}</strong>
|
||||
</a>
|
||||
{{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}}">
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div class="ui divider"></div>
|
||||
{{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}}">
|
||||
<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>
|
||||
</a>
|
||||
{{end}}
|
||||
|
@ -61,7 +61,7 @@
|
|||
{{range .Issues}}
|
||||
{{ $timeStr:= TimeSince .Created $.Lang }}
|
||||
<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>
|
||||
|
||||
{{range .Labels}}
|
||||
|
|
Reference in a new issue