mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Backport #33594 by lunny --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		@@ -49,6 +49,21 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
 | 
			
		||||
	return ip.ProjectColumnID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) {
 | 
			
		||||
	issues := make([]project_model.ProjectIssue, 0)
 | 
			
		||||
	if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	result := make(map[int64]int64, len(issues))
 | 
			
		||||
	for _, issue := range issues {
 | 
			
		||||
		if issue.ProjectColumnID == 0 {
 | 
			
		||||
			issue.ProjectColumnID = defaultColumnID
 | 
			
		||||
		}
 | 
			
		||||
		result[issue.IssueID] = issue.ProjectColumnID
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadIssuesFromColumn load issues assigned to this column
 | 
			
		||||
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
 | 
			
		||||
	issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
 | 
			
		||||
@@ -61,11 +76,11 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b.Default {
 | 
			
		||||
		issues, err := Issues(ctx, &IssuesOptions{
 | 
			
		||||
			ProjectColumnID: db.NoConditionID,
 | 
			
		||||
			ProjectID:       b.ProjectID,
 | 
			
		||||
			SortType:        "project-column-sorting",
 | 
			
		||||
		})
 | 
			
		||||
		issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
 | 
			
		||||
			o.ProjectColumnID = db.NoConditionID
 | 
			
		||||
			o.ProjectID = b.ProjectID
 | 
			
		||||
			o.SortType = "project-column-sorting"
 | 
			
		||||
		}))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -79,19 +94,6 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
 | 
			
		||||
	return issueList, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadIssuesFromColumnList load issues assigned to the columns
 | 
			
		||||
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) {
 | 
			
		||||
	issuesMap := make(map[int64]IssueList, len(bs))
 | 
			
		||||
	for i := range bs {
 | 
			
		||||
		il, err := LoadIssuesFromColumn(ctx, bs[i], opts)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		issuesMap[bs[i].ID] = il
 | 
			
		||||
	}
 | 
			
		||||
	return issuesMap, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IssueAssignOrRemoveProject changes the project associated with an issue
 | 
			
		||||
// If newProjectID is 0, the issue is removed from the project
 | 
			
		||||
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
 | 
			
		||||
				return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
 | 
			
		||||
			}
 | 
			
		||||
			if newColumnID == 0 {
 | 
			
		||||
				newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
 | 
			
		||||
				newDefaultColumn, err := newProject.MustDefaultColumn(ctx)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,9 +49,9 @@ type IssuesOptions struct { //nolint
 | 
			
		||||
	// prioritize issues from this repo
 | 
			
		||||
	PriorityRepoID int64
 | 
			
		||||
	IsArchived     optional.Option[bool]
 | 
			
		||||
	Org            *organization.Organization // issues permission scope
 | 
			
		||||
	Owner          *user_model.User   // issues permission scope, it could be an organization or a user
 | 
			
		||||
	Team           *organization.Team // issues permission scope
 | 
			
		||||
	User           *user_model.User           // issues permission scope
 | 
			
		||||
	Doer           *user_model.User   // issues permission scope
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Copy returns a copy of the options.
 | 
			
		||||
@@ -273,8 +273,12 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
 | 
			
		||||
 | 
			
		||||
	applyLabelsCondition(sess, opts)
 | 
			
		||||
 | 
			
		||||
	if opts.User != nil {
 | 
			
		||||
		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
 | 
			
		||||
	if opts.Owner != nil {
 | 
			
		||||
		sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.Doer != nil && !opts.Doer.IsAdmin {
 | 
			
		||||
		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value()))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -321,20 +325,20 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table
 | 
			
		||||
func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond {
 | 
			
		||||
func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_model.User, team *organization.Team, isPull bool) builder.Cond {
 | 
			
		||||
	cond := builder.NewCond()
 | 
			
		||||
	unitType := unit.TypeIssues
 | 
			
		||||
	if isPull {
 | 
			
		||||
		unitType = unit.TypePullRequests
 | 
			
		||||
	}
 | 
			
		||||
	if org != nil {
 | 
			
		||||
	if owner != nil && owner.IsOrganization() {
 | 
			
		||||
		if team != nil {
 | 
			
		||||
			cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos
 | 
			
		||||
			cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
 | 
			
		||||
		} else {
 | 
			
		||||
			cond = cond.And(
 | 
			
		||||
				builder.Or(
 | 
			
		||||
					repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos
 | 
			
		||||
					repo_model.UserOrgPublicUnitRepoCond(userID, org.ID),                // user org public non-member repos, TODO: check repo has issues
 | 
			
		||||
					repo_model.UserOrgUnitRepoCond(repoIDstr, userID, owner.ID, unitType), // team member repos
 | 
			
		||||
					repo_model.UserOrgPublicUnitRepoCond(userID, owner.ID),                // user org public non-member repos, TODO: check repo has issues
 | 
			
		||||
				),
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,8 @@ type Column struct {
 | 
			
		||||
	ProjectID int64 `xorm:"INDEX NOT NULL"`
 | 
			
		||||
	CreatorID int64 `xorm:"NOT NULL"`
 | 
			
		||||
 | 
			
		||||
	NumIssues int64 `xorm:"-"`
 | 
			
		||||
 | 
			
		||||
	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 | 
			
		||||
	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
			
		||||
}
 | 
			
		||||
@@ -57,20 +59,6 @@ func (Column) TableName() string {
 | 
			
		||||
	return "project_board" // TODO: the legacy table name should be project_column
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NumIssues return counter of all issues assigned to the column
 | 
			
		||||
func (c *Column) NumIssues(ctx context.Context) int {
 | 
			
		||||
	total, err := db.GetEngine(ctx).Table("project_issue").
 | 
			
		||||
		Where("project_id=?", c.ProjectID).
 | 
			
		||||
		And("project_board_id=?", c.ID).
 | 
			
		||||
		GroupBy("issue_id").
 | 
			
		||||
		Cols("issue_id").
 | 
			
		||||
		Count()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return int(total)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
 | 
			
		||||
	issues := make([]*ProjectIssue, 0, 5)
 | 
			
		||||
	if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
 | 
			
		||||
@@ -192,7 +180,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defaultColumn, err := project.GetDefaultColumn(ctx)
 | 
			
		||||
	defaultColumn, err := project.MustDefaultColumn(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -257,8 +245,8 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
 | 
			
		||||
	return columns, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDefaultColumn return default column and ensure only one exists
 | 
			
		||||
func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
 | 
			
		||||
// getDefaultColumn return default column and ensure only one exists
 | 
			
		||||
func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
 | 
			
		||||
	var column Column
 | 
			
		||||
	has, err := db.GetEngine(ctx).
 | 
			
		||||
		Where("project_id=? AND `default` = ?", p.ID, true).
 | 
			
		||||
@@ -270,6 +258,33 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
 | 
			
		||||
	if has {
 | 
			
		||||
		return &column, nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, ErrProjectColumnNotExist{ColumnID: 0}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MustDefaultColumn returns the default column for a project.
 | 
			
		||||
// If one exists, it is returned
 | 
			
		||||
// If none exists, the first column will be elevated to the default column of this project
 | 
			
		||||
func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
 | 
			
		||||
	c, err := p.getDefaultColumn(ctx)
 | 
			
		||||
	if err != nil && !IsErrProjectColumnNotExist(err) {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if c != nil {
 | 
			
		||||
		return c, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var column Column
 | 
			
		||||
	has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if has {
 | 
			
		||||
		column.Default = true
 | 
			
		||||
		if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return &column, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// create a default column if none is found
 | 
			
		||||
	column = Column{
 | 
			
		||||
 
 | 
			
		||||
@@ -20,19 +20,19 @@ func TestGetDefaultColumn(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// check if default column was added
 | 
			
		||||
	column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
 | 
			
		||||
	column, err := projectWithoutDefault.MustDefaultColumn(db.DefaultContext)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, int64(5), column.ProjectID)
 | 
			
		||||
	assert.Equal(t, "Uncategorized", column.Title)
 | 
			
		||||
	assert.Equal(t, "Done", column.Title)
 | 
			
		||||
 | 
			
		||||
	projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// check if multiple defaults were removed
 | 
			
		||||
	column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
 | 
			
		||||
	column, err = projectWithMultipleDefaults.MustDefaultColumn(db.DefaultContext)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, int64(6), column.ProjectID)
 | 
			
		||||
	assert.Equal(t, int64(9), column.ID)
 | 
			
		||||
	assert.Equal(t, int64(9), column.ID) // there are 2 default columns in the test data, use the latest one
 | 
			
		||||
 | 
			
		||||
	// set 8 as default column
 | 
			
		||||
	assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -34,48 +33,6 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NumIssues return counter of all issues assigned to a project
 | 
			
		||||
func (p *Project) NumIssues(ctx context.Context) int {
 | 
			
		||||
	c, err := db.GetEngine(ctx).Table("project_issue").
 | 
			
		||||
		Where("project_id=?", p.ID).
 | 
			
		||||
		GroupBy("issue_id").
 | 
			
		||||
		Cols("issue_id").
 | 
			
		||||
		Count()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("NumIssues: %v", err)
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return int(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NumClosedIssues return counter of closed issues assigned to a project
 | 
			
		||||
func (p *Project) NumClosedIssues(ctx context.Context) int {
 | 
			
		||||
	c, err := db.GetEngine(ctx).Table("project_issue").
 | 
			
		||||
		Join("INNER", "issue", "project_issue.issue_id=issue.id").
 | 
			
		||||
		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
 | 
			
		||||
		Cols("issue_id").
 | 
			
		||||
		Count()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("NumClosedIssues: %v", err)
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return int(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NumOpenIssues return counter of open issues assigned to a project
 | 
			
		||||
func (p *Project) NumOpenIssues(ctx context.Context) int {
 | 
			
		||||
	c, err := db.GetEngine(ctx).Table("project_issue").
 | 
			
		||||
		Join("INNER", "issue", "project_issue.issue_id=issue.id").
 | 
			
		||||
		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
 | 
			
		||||
		Cols("issue_id").
 | 
			
		||||
		Count()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("NumOpenIssues: %v", err)
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return int(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
 | 
			
		||||
	if c.ProjectID != newColumn.ProjectID {
 | 
			
		||||
		return fmt.Errorf("columns have to be in the same project")
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,9 @@ type Project struct {
 | 
			
		||||
	Type         Type
 | 
			
		||||
 | 
			
		||||
	RenderedContent template.HTML `xorm:"-"`
 | 
			
		||||
	NumOpenIssues   int64         `xorm:"-"`
 | 
			
		||||
	NumClosedIssues int64         `xorm:"-"`
 | 
			
		||||
	NumIssues       int64         `xorm:"-"`
 | 
			
		||||
 | 
			
		||||
	CreatedUnix    timeutil.TimeStamp `xorm:"INDEX created"`
 | 
			
		||||
	UpdatedUnix    timeutil.TimeStamp `xorm:"INDEX updated"`
 | 
			
		||||
 
 | 
			
		||||
@@ -73,9 +73,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 | 
			
		||||
		UpdatedBeforeUnix:  options.UpdatedBeforeUnix.Value(),
 | 
			
		||||
		PriorityRepoID:     0,
 | 
			
		||||
		IsArchived:         options.IsArchived,
 | 
			
		||||
		Org:                nil,
 | 
			
		||||
		Owner:              nil,
 | 
			
		||||
		Team:               nil,
 | 
			
		||||
		User:               nil,
 | 
			
		||||
		Doer:               nil,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,11 @@ func Projects(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
 | 
			
		||||
		ctx.ServerError("LoadIssueNumbersForProjects", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
 | 
			
		||||
		OwnerID:  ctx.ContextUser.ID,
 | 
			
		||||
		IsClosed: optional.Some(!isShowClosed),
 | 
			
		||||
@@ -328,6 +333,10 @@ func ViewProject(ctx *context.Context) {
 | 
			
		||||
		ctx.NotFound("", nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err := project.LoadOwner(ctx); err != nil {
 | 
			
		||||
		ctx.ServerError("LoadOwner", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	columns, err := project.GetColumns(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -341,14 +350,21 @@ func ViewProject(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
 | 
			
		||||
 | 
			
		||||
	issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
 | 
			
		||||
	opts := issues_model.IssuesOptions{
 | 
			
		||||
		LabelIDs:   labelIDs,
 | 
			
		||||
		AssigneeID: optional.Some(assigneeID),
 | 
			
		||||
	})
 | 
			
		||||
		Owner:      project.Owner,
 | 
			
		||||
		Doer:       ctx.Doer,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("LoadIssuesOfColumns", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, column := range columns {
 | 
			
		||||
		column.NumIssues = int64(len(issuesMap[column.ID]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if project.CardType != project_model.CardTypeTextOnly {
 | 
			
		||||
		issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
 | 
			
		||||
 
 | 
			
		||||
@@ -92,6 +92,11 @@ func Projects(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
 | 
			
		||||
		ctx.ServerError("LoadIssueNumbersForProjects", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range projects {
 | 
			
		||||
		rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
 | 
			
		||||
		projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description)
 | 
			
		||||
@@ -312,7 +317,8 @@ func ViewProject(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
 | 
			
		||||
 | 
			
		||||
	issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
 | 
			
		||||
	issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
 | 
			
		||||
		RepoIDs:    []int64{ctx.Repo.Repository.ID},
 | 
			
		||||
		LabelIDs:   labelIDs,
 | 
			
		||||
		AssigneeID: optional.Some(assigneeID),
 | 
			
		||||
	})
 | 
			
		||||
@@ -320,6 +326,9 @@ func ViewProject(ctx *context.Context) {
 | 
			
		||||
		ctx.ServerError("LoadIssuesOfColumns", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, column := range columns {
 | 
			
		||||
		column.NumIssues = int64(len(issuesMap[column.ID]))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if project.CardType != project_model.CardTypeTextOnly {
 | 
			
		||||
		issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
 | 
			
		||||
 
 | 
			
		||||
@@ -419,7 +419,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
			
		||||
		IsPull:     optional.Some(isPullList),
 | 
			
		||||
		SortType:   sortType,
 | 
			
		||||
		IsArchived: optional.Some(false),
 | 
			
		||||
		User:       ctx.Doer,
 | 
			
		||||
		Doer:       ctx.Doer,
 | 
			
		||||
	}
 | 
			
		||||
	// --------------------------------------------------------------------------
 | 
			
		||||
	// Build opts (IssuesOptions), which contains filter information.
 | 
			
		||||
@@ -431,7 +431,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 | 
			
		||||
 | 
			
		||||
	// Get repository IDs where User/Org/Team has access.
 | 
			
		||||
	if ctx.Org != nil && ctx.Org.Organization != nil {
 | 
			
		||||
		opts.Org = ctx.Org.Organization
 | 
			
		||||
		opts.Owner = ctx.Org.Organization.AsUser()
 | 
			
		||||
		opts.Team = ctx.Org.Team
 | 
			
		||||
 | 
			
		||||
		issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	project_model "code.gitea.io/gitea/models/project"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
 | 
			
		||||
@@ -84,3 +85,123 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadIssuesFromProject load issues assigned to each project column inside the given project
 | 
			
		||||
func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (map[int64]issues_model.IssueList, error) {
 | 
			
		||||
	issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
 | 
			
		||||
		o.ProjectID = project.ID
 | 
			
		||||
		o.SortType = "project-column-sorting"
 | 
			
		||||
	}))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := issueList.LoadComments(ctx); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defaultColumn, err := project.MustDefaultColumn(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	issueColumnMap, err := issues_model.LoadProjectIssueColumnMap(ctx, project.ID, defaultColumn.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	results := make(map[int64]issues_model.IssueList)
 | 
			
		||||
	for _, issue := range issueList {
 | 
			
		||||
		projectColumnID, ok := issueColumnMap[issue.ID]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if _, ok := results[projectColumnID]; !ok {
 | 
			
		||||
			results[projectColumnID] = make(issues_model.IssueList, 0)
 | 
			
		||||
		}
 | 
			
		||||
		results[projectColumnID] = append(results[projectColumnID], issue)
 | 
			
		||||
	}
 | 
			
		||||
	return results, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NumClosedIssues return counter of closed issues assigned to a project
 | 
			
		||||
func loadNumClosedIssues(ctx context.Context, p *project_model.Project) error {
 | 
			
		||||
	cnt, err := db.GetEngine(ctx).Table("project_issue").
 | 
			
		||||
		Join("INNER", "issue", "project_issue.issue_id=issue.id").
 | 
			
		||||
		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
 | 
			
		||||
		Cols("issue_id").
 | 
			
		||||
		Count()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	p.NumClosedIssues = cnt
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NumOpenIssues return counter of open issues assigned to a project
 | 
			
		||||
func loadNumOpenIssues(ctx context.Context, p *project_model.Project) error {
 | 
			
		||||
	cnt, err := db.GetEngine(ctx).Table("project_issue").
 | 
			
		||||
		Join("INNER", "issue", "project_issue.issue_id=issue.id").
 | 
			
		||||
		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
 | 
			
		||||
		Cols("issue_id").
 | 
			
		||||
		Count()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	p.NumOpenIssues = cnt
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadIssueNumbersForProjects(ctx context.Context, projects []*project_model.Project, doer *user_model.User) error {
 | 
			
		||||
	for _, project := range projects {
 | 
			
		||||
		if err := LoadIssueNumbersForProject(ctx, project, doer); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Project, doer *user_model.User) error {
 | 
			
		||||
	// for repository project, just get the numbers
 | 
			
		||||
	if project.OwnerID == 0 {
 | 
			
		||||
		if err := loadNumClosedIssues(ctx, project); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := loadNumOpenIssues(ctx, project); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		project.NumIssues = project.NumClosedIssues + project.NumOpenIssues
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := project.LoadOwner(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// for user or org projects, we need to check access permissions
 | 
			
		||||
	opts := issues_model.IssuesOptions{
 | 
			
		||||
		ProjectID: project.ID,
 | 
			
		||||
		Doer:      doer,
 | 
			
		||||
		AllPublic: doer == nil,
 | 
			
		||||
		Owner:     project.Owner,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	project.NumOpenIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
 | 
			
		||||
		o.IsClosed = optional.Some(false)
 | 
			
		||||
	}))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	project.NumClosedIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
 | 
			
		||||
		o.IsClosed = optional.Some(true)
 | 
			
		||||
	}))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	project.NumIssues = project.NumClosedIssues + project.NumOpenIssues
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										210
									
								
								services/projects/issue_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								services/projects/issue_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package project
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	org_model "code.gitea.io/gitea/models/organization"
 | 
			
		||||
	project_model "code.gitea.io/gitea/models/project"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_Projects(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
			
		||||
	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
	org3 := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3})
 | 
			
		||||
	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
			
		||||
 | 
			
		||||
	t.Run("User projects", func(t *testing.T) {
 | 
			
		||||
		pi1 := project_model.ProjectIssue{
 | 
			
		||||
			ProjectID:       4,
 | 
			
		||||
			IssueID:         1,
 | 
			
		||||
			ProjectColumnID: 4,
 | 
			
		||||
		}
 | 
			
		||||
		err := db.Insert(db.DefaultContext, &pi1)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		defer func() {
 | 
			
		||||
			_, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi1.ID)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		pi2 := project_model.ProjectIssue{
 | 
			
		||||
			ProjectID:       4,
 | 
			
		||||
			IssueID:         4,
 | 
			
		||||
			ProjectColumnID: 4,
 | 
			
		||||
		}
 | 
			
		||||
		err = db.Insert(db.DefaultContext, &pi2)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		defer func() {
 | 
			
		||||
			_, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi2.ID)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
 | 
			
		||||
			OwnerID: user2.ID,
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Len(t, projects, 3)
 | 
			
		||||
		assert.EqualValues(t, 4, projects[0].ID)
 | 
			
		||||
 | 
			
		||||
		t.Run("Authenticated user", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				Owner: user2,
 | 
			
		||||
				Doer:  user2,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 1)    // 4 has 2 issues, 6 will not contains here because 0 issues
 | 
			
		||||
			assert.Len(t, columnIssues[4], 2) // user2 can visit both issues, one from public repository one from private repository
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("Anonymous user", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				AllPublic: true,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 1)
 | 
			
		||||
			assert.Len(t, columnIssues[4], 1) // anonymous user can only visit public repo issues
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				Owner: user2,
 | 
			
		||||
				Doer:  user4,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 1)
 | 
			
		||||
			assert.Len(t, columnIssues[4], 1) // user4 can only visit public repo issues
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Org projects", func(t *testing.T) {
 | 
			
		||||
		project1 := project_model.Project{
 | 
			
		||||
			Title:        "project in an org",
 | 
			
		||||
			OwnerID:      org3.ID,
 | 
			
		||||
			Type:         project_model.TypeOrganization,
 | 
			
		||||
			TemplateType: project_model.TemplateTypeBasicKanban,
 | 
			
		||||
		}
 | 
			
		||||
		err := project_model.NewProject(db.DefaultContext, &project1)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		defer func() {
 | 
			
		||||
			err := project_model.DeleteProjectByID(db.DefaultContext, project1.ID)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		column1 := project_model.Column{
 | 
			
		||||
			Title:     "column 1",
 | 
			
		||||
			ProjectID: project1.ID,
 | 
			
		||||
		}
 | 
			
		||||
		err = project_model.NewColumn(db.DefaultContext, &column1)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		column2 := project_model.Column{
 | 
			
		||||
			Title:     "column 2",
 | 
			
		||||
			ProjectID: project1.ID,
 | 
			
		||||
		}
 | 
			
		||||
		err = project_model.NewColumn(db.DefaultContext, &column2)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// issue 6 belongs to private repo 3 under org 3
 | 
			
		||||
		issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
 | 
			
		||||
		err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue6, user2, project1.ID, column1.ID)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// issue 16 belongs to public repo 16 under org 3
 | 
			
		||||
		issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16})
 | 
			
		||||
		err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue16, user2, project1.ID, column1.ID)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
 | 
			
		||||
			OwnerID: org3.ID,
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Len(t, projects, 1)
 | 
			
		||||
		assert.EqualValues(t, project1.ID, projects[0].ID)
 | 
			
		||||
 | 
			
		||||
		t.Run("Authenticated user", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				Owner: org3.AsUser(),
 | 
			
		||||
				Doer:  userAdmin,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 1)             // column1 has 2 issues, 6 will not contains here because 0 issues
 | 
			
		||||
			assert.Len(t, columnIssues[column1.ID], 2) // user2 can visit both issues, one from public repository one from private repository
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("Anonymous user", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				AllPublic: true,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 1)
 | 
			
		||||
			assert.Len(t, columnIssues[column1.ID], 1) // anonymous user can only visit public repo issues
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				Owner: org3.AsUser(),
 | 
			
		||||
				Doer:  user2,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 1)
 | 
			
		||||
			assert.Len(t, columnIssues[column1.ID], 1) // user4 can only visit public repo issues
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Repository projects", func(t *testing.T) {
 | 
			
		||||
		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 | 
			
		||||
 | 
			
		||||
		projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
 | 
			
		||||
			RepoID: repo1.ID,
 | 
			
		||||
		})
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.Len(t, projects, 1)
 | 
			
		||||
		assert.EqualValues(t, 1, projects[0].ID)
 | 
			
		||||
 | 
			
		||||
		t.Run("Authenticated user", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				RepoIDs: []int64{repo1.ID},
 | 
			
		||||
				Doer:    userAdmin,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 3)
 | 
			
		||||
			assert.Len(t, columnIssues[1], 2)
 | 
			
		||||
			assert.Len(t, columnIssues[2], 1)
 | 
			
		||||
			assert.Len(t, columnIssues[3], 1)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("Anonymous user", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				AllPublic: true,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 3)
 | 
			
		||||
			assert.Len(t, columnIssues[1], 2)
 | 
			
		||||
			assert.Len(t, columnIssues[2], 1)
 | 
			
		||||
			assert.Len(t, columnIssues[3], 1)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
 | 
			
		||||
			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
 | 
			
		||||
				RepoIDs: []int64{repo1.ID},
 | 
			
		||||
				Doer:    user2,
 | 
			
		||||
			})
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			assert.Len(t, columnIssues, 3)
 | 
			
		||||
			assert.Len(t, columnIssues[1], 2)
 | 
			
		||||
			assert.Len(t, columnIssues[2], 1)
 | 
			
		||||
			assert.Len(t, columnIssues[3], 1)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								services/projects/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								services/projects/main_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package project
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
 | 
			
		||||
	_ "code.gitea.io/gitea/models"
 | 
			
		||||
	_ "code.gitea.io/gitea/models/actions"
 | 
			
		||||
	_ "code.gitea.io/gitea/models/activities"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	unittest.MainTest(m)
 | 
			
		||||
}
 | 
			
		||||
@@ -52,11 +52,11 @@
 | 
			
		||||
				<div class="group">
 | 
			
		||||
					<div class="flex-text-block">
 | 
			
		||||
						{{svg "octicon-issue-opened" 14}}
 | 
			
		||||
						{{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}} {{ctx.Locale.Tr "repo.issues.open_title"}}
 | 
			
		||||
						{{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="flex-text-block">
 | 
			
		||||
						{{svg "octicon-check" 14}}
 | 
			
		||||
						{{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
 | 
			
		||||
						{{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,7 @@
 | 
			
		||||
			<div class="project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
 | 
			
		||||
				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
 | 
			
		||||
					<div class="ui circular label project-column-issue-count">
 | 
			
		||||
						{{.NumIssues ctx}}
 | 
			
		||||
						{{.NumIssues}}
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="project-column-title-label gt-ellipsis">{{.Title}}</div>
 | 
			
		||||
					{{if $canWriteProject}}
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	columnsAfter, err := project1.GetColumns(db.DefaultContext)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, columns, 3)
 | 
			
		||||
	assert.Len(t, columnsAfter, 3)
 | 
			
		||||
	assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
 | 
			
		||||
	assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID)
 | 
			
		||||
	assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user