Skip to content

Commit 0512b02

Browse files
GiteaBotlunnydelvhwxiaoguang
authored
Fix project issues list and counting (#33594) (#33619)
Backport #33594 by lunny --------- Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: delvh <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 99545ae commit 0512b02

File tree

16 files changed

+459
-104
lines changed

16 files changed

+459
-104
lines changed

models/issues/issue_project.go

+21-19
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
4949
return ip.ProjectColumnID, nil
5050
}
5151

52+
func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) {
53+
issues := make([]project_model.ProjectIssue, 0)
54+
if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil {
55+
return nil, err
56+
}
57+
result := make(map[int64]int64, len(issues))
58+
for _, issue := range issues {
59+
if issue.ProjectColumnID == 0 {
60+
issue.ProjectColumnID = defaultColumnID
61+
}
62+
result[issue.IssueID] = issue.ProjectColumnID
63+
}
64+
return result, nil
65+
}
66+
5267
// LoadIssuesFromColumn load issues assigned to this column
5368
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
5469
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
@@ -61,11 +76,11 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
6176
}
6277

6378
if b.Default {
64-
issues, err := Issues(ctx, &IssuesOptions{
65-
ProjectColumnID: db.NoConditionID,
66-
ProjectID: b.ProjectID,
67-
SortType: "project-column-sorting",
68-
})
79+
issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
80+
o.ProjectColumnID = db.NoConditionID
81+
o.ProjectID = b.ProjectID
82+
o.SortType = "project-column-sorting"
83+
}))
6984
if err != nil {
7085
return nil, err
7186
}
@@ -79,19 +94,6 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
7994
return issueList, nil
8095
}
8196

82-
// LoadIssuesFromColumnList load issues assigned to the columns
83-
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) {
84-
issuesMap := make(map[int64]IssueList, len(bs))
85-
for i := range bs {
86-
il, err := LoadIssuesFromColumn(ctx, bs[i], opts)
87-
if err != nil {
88-
return nil, err
89-
}
90-
issuesMap[bs[i].ID] = il
91-
}
92-
return issuesMap, nil
93-
}
94-
9597
// IssueAssignOrRemoveProject changes the project associated with an issue
9698
// If newProjectID is 0, the issue is removed from the project
9799
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
@@ -112,7 +114,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
112114
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
113115
}
114116
if newColumnID == 0 {
115-
newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
117+
newDefaultColumn, err := newProject.MustDefaultColumn(ctx)
116118
if err != nil {
117119
return err
118120
}

models/issues/issue_search.go

+14-10
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ type IssuesOptions struct { //nolint
4949
// prioritize issues from this repo
5050
PriorityRepoID int64
5151
IsArchived optional.Option[bool]
52-
Org *organization.Organization // issues permission scope
53-
Team *organization.Team // issues permission scope
54-
User *user_model.User // issues permission scope
52+
Owner *user_model.User // issues permission scope, it could be an organization or a user
53+
Team *organization.Team // issues permission scope
54+
Doer *user_model.User // issues permission scope
5555
}
5656

5757
// Copy returns a copy of the options.
@@ -273,8 +273,12 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
273273

274274
applyLabelsCondition(sess, opts)
275275

276-
if opts.User != nil {
277-
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
276+
if opts.Owner != nil {
277+
sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
278+
}
279+
280+
if opts.Doer != nil && !opts.Doer.IsAdmin {
281+
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value()))
278282
}
279283
}
280284

@@ -321,20 +325,20 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
321325
}
322326

323327
// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table
324-
func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond {
328+
func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_model.User, team *organization.Team, isPull bool) builder.Cond {
325329
cond := builder.NewCond()
326330
unitType := unit.TypeIssues
327331
if isPull {
328332
unitType = unit.TypePullRequests
329333
}
330-
if org != nil {
334+
if owner != nil && owner.IsOrganization() {
331335
if team != nil {
332-
cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos
336+
cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
333337
} else {
334338
cond = cond.And(
335339
builder.Or(
336-
repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos
337-
repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues
340+
repo_model.UserOrgUnitRepoCond(repoIDstr, userID, owner.ID, unitType), // team member repos
341+
repo_model.UserOrgPublicUnitRepoCond(userID, owner.ID), // user org public non-member repos, TODO: check repo has issues
338342
),
339343
)
340344
}

models/project/column.go

+32-17
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ type Column struct {
4848
ProjectID int64 `xorm:"INDEX NOT NULL"`
4949
CreatorID int64 `xorm:"NOT NULL"`
5050

51+
NumIssues int64 `xorm:"-"`
52+
5153
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
5254
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
5355
}
@@ -57,20 +59,6 @@ func (Column) TableName() string {
5759
return "project_board" // TODO: the legacy table name should be project_column
5860
}
5961

60-
// NumIssues return counter of all issues assigned to the column
61-
func (c *Column) NumIssues(ctx context.Context) int {
62-
total, err := db.GetEngine(ctx).Table("project_issue").
63-
Where("project_id=?", c.ProjectID).
64-
And("project_board_id=?", c.ID).
65-
GroupBy("issue_id").
66-
Cols("issue_id").
67-
Count()
68-
if err != nil {
69-
return 0
70-
}
71-
return int(total)
72-
}
73-
7462
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
7563
issues := make([]*ProjectIssue, 0, 5)
7664
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
@@ -192,7 +180,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
192180
if err != nil {
193181
return err
194182
}
195-
defaultColumn, err := project.GetDefaultColumn(ctx)
183+
defaultColumn, err := project.MustDefaultColumn(ctx)
196184
if err != nil {
197185
return err
198186
}
@@ -257,8 +245,8 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
257245
return columns, nil
258246
}
259247

260-
// GetDefaultColumn return default column and ensure only one exists
261-
func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
248+
// getDefaultColumn return default column and ensure only one exists
249+
func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
262250
var column Column
263251
has, err := db.GetEngine(ctx).
264252
Where("project_id=? AND `default` = ?", p.ID, true).
@@ -270,6 +258,33 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
270258
if has {
271259
return &column, nil
272260
}
261+
return nil, ErrProjectColumnNotExist{ColumnID: 0}
262+
}
263+
264+
// MustDefaultColumn returns the default column for a project.
265+
// If one exists, it is returned
266+
// If none exists, the first column will be elevated to the default column of this project
267+
func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
268+
c, err := p.getDefaultColumn(ctx)
269+
if err != nil && !IsErrProjectColumnNotExist(err) {
270+
return nil, err
271+
}
272+
if c != nil {
273+
return c, nil
274+
}
275+
276+
var column Column
277+
has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
278+
if err != nil {
279+
return nil, err
280+
}
281+
if has {
282+
column.Default = true
283+
if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil {
284+
return nil, err
285+
}
286+
return &column, nil
287+
}
273288

274289
// create a default column if none is found
275290
column = Column{

models/project/column_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ func TestGetDefaultColumn(t *testing.T) {
2020
assert.NoError(t, err)
2121

2222
// check if default column was added
23-
column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
23+
column, err := projectWithoutDefault.MustDefaultColumn(db.DefaultContext)
2424
assert.NoError(t, err)
2525
assert.Equal(t, int64(5), column.ProjectID)
26-
assert.Equal(t, "Uncategorized", column.Title)
26+
assert.Equal(t, "Done", column.Title)
2727

2828
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
2929
assert.NoError(t, err)
3030

3131
// check if multiple defaults were removed
32-
column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
32+
column, err = projectWithMultipleDefaults.MustDefaultColumn(db.DefaultContext)
3333
assert.NoError(t, err)
3434
assert.Equal(t, int64(6), column.ProjectID)
35-
assert.Equal(t, int64(9), column.ID)
35+
assert.Equal(t, int64(9), column.ID) // there are 2 default columns in the test data, use the latest one
3636

3737
// set 8 as default column
3838
assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))

models/project/issue.go

-43
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99

1010
"code.gitea.io/gitea/models/db"
11-
"code.gitea.io/gitea/modules/log"
1211
"code.gitea.io/gitea/modules/util"
1312
)
1413

@@ -34,48 +33,6 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
3433
return err
3534
}
3635

37-
// NumIssues return counter of all issues assigned to a project
38-
func (p *Project) NumIssues(ctx context.Context) int {
39-
c, err := db.GetEngine(ctx).Table("project_issue").
40-
Where("project_id=?", p.ID).
41-
GroupBy("issue_id").
42-
Cols("issue_id").
43-
Count()
44-
if err != nil {
45-
log.Error("NumIssues: %v", err)
46-
return 0
47-
}
48-
return int(c)
49-
}
50-
51-
// NumClosedIssues return counter of closed issues assigned to a project
52-
func (p *Project) NumClosedIssues(ctx context.Context) int {
53-
c, err := db.GetEngine(ctx).Table("project_issue").
54-
Join("INNER", "issue", "project_issue.issue_id=issue.id").
55-
Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
56-
Cols("issue_id").
57-
Count()
58-
if err != nil {
59-
log.Error("NumClosedIssues: %v", err)
60-
return 0
61-
}
62-
return int(c)
63-
}
64-
65-
// NumOpenIssues return counter of open issues assigned to a project
66-
func (p *Project) NumOpenIssues(ctx context.Context) int {
67-
c, err := db.GetEngine(ctx).Table("project_issue").
68-
Join("INNER", "issue", "project_issue.issue_id=issue.id").
69-
Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
70-
Cols("issue_id").
71-
Count()
72-
if err != nil {
73-
log.Error("NumOpenIssues: %v", err)
74-
return 0
75-
}
76-
return int(c)
77-
}
78-
7936
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
8037
if c.ProjectID != newColumn.ProjectID {
8138
return fmt.Errorf("columns have to be in the same project")

models/project/project.go

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ type Project struct {
9797
Type Type
9898

9999
RenderedContent template.HTML `xorm:"-"`
100+
NumOpenIssues int64 `xorm:"-"`
101+
NumClosedIssues int64 `xorm:"-"`
102+
NumIssues int64 `xorm:"-"`
100103

101104
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
102105
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`

modules/indexer/issues/db/options.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
7373
UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
7474
PriorityRepoID: 0,
7575
IsArchived: options.IsArchived,
76-
Org: nil,
76+
Owner: nil,
7777
Team: nil,
78-
User: nil,
78+
Doer: nil,
7979
}
8080

8181
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {

routers/web/org/projects.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ func Projects(ctx *context.Context) {
7878
return
7979
}
8080

81+
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
82+
ctx.ServerError("LoadIssueNumbersForProjects", err)
83+
return
84+
}
85+
8186
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
8287
OwnerID: ctx.ContextUser.ID,
8388
IsClosed: optional.Some(!isShowClosed),
@@ -328,6 +333,10 @@ func ViewProject(ctx *context.Context) {
328333
ctx.NotFound("", nil)
329334
return
330335
}
336+
if err := project.LoadOwner(ctx); err != nil {
337+
ctx.ServerError("LoadOwner", err)
338+
return
339+
}
331340

332341
columns, err := project.GetColumns(ctx)
333342
if err != nil {
@@ -341,14 +350,21 @@ func ViewProject(ctx *context.Context) {
341350
}
342351
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
343352

344-
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
353+
opts := issues_model.IssuesOptions{
345354
LabelIDs: labelIDs,
346355
AssigneeID: optional.Some(assigneeID),
347-
})
356+
Owner: project.Owner,
357+
Doer: ctx.Doer,
358+
}
359+
360+
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
348361
if err != nil {
349362
ctx.ServerError("LoadIssuesOfColumns", err)
350363
return
351364
}
365+
for _, column := range columns {
366+
column.NumIssues = int64(len(issuesMap[column.ID]))
367+
}
352368

353369
if project.CardType != project_model.CardTypeTextOnly {
354370
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)

routers/web/repo/projects.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ func Projects(ctx *context.Context) {
9292
return
9393
}
9494

95+
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
96+
ctx.ServerError("LoadIssueNumbersForProjects", err)
97+
return
98+
}
99+
95100
for i := range projects {
96101
rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
97102
projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description)
@@ -312,14 +317,18 @@ func ViewProject(ctx *context.Context) {
312317

313318
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
314319

315-
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
320+
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
321+
RepoIDs: []int64{ctx.Repo.Repository.ID},
316322
LabelIDs: labelIDs,
317323
AssigneeID: optional.Some(assigneeID),
318324
})
319325
if err != nil {
320326
ctx.ServerError("LoadIssuesOfColumns", err)
321327
return
322328
}
329+
for _, column := range columns {
330+
column.NumIssues = int64(len(issuesMap[column.ID]))
331+
}
323332

324333
if project.CardType != project_model.CardTypeTextOnly {
325334
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)

0 commit comments

Comments
 (0)