* 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.21.12.1
| @@ -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}} | |||