* 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 dashboardtags/v1.2.0-rc1
@@ -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 | |||
stats.OpenCount, err = countSession(opts). | |||
And("is_closed = ?", false). | |||
Count(&Issue{}) | |||
if err != nil { | |||
return nil, err | |||
} | |||
stats.ClosedCount, err = countSession(opts). | |||
And("is_closed = ?", true). | |||
Count(&Issue{}) | |||
if err != nil { | |||
return nil, err | |||
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)) | |||
} | |||
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) | |||
userRepoIDs = append(userRepoIDs, repo.ID) | |||
} | |||
if isPullList { | |||
allCount += repo.NumOpenPulls | |||
repo.NumOpenIssues = repo.NumOpenPulls | |||
repo.NumClosedIssues = repo.NumClosedPulls | |||
} else { | |||
allCount += repo.NumOpenIssues | |||
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 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 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) | |||
if err = repo.GetOwner(); err != nil { | |||
ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", repoID, err)) | |||
return | |||
} | |||
if repo.ID == repoID || | |||
(isShowClosed && repo.NumClosedIssues > 0) || | |||
(!isShowClosed && repo.NumOpenIssues > 0) { | |||
showRepos = append(showRepos, repo) | |||
// 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} | |||
} | |||
issueStats := models.GetUserIssueStats(repoID, ctxUser.ID, repoIDs, filterMode, isPullList) | |||
issueStats.AllCount = int64(allCount) | |||
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 | |||
} | |||
// Get Owner data. | |||
if err = issue.Repo.GetOwner(); err != nil { | |||
ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", issue.RepoID, err)) | |||
return | |||
} | |||
page := ctx.QueryInt("page") | |||
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 | |||
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}} | |||