Fix bugs in issue dashboard stats (#3073)
This commit is contained in:
parent
fabf3f2fc2
commit
4c9341f689
7 changed files with 224 additions and 55 deletions
|
@ -47,6 +47,8 @@
|
||||||
content: content for the fourth issue
|
content: content for the fourth issue
|
||||||
is_closed: true
|
is_closed: true
|
||||||
is_pull: false
|
is_pull: false
|
||||||
|
created_unix: 946684830
|
||||||
|
updated_unix: 978307200
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 5
|
id: 5
|
||||||
|
@ -57,6 +59,9 @@
|
||||||
content: content for the fifth issue
|
content: content for the fifth issue
|
||||||
is_closed: true
|
is_closed: true
|
||||||
is_pull: false
|
is_pull: false
|
||||||
|
created_unix: 946684840
|
||||||
|
updated_unix: 978307200
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 6
|
id: 6
|
||||||
repo_id: 3
|
repo_id: 3
|
||||||
|
@ -68,5 +73,5 @@
|
||||||
is_closed: false
|
is_closed: false
|
||||||
is_pull: false
|
is_pull: false
|
||||||
num_comments: 0
|
num_comments: 0
|
||||||
created_unix: 946684800
|
created_unix: 946684850
|
||||||
updated_unix: 978307200
|
updated_unix: 978307200
|
||||||
|
|
122
models/issue.go
122
models/issue.go
|
@ -10,14 +10,15 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
api "code.gitea.io/sdk/gitea"
|
|
||||||
"github.com/Unknwon/com"
|
|
||||||
"github.com/go-xorm/xorm"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/go-xorm/builder"
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Issue represents an issue or pull request of repository.
|
// Issue represents an issue or pull request of repository.
|
||||||
|
@ -1022,12 +1023,11 @@ func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
|
||||||
|
|
||||||
// IssuesOptions represents options of an issue.
|
// IssuesOptions represents options of an issue.
|
||||||
type IssuesOptions struct {
|
type IssuesOptions struct {
|
||||||
RepoID int64
|
RepoIDs []int64 // include all repos if empty
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
PosterID int64
|
PosterID int64
|
||||||
MentionedID int64
|
MentionedID int64
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
RepoIDs []int64
|
|
||||||
Page int
|
Page int
|
||||||
PageSize int
|
PageSize int
|
||||||
IsClosed util.OptionalBool
|
IsClosed util.OptionalBool
|
||||||
|
@ -1073,9 +1073,7 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error {
|
||||||
sess.In("issue.id", opts.IssueIDs)
|
sess.In("issue.id", opts.IssueIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.RepoID > 0 {
|
if len(opts.RepoIDs) > 0 {
|
||||||
sess.And("issue.repo_id=?", opts.RepoID)
|
|
||||||
} else if len(opts.RepoIDs) > 0 {
|
|
||||||
// In case repository IDs are provided but actually no repository has issue.
|
// In case repository IDs are provided but actually no repository has issue.
|
||||||
sess.In("issue.repo_id", opts.RepoIDs)
|
sess.In("issue.repo_id", opts.RepoIDs)
|
||||||
}
|
}
|
||||||
|
@ -1339,58 +1337,92 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
|
||||||
return stats, err
|
return stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
|
||||||
|
type UserIssueStatsOptions struct {
|
||||||
|
UserID int64
|
||||||
|
RepoID int64
|
||||||
|
UserRepoIDs []int64
|
||||||
|
FilterMode int
|
||||||
|
IsPull bool
|
||||||
|
IsClosed bool
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
|
// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
|
||||||
func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPull bool) *IssueStats {
|
func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
||||||
|
var err error
|
||||||
stats := &IssueStats{}
|
stats := &IssueStats{}
|
||||||
|
|
||||||
countSession := func(isClosed, isPull bool, repoID int64, repoIDs []int64) *xorm.Session {
|
cond := builder.NewCond()
|
||||||
sess := x.
|
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull})
|
||||||
Where("issue.is_closed = ?", isClosed).
|
if opts.RepoID > 0 {
|
||||||
And("issue.is_pull = ?", isPull)
|
cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
|
||||||
|
|
||||||
if repoID > 0 {
|
|
||||||
sess.And("repo_id = ?", repoID)
|
|
||||||
} else if len(repoIDs) > 0 {
|
|
||||||
sess.In("repo_id", repoIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.AssignCount, _ = countSession(false, isPull, repoID, nil).
|
switch opts.FilterMode {
|
||||||
And("assignee_id = ?", uid).
|
|
||||||
Count(new(Issue))
|
|
||||||
|
|
||||||
stats.CreateCount, _ = countSession(false, isPull, repoID, nil).
|
|
||||||
And("poster_id = ?", uid).
|
|
||||||
Count(new(Issue))
|
|
||||||
|
|
||||||
stats.YourRepositoriesCount, _ = countSession(false, isPull, repoID, repoIDs).
|
|
||||||
Count(new(Issue))
|
|
||||||
|
|
||||||
switch filterMode {
|
|
||||||
case FilterModeAll:
|
case FilterModeAll:
|
||||||
stats.OpenCount, _ = countSession(false, isPull, repoID, repoIDs).
|
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||||
|
And(builder.In("issue.repo_id", opts.UserRepoIDs)).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
stats.ClosedCount, _ = countSession(true, isPull, repoID, repoIDs).
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||||
|
And(builder.In("issue.repo_id", opts.UserRepoIDs)).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
case FilterModeAssign:
|
case FilterModeAssign:
|
||||||
stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
|
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||||
And("assignee_id = ?", uid).
|
And("assignee_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
|
if err != nil {
|
||||||
And("assignee_id = ?", uid).
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||||
|
And("assignee_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
case FilterModeCreate:
|
case FilterModeCreate:
|
||||||
stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
|
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||||
And("poster_id = ?", uid).
|
And("poster_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
|
if err != nil {
|
||||||
And("poster_id = ?", uid).
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||||
|
And("poster_id = ?", opts.UserID).
|
||||||
Count(new(Issue))
|
Count(new(Issue))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats
|
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
|
||||||
|
stats.AssignCount, err = x.Where(cond).
|
||||||
|
And("assignee_id = ?", opts.UserID).
|
||||||
|
Count(new(Issue))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.CreateCount, err = x.Where(cond).
|
||||||
|
And("poster_id = ?", opts.UserID).
|
||||||
|
Count(new(Issue))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.YourRepositoriesCount, err = x.Where(cond).
|
||||||
|
And(builder.In("issue.repo_id", opts.UserRepoIDs)).
|
||||||
|
Count(new(Issue))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
|
// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
|
||||||
|
|
|
@ -42,7 +42,7 @@ func populateIssueIndexer() error {
|
||||||
}
|
}
|
||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
issues, err := Issues(&IssuesOptions{
|
issues, err := Issues(&IssuesOptions{
|
||||||
RepoID: repo.ID,
|
RepoIDs: []int64{repo.ID},
|
||||||
IsClosed: util.OptionalBoolNone,
|
IsClosed: util.OptionalBoolNone,
|
||||||
IsPull: util.OptionalBoolNone,
|
IsPull: util.OptionalBoolNone,
|
||||||
})
|
})
|
||||||
|
|
|
@ -168,3 +168,114 @@ func TestUpdateIssueCols(t *testing.T) {
|
||||||
assert.EqualValues(t, prevContent, updatedIssue.Content)
|
assert.EqualValues(t, prevContent, updatedIssue.Content)
|
||||||
AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssues(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
for _, test := range []struct {
|
||||||
|
Opts IssuesOptions
|
||||||
|
ExpectedIssueIDs []int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
IssuesOptions{
|
||||||
|
AssigneeID: 1,
|
||||||
|
SortType: "oldest",
|
||||||
|
},
|
||||||
|
[]int64{1, 6},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IssuesOptions{
|
||||||
|
RepoIDs: []int64{1, 3},
|
||||||
|
SortType: "oldest",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 4,
|
||||||
|
},
|
||||||
|
[]int64{1, 2, 3, 5},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IssuesOptions{
|
||||||
|
Labels: "1,2",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 4,
|
||||||
|
},
|
||||||
|
[]int64{5, 2, 1},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
issues, err := Issues(&test.Opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, issues, len(test.ExpectedIssueIDs)) {
|
||||||
|
for i, issue := range issues {
|
||||||
|
assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserIssueStats(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
for _, test := range []struct {
|
||||||
|
Opts UserIssueStatsOptions
|
||||||
|
ExpectedIssueStats IssueStats
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
UserIssueStatsOptions{
|
||||||
|
UserID: 1,
|
||||||
|
RepoID: 1,
|
||||||
|
FilterMode: FilterModeAll,
|
||||||
|
},
|
||||||
|
IssueStats{
|
||||||
|
YourRepositoriesCount: 0,
|
||||||
|
AssignCount: 1,
|
||||||
|
CreateCount: 1,
|
||||||
|
OpenCount: 0,
|
||||||
|
ClosedCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserIssueStatsOptions{
|
||||||
|
UserID: 1,
|
||||||
|
FilterMode: FilterModeAssign,
|
||||||
|
},
|
||||||
|
IssueStats{
|
||||||
|
YourRepositoriesCount: 0,
|
||||||
|
AssignCount: 2,
|
||||||
|
CreateCount: 2,
|
||||||
|
OpenCount: 2,
|
||||||
|
ClosedCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserIssueStatsOptions{
|
||||||
|
UserID: 1,
|
||||||
|
FilterMode: FilterModeCreate,
|
||||||
|
},
|
||||||
|
IssueStats{
|
||||||
|
YourRepositoriesCount: 0,
|
||||||
|
AssignCount: 2,
|
||||||
|
CreateCount: 2,
|
||||||
|
OpenCount: 2,
|
||||||
|
ClosedCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserIssueStatsOptions{
|
||||||
|
UserID: 2,
|
||||||
|
UserRepoIDs: []int64{1, 2},
|
||||||
|
FilterMode: FilterModeAll,
|
||||||
|
IsClosed: true,
|
||||||
|
},
|
||||||
|
IssueStats{
|
||||||
|
YourRepositoriesCount: 2,
|
||||||
|
AssignCount: 0,
|
||||||
|
CreateCount: 2,
|
||||||
|
OpenCount: 1,
|
||||||
|
ClosedCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
stats, err := GetUserIssueStats(test.Opts)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.ExpectedIssueStats, *stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
issues, err := models.Issues(&models.IssuesOptions{
|
issues, err := models.Issues(&models.IssuesOptions{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
Page: ctx.QueryInt("page"),
|
Page: ctx.QueryInt("page"),
|
||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
|
|
|
@ -190,8 +190,8 @@ func Issues(ctx *context.Context) {
|
||||||
issues = []*models.Issue{}
|
issues = []*models.Issue{}
|
||||||
} else {
|
} else {
|
||||||
issues, err = models.Issues(&models.IssuesOptions{
|
issues, err = models.Issues(&models.IssuesOptions{
|
||||||
|
RepoIDs: []int64{repo.ID},
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
RepoID: repo.ID,
|
|
||||||
PosterID: posterID,
|
PosterID: posterID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
MilestoneID: milestoneID,
|
MilestoneID: milestoneID,
|
||||||
|
|
|
@ -9,13 +9,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/Unknwon/paginater"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/Unknwon/paginater"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -231,21 +232,30 @@ func Issues(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(userRepoIDs) <= 0 {
|
if len(userRepoIDs) <= 0 {
|
||||||
userRepoIDs = []int64{-1}
|
userRepoIDs = []int64{-1}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &models.IssuesOptions{
|
opts := &models.IssuesOptions{
|
||||||
RepoID: repoID,
|
|
||||||
IsClosed: util.OptionalBoolOf(isShowClosed),
|
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||||
IsPull: util.OptionalBoolOf(isPullList),
|
IsPull: util.OptionalBoolOf(isPullList),
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if repoID > 0 {
|
||||||
|
opts.RepoIDs = []int64{repoID}
|
||||||
|
}
|
||||||
|
|
||||||
switch filterMode {
|
switch filterMode {
|
||||||
case models.FilterModeAll:
|
case models.FilterModeAll:
|
||||||
opts.RepoIDs = userRepoIDs
|
if repoID > 0 {
|
||||||
|
if !com.IsSliceContainsInt64(userRepoIDs, repoID) {
|
||||||
|
// force an empty result
|
||||||
|
opts.RepoIDs = []int64{-1}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts.RepoIDs = userRepoIDs
|
||||||
|
}
|
||||||
case models.FilterModeAssign:
|
case models.FilterModeAssign:
|
||||||
opts.AssigneeID = ctxUser.ID
|
opts.AssigneeID = ctxUser.ID
|
||||||
case models.FilterModeCreate:
|
case models.FilterModeCreate:
|
||||||
|
@ -308,7 +318,18 @@ func Issues(ctx *context.Context) {
|
||||||
issue.Repo = showReposMap[issue.RepoID]
|
issue.Repo = showReposMap[issue.RepoID]
|
||||||
}
|
}
|
||||||
|
|
||||||
issueStats := models.GetUserIssueStats(repoID, ctxUser.ID, userRepoIDs, filterMode, isPullList)
|
issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{
|
||||||
|
UserID: ctxUser.ID,
|
||||||
|
RepoID: repoID,
|
||||||
|
UserRepoIDs: userRepoIDs,
|
||||||
|
FilterMode: filterMode,
|
||||||
|
IsPull: isPullList,
|
||||||
|
IsClosed: isShowClosed,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Handle(500, "GetUserIssueStats", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var total int
|
var total int
|
||||||
if !isShowClosed {
|
if !isShowClosed {
|
||||||
|
|
Reference in a new issue