mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Move project files into models/project sub package (#17704)
* Move project files into models/project sub package * Fix test * Fix test * Fix test * Fix build * Fix test * Fix template bug * Fix bug * Fix lint * Fix test * Fix import * Improve codes Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
		| @@ -1063,44 +1063,6 @@ func (err ErrLabelNotExist) Error() string { | |||||||
| 	return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) | 	return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) | ||||||
| } | } | ||||||
|  |  | ||||||
| // __________                   __               __ |  | ||||||
| // \______   \_______  ____    |__| ____   _____/  |_  ______ |  | ||||||
| //  |     ___/\_  __ \/  _ \   |  |/ __ \_/ ___\   __\/  ___/ |  | ||||||
| //  |    |     |  | \(  <_> )  |  \  ___/\  \___|  |  \___ \ |  | ||||||
| //  |____|     |__|   \____/\__|  |\___  >\___  >__| /____  > |  | ||||||
| //                         \______|    \/     \/          \/ |  | ||||||
|  |  | ||||||
| // ErrProjectNotExist represents a "ProjectNotExist" kind of error. |  | ||||||
| type ErrProjectNotExist struct { |  | ||||||
| 	ID     int64 |  | ||||||
| 	RepoID int64 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // IsErrProjectNotExist checks if an error is a ErrProjectNotExist |  | ||||||
| func IsErrProjectNotExist(err error) bool { |  | ||||||
| 	_, ok := err.(ErrProjectNotExist) |  | ||||||
| 	return ok |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (err ErrProjectNotExist) Error() string { |  | ||||||
| 	return fmt.Sprintf("projects does not exist [id: %d]", err.ID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error. |  | ||||||
| type ErrProjectBoardNotExist struct { |  | ||||||
| 	BoardID int64 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist |  | ||||||
| func IsErrProjectBoardNotExist(err error) bool { |  | ||||||
| 	_, ok := err.(ErrProjectBoardNotExist) |  | ||||||
| 	return ok |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (err ErrProjectBoardNotExist) Error() string { |  | ||||||
| 	return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //    _____  .__.__                   __ | //    _____  .__.__                   __ | ||||||
| //   /     \ |__|  |   ____   _______/  |_  ____   ____   ____ | //   /     \ |__|  |   ____   _______/  |_  ____   ____   ____ | ||||||
| //  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \ | //  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \ | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/issues" | 	"code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	"code.gitea.io/gitea/models/perm" | 	"code.gitea.io/gitea/models/perm" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -45,14 +46,14 @@ type Issue struct { | |||||||
| 	PosterID         int64                  `xorm:"INDEX"` | 	PosterID         int64                  `xorm:"INDEX"` | ||||||
| 	Poster           *user_model.User       `xorm:"-"` | 	Poster           *user_model.User       `xorm:"-"` | ||||||
| 	OriginalAuthor   string | 	OriginalAuthor   string | ||||||
| 	OriginalAuthorID int64      `xorm:"index"` | 	OriginalAuthorID int64                  `xorm:"index"` | ||||||
| 	Title            string     `xorm:"name"` | 	Title            string                 `xorm:"name"` | ||||||
| 	Content          string     `xorm:"LONGTEXT"` | 	Content          string                 `xorm:"LONGTEXT"` | ||||||
| 	RenderedContent  string     `xorm:"-"` | 	RenderedContent  string                 `xorm:"-"` | ||||||
| 	Labels           []*Label   `xorm:"-"` | 	Labels           []*Label               `xorm:"-"` | ||||||
| 	MilestoneID      int64      `xorm:"INDEX"` | 	MilestoneID      int64                  `xorm:"INDEX"` | ||||||
| 	Milestone        *Milestone `xorm:"-"` | 	Milestone        *Milestone             `xorm:"-"` | ||||||
| 	Project          *Project   `xorm:"-"` | 	Project          *project_model.Project `xorm:"-"` | ||||||
| 	Priority         int | 	Priority         int | ||||||
| 	AssigneeID       int64            `xorm:"-"` | 	AssigneeID       int64            `xorm:"-"` | ||||||
| 	Assignee         *user_model.User `xorm:"-"` | 	Assignee         *user_model.User `xorm:"-"` | ||||||
| @@ -2135,7 +2136,7 @@ func deleteIssue(ctx context.Context, issue *Issue) error { | |||||||
| 		&IssueWatch{}, | 		&IssueWatch{}, | ||||||
| 		&Stopwatch{}, | 		&Stopwatch{}, | ||||||
| 		&TrackedTime{}, | 		&TrackedTime{}, | ||||||
| 		&ProjectIssue{}, | 		&project_model.ProjectIssue{}, | ||||||
| 		&repo_model.Attachment{}, | 		&repo_model.Attachment{}, | ||||||
| 		&PullRequest{}, | 		&PullRequest{}, | ||||||
| 	); err != nil { | 	); err != nil { | ||||||
| @@ -2469,7 +2470,7 @@ func deleteIssuesByRepoID(sess db.Engine, repoID int64) (attachmentPaths []strin | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err = sess.In("issue_id", deleteCond). | 	if _, err = sess.In("issue_id", deleteCond). | ||||||
| 		Delete(&ProjectIssue{}); err != nil { | 		Delete(&project_model.ProjectIssue{}); err != nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/issues" | 	"code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| @@ -204,8 +205,8 @@ type Comment struct { | |||||||
| 	RemovedLabels    []*Label `xorm:"-"` | 	RemovedLabels    []*Label `xorm:"-"` | ||||||
| 	OldProjectID     int64 | 	OldProjectID     int64 | ||||||
| 	ProjectID        int64 | 	ProjectID        int64 | ||||||
| 	OldProject       *Project `xorm:"-"` | 	OldProject       *project_model.Project `xorm:"-"` | ||||||
| 	Project          *Project `xorm:"-"` | 	Project          *project_model.Project `xorm:"-"` | ||||||
| 	OldMilestoneID   int64 | 	OldMilestoneID   int64 | ||||||
| 	MilestoneID      int64 | 	MilestoneID      int64 | ||||||
| 	OldMilestone     *Milestone `xorm:"-"` | 	OldMilestone     *Milestone `xorm:"-"` | ||||||
| @@ -469,7 +470,7 @@ func (c *Comment) LoadLabel() error { | |||||||
| // LoadProject if comment.Type is CommentTypeProject, then load project. | // LoadProject if comment.Type is CommentTypeProject, then load project. | ||||||
| func (c *Comment) LoadProject() error { | func (c *Comment) LoadProject() error { | ||||||
| 	if c.OldProjectID > 0 { | 	if c.OldProjectID > 0 { | ||||||
| 		var oldProject Project | 		var oldProject project_model.Project | ||||||
| 		has, err := db.GetEngine(db.DefaultContext).ID(c.OldProjectID).Get(&oldProject) | 		has, err := db.GetEngine(db.DefaultContext).ID(c.OldProjectID).Get(&oldProject) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -479,7 +480,7 @@ func (c *Comment) LoadProject() error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if c.ProjectID > 0 { | 	if c.ProjectID > 0 { | ||||||
| 		var project Project | 		var project project_model.Project | ||||||
| 		has, err := db.GetEngine(db.DefaultContext).ID(c.ProjectID).Get(&project) | 		has, err := db.GetEngine(db.DefaultContext).ID(c.ProjectID).Get(&project) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
|   | |||||||
							
								
								
									
										181
									
								
								models/issue_project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								models/issue_project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | // Copyright 2021 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package models | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // LoadProject load the project the issue was assigned to | ||||||
|  | func (i *Issue) LoadProject() (err error) { | ||||||
|  | 	return i.loadProject(db.GetEngine(db.DefaultContext)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (i *Issue) loadProject(e db.Engine) (err error) { | ||||||
|  | 	if i.Project == nil { | ||||||
|  | 		var p project_model.Project | ||||||
|  | 		if _, err = e.Table("project"). | ||||||
|  | 			Join("INNER", "project_issue", "project.id=project_issue.project_id"). | ||||||
|  | 			Where("project_issue.issue_id = ?", i.ID). | ||||||
|  | 			Get(&p); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		i.Project = &p | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ProjectID return project id if issue was assigned to one | ||||||
|  | func (i *Issue) ProjectID() int64 { | ||||||
|  | 	return i.projectID(db.GetEngine(db.DefaultContext)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (i *Issue) projectID(e db.Engine) int64 { | ||||||
|  | 	var ip project_model.ProjectIssue | ||||||
|  | 	has, err := e.Where("issue_id=?", i.ID).Get(&ip) | ||||||
|  | 	if err != nil || !has { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 	return ip.ProjectID | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ProjectBoardID return project board id if issue was assigned to one | ||||||
|  | func (i *Issue) ProjectBoardID() int64 { | ||||||
|  | 	return i.projectBoardID(db.GetEngine(db.DefaultContext)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (i *Issue) projectBoardID(e db.Engine) int64 { | ||||||
|  | 	var ip project_model.ProjectIssue | ||||||
|  | 	has, err := e.Where("issue_id=?", i.ID).Get(&ip) | ||||||
|  | 	if err != nil || !has { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 	return ip.ProjectBoardID | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LoadIssuesFromBoard load issues assigned to this board | ||||||
|  | func LoadIssuesFromBoard(b *project_model.Board) (IssueList, error) { | ||||||
|  | 	issueList := make([]*Issue, 0, 10) | ||||||
|  |  | ||||||
|  | 	if b.ID != 0 { | ||||||
|  | 		issues, err := Issues(&IssuesOptions{ | ||||||
|  | 			ProjectBoardID: b.ID, | ||||||
|  | 			ProjectID:      b.ProjectID, | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		issueList = issues | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if b.Default { | ||||||
|  | 		issues, err := Issues(&IssuesOptions{ | ||||||
|  | 			ProjectBoardID: -1, // Issues without ProjectBoardID | ||||||
|  | 			ProjectID:      b.ProjectID, | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		issueList = append(issueList, issues...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := IssueList(issueList).LoadComments(); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return issueList, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LoadIssuesFromBoardList load issues assigned to the boards | ||||||
|  | func LoadIssuesFromBoardList(bs project_model.BoardList) (map[int64]IssueList, error) { | ||||||
|  | 	issuesMap := make(map[int64]IssueList, len(bs)) | ||||||
|  | 	for i := range bs { | ||||||
|  | 		il, err := LoadIssuesFromBoard(bs[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		issuesMap[bs[i].ID] = il | ||||||
|  | 	} | ||||||
|  | 	return issuesMap, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ChangeProjectAssign changes the project associated with an issue | ||||||
|  | func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error { | ||||||
|  | 	ctx, committer, err := db.TxContext() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer committer.Close() | ||||||
|  |  | ||||||
|  | 	if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return committer.Commit() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { | ||||||
|  | 	e := db.GetEngine(ctx) | ||||||
|  | 	oldProjectID := issue.projectID(e) | ||||||
|  |  | ||||||
|  | 	if _, err := e.Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := issue.loadRepo(ctx); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if oldProjectID > 0 || newProjectID > 0 { | ||||||
|  | 		if _, err := createComment(ctx, &CreateCommentOptions{ | ||||||
|  | 			Type:         CommentTypeProject, | ||||||
|  | 			Doer:         doer, | ||||||
|  | 			Repo:         issue.Repo, | ||||||
|  | 			Issue:        issue, | ||||||
|  | 			OldProjectID: oldProjectID, | ||||||
|  | 			ProjectID:    newProjectID, | ||||||
|  | 		}); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err := e.Insert(&project_model.ProjectIssue{ | ||||||
|  | 		IssueID:   issue.ID, | ||||||
|  | 		ProjectID: newProjectID, | ||||||
|  | 	}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MoveIssueAcrossProjectBoards move a card from one board to another | ||||||
|  | func MoveIssueAcrossProjectBoards(issue *Issue, board *project_model.Board) error { | ||||||
|  | 	ctx, committer, err := db.TxContext() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer committer.Close() | ||||||
|  | 	sess := db.GetEngine(ctx) | ||||||
|  |  | ||||||
|  | 	var pis project_model.ProjectIssue | ||||||
|  | 	has, err := sess.Where("issue_id=?", issue.ID).Get(&pis) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !has { | ||||||
|  | 		return fmt.Errorf("issue has to be added to a project first") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pis.ProjectBoardID = board.ID | ||||||
|  | 	if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return committer.Commit() | ||||||
|  | } | ||||||
							
								
								
									
										289
									
								
								models/project/board.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								models/project/board.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,289 @@ | |||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package project | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"regexp" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  |  | ||||||
|  | 	"xorm.io/builder" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type ( | ||||||
|  | 	// BoardType is used to represent a project board type | ||||||
|  | 	BoardType uint8 | ||||||
|  |  | ||||||
|  | 	// BoardList is a list of all project boards in a repository | ||||||
|  | 	BoardList []*Board | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// BoardTypeNone is a project board type that has no predefined columns | ||||||
|  | 	BoardTypeNone BoardType = iota | ||||||
|  |  | ||||||
|  | 	// BoardTypeBasicKanban is a project board type that has basic predefined columns | ||||||
|  | 	BoardTypeBasicKanban | ||||||
|  |  | ||||||
|  | 	// BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs | ||||||
|  | 	BoardTypeBugTriage | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // BoardColorPattern is a regexp witch can validate BoardColor | ||||||
|  | var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") | ||||||
|  |  | ||||||
|  | // Board is used to represent boards on a project | ||||||
|  | type Board struct { | ||||||
|  | 	ID      int64 `xorm:"pk autoincr"` | ||||||
|  | 	Title   string | ||||||
|  | 	Default bool   `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board | ||||||
|  | 	Sorting int8   `xorm:"NOT NULL DEFAULT 0"` | ||||||
|  | 	Color   string `xorm:"VARCHAR(7)"` | ||||||
|  |  | ||||||
|  | 	ProjectID int64 `xorm:"INDEX NOT NULL"` | ||||||
|  | 	CreatorID int64 `xorm:"NOT NULL"` | ||||||
|  |  | ||||||
|  | 	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | ||||||
|  | 	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TableName return the real table name | ||||||
|  | func (Board) TableName() string { | ||||||
|  | 	return "project_board" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NumIssues return counter of all issues assigned to the board | ||||||
|  | func (b *Board) NumIssues() int { | ||||||
|  | 	c, err := db.GetEngine(db.DefaultContext).Table("project_issue"). | ||||||
|  | 		Where("project_id=?", b.ProjectID). | ||||||
|  | 		And("project_board_id=?", b.ID). | ||||||
|  | 		GroupBy("issue_id"). | ||||||
|  | 		Cols("issue_id"). | ||||||
|  | 		Count() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 	return int(c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	db.RegisterModel(new(Board)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsBoardTypeValid checks if the project board type is valid | ||||||
|  | func IsBoardTypeValid(p BoardType) bool { | ||||||
|  | 	switch p { | ||||||
|  | 	case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage: | ||||||
|  | 		return true | ||||||
|  | 	default: | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createBoardsForProjectsType(ctx context.Context, project *Project) error { | ||||||
|  | 	var items []string | ||||||
|  |  | ||||||
|  | 	switch project.BoardType { | ||||||
|  |  | ||||||
|  | 	case BoardTypeBugTriage: | ||||||
|  | 		items = setting.Project.ProjectBoardBugTriageType | ||||||
|  |  | ||||||
|  | 	case BoardTypeBasicKanban: | ||||||
|  | 		items = setting.Project.ProjectBoardBasicKanbanType | ||||||
|  |  | ||||||
|  | 	case BoardTypeNone: | ||||||
|  | 		fallthrough | ||||||
|  | 	default: | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(items) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	boards := make([]Board, 0, len(items)) | ||||||
|  |  | ||||||
|  | 	for _, v := range items { | ||||||
|  | 		boards = append(boards, Board{ | ||||||
|  | 			CreatedUnix: timeutil.TimeStampNow(), | ||||||
|  | 			CreatorID:   project.CreatorID, | ||||||
|  | 			Title:       v, | ||||||
|  | 			ProjectID:   project.ID, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return db.Insert(ctx, boards) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBoard adds a new project board to a given project | ||||||
|  | func NewBoard(board *Board) error { | ||||||
|  | 	if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { | ||||||
|  | 		return fmt.Errorf("bad color code: %s", board.Color) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err := db.GetEngine(db.DefaultContext).Insert(board) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteBoardByID removes all issues references to the project board. | ||||||
|  | func DeleteBoardByID(boardID int64) error { | ||||||
|  | 	ctx, committer, err := db.TxContext() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer committer.Close() | ||||||
|  |  | ||||||
|  | 	if err := deleteBoardByID(ctx, boardID); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return committer.Commit() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func deleteBoardByID(ctx context.Context, boardID int64) error { | ||||||
|  | 	e := db.GetEngine(ctx) | ||||||
|  | 	board, err := getBoard(e, boardID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if IsErrProjectBoardNotExist(err) { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = board.removeIssues(e); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if _, err := e.ID(board.ID).Delete(board); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func deleteBoardByProjectID(e db.Engine, projectID int64) error { | ||||||
|  | 	_, err := e.Where("project_id=?", projectID).Delete(&Board{}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetBoard fetches the current board of a project | ||||||
|  | func GetBoard(boardID int64) (*Board, error) { | ||||||
|  | 	return getBoard(db.GetEngine(db.DefaultContext), boardID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getBoard(e db.Engine, boardID int64) (*Board, error) { | ||||||
|  | 	board := new(Board) | ||||||
|  |  | ||||||
|  | 	has, err := e.ID(boardID).Get(board) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} else if !has { | ||||||
|  | 		return nil, ErrProjectBoardNotExist{BoardID: boardID} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return board, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UpdateBoard updates a project board | ||||||
|  | func UpdateBoard(board *Board) error { | ||||||
|  | 	return updateBoard(db.GetEngine(db.DefaultContext), board) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func updateBoard(e db.Engine, board *Board) error { | ||||||
|  | 	var fieldToUpdate []string | ||||||
|  |  | ||||||
|  | 	if board.Sorting != 0 { | ||||||
|  | 		fieldToUpdate = append(fieldToUpdate, "sorting") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if board.Title != "" { | ||||||
|  | 		fieldToUpdate = append(fieldToUpdate, "title") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { | ||||||
|  | 		return fmt.Errorf("bad color code: %s", board.Color) | ||||||
|  | 	} | ||||||
|  | 	fieldToUpdate = append(fieldToUpdate, "color") | ||||||
|  |  | ||||||
|  | 	_, err := e.ID(board.ID).Cols(fieldToUpdate...).Update(board) | ||||||
|  |  | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetBoards fetches all boards related to a project | ||||||
|  | // if no default board set, first board is a temporary "Uncategorized" board | ||||||
|  | func GetBoards(projectID int64) (BoardList, error) { | ||||||
|  | 	return getBoards(db.GetEngine(db.DefaultContext), projectID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getBoards(e db.Engine, projectID int64) ([]*Board, error) { | ||||||
|  | 	boards := make([]*Board, 0, 5) | ||||||
|  |  | ||||||
|  | 	if err := e.Where("project_id=? AND `default`=?", projectID, false).OrderBy("Sorting").Find(&boards); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defaultB, err := getDefaultBoard(e, projectID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return append([]*Board{defaultB}, boards...), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getDefaultBoard return default board and create a dummy if none exist | ||||||
|  | func getDefaultBoard(e db.Engine, projectID int64) (*Board, error) { | ||||||
|  | 	var board Board | ||||||
|  | 	exist, err := e.Where("project_id=? AND `default`=?", projectID, true).Get(&board) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if exist { | ||||||
|  | 		return &board, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// represents a board for issues not assigned to one | ||||||
|  | 	return &Board{ | ||||||
|  | 		ProjectID: projectID, | ||||||
|  | 		Title:     "Uncategorized", | ||||||
|  | 		Default:   true, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetDefaultBoard represents a board for issues not assigned to one | ||||||
|  | // if boardID is 0 unset default | ||||||
|  | func SetDefaultBoard(projectID, boardID int64) error { | ||||||
|  | 	_, err := db.GetEngine(db.DefaultContext).Where(builder.Eq{ | ||||||
|  | 		"project_id": projectID, | ||||||
|  | 		"`default`":  true, | ||||||
|  | 	}).Cols("`default`").Update(&Board{Default: false}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if boardID > 0 { | ||||||
|  | 		_, err = db.GetEngine(db.DefaultContext).ID(boardID).Where(builder.Eq{"project_id": projectID}). | ||||||
|  | 			Cols("`default`").Update(&Board{Default: true}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UpdateBoardSorting update project board sorting | ||||||
|  | func UpdateBoardSorting(bs BoardList) error { | ||||||
|  | 	for i := range bs { | ||||||
|  | 		_, err := db.GetEngine(db.DefaultContext).ID(bs[i].ID).Cols( | ||||||
|  | 			"sorting", | ||||||
|  | 		).Update(bs[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										100
									
								
								models/project/issue.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								models/project/issue.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package project | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ProjectIssue saves relation from issue to a project | ||||||
|  | type ProjectIssue struct { //revive:disable-line:exported | ||||||
|  | 	ID        int64 `xorm:"pk autoincr"` | ||||||
|  | 	IssueID   int64 `xorm:"INDEX"` | ||||||
|  | 	ProjectID int64 `xorm:"INDEX"` | ||||||
|  |  | ||||||
|  | 	// If 0, then it has not been added to a specific board in the project | ||||||
|  | 	ProjectBoardID int64 `xorm:"INDEX"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	db.RegisterModel(new(ProjectIssue)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func deleteProjectIssuesByProjectID(e db.Engine, projectID int64) error { | ||||||
|  | 	_, err := e.Where("project_id=?", projectID).Delete(&ProjectIssue{}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NumIssues return counter of all issues assigned to a project | ||||||
|  | func (p *Project) NumIssues() int { | ||||||
|  | 	c, err := db.GetEngine(db.DefaultContext).Table("project_issue"). | ||||||
|  | 		Where("project_id=?", p.ID). | ||||||
|  | 		GroupBy("issue_id"). | ||||||
|  | 		Cols("issue_id"). | ||||||
|  | 		Count() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 	return int(c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NumClosedIssues return counter of closed issues assigned to a project | ||||||
|  | func (p *Project) NumClosedIssues() int { | ||||||
|  | 	c, err := db.GetEngine(db.DefaultContext).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 0 | ||||||
|  | 	} | ||||||
|  | 	return int(c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NumOpenIssues return counter of open issues assigned to a project | ||||||
|  | func (p *Project) NumOpenIssues() int { | ||||||
|  | 	c, err := db.GetEngine(db.DefaultContext).Table("project_issue"). | ||||||
|  | 		Join("INNER", "issue", "project_issue.issue_id=issue.id"). | ||||||
|  | 		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).Count("issue.id") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0 | ||||||
|  | 	} | ||||||
|  | 	return int(c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column | ||||||
|  | func MoveIssuesOnProjectBoard(board *Board, sortedIssueIDs map[int64]int64) error { | ||||||
|  | 	return db.WithTx(func(ctx context.Context) error { | ||||||
|  | 		sess := db.GetEngine(ctx) | ||||||
|  |  | ||||||
|  | 		issueIDs := make([]int64, 0, len(sortedIssueIDs)) | ||||||
|  | 		for _, issueID := range sortedIssueIDs { | ||||||
|  | 			issueIDs = append(issueIDs, issueID) | ||||||
|  | 		} | ||||||
|  | 		count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if int(count) != len(sortedIssueIDs) { | ||||||
|  | 			return fmt.Errorf("all issues have to be added to a project first") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for sorting, issueID := range sortedIssueIDs { | ||||||
|  | 			_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pb *Board) removeIssues(e db.Engine) error { | ||||||
|  | 	_, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", pb.ID) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								models/project/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								models/project/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a MIT-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package project | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/unittest" | ||||||
|  |  | ||||||
|  | 	_ "code.gitea.io/gitea/models/repo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestMain(m *testing.M) { | ||||||
|  | 	unittest.MainTest(m, filepath.Join("..", ".."), | ||||||
|  | 		"project.yml", | ||||||
|  | 		"project_board.yml", | ||||||
|  | 		"project_issue.yml", | ||||||
|  | 		"repository.yml", | ||||||
|  | 	) | ||||||
|  | } | ||||||
| @@ -2,9 +2,10 @@ | |||||||
| // Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||||
| // license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||||
| 
 | 
 | ||||||
| package models | package project | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| @@ -19,25 +20,56 @@ import ( | |||||||
| type ( | type ( | ||||||
| 	// ProjectsConfig is used to identify the type of board that is being created | 	// ProjectsConfig is used to identify the type of board that is being created | ||||||
| 	ProjectsConfig struct { | 	ProjectsConfig struct { | ||||||
| 		BoardType   ProjectBoardType | 		BoardType   BoardType | ||||||
| 		Translation string | 		Translation string | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// ProjectType is used to identify the type of project in question and ownership | 	// Type is used to identify the type of project in question and ownership | ||||||
| 	ProjectType uint8 | 	Type uint8 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	// ProjectTypeIndividual is a type of project board that is owned by an individual | 	// TypeIndividual is a type of project board that is owned by an individual | ||||||
| 	ProjectTypeIndividual ProjectType = iota + 1 | 	TypeIndividual Type = iota + 1 | ||||||
| 
 | 
 | ||||||
| 	// ProjectTypeRepository is a project that is tied to a repository | 	// TypeRepository is a project that is tied to a repository | ||||||
| 	ProjectTypeRepository | 	TypeRepository | ||||||
| 
 | 
 | ||||||
| 	// ProjectTypeOrganization is a project that is tied to an organisation | 	// TypeOrganization is a project that is tied to an organisation | ||||||
| 	ProjectTypeOrganization | 	TypeOrganization | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // ErrProjectNotExist represents a "ProjectNotExist" kind of error. | ||||||
|  | type ErrProjectNotExist struct { | ||||||
|  | 	ID     int64 | ||||||
|  | 	RepoID int64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsErrProjectNotExist checks if an error is a ErrProjectNotExist | ||||||
|  | func IsErrProjectNotExist(err error) bool { | ||||||
|  | 	_, ok := err.(ErrProjectNotExist) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (err ErrProjectNotExist) Error() string { | ||||||
|  | 	return fmt.Sprintf("projects does not exist [id: %d]", err.ID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error. | ||||||
|  | type ErrProjectBoardNotExist struct { | ||||||
|  | 	BoardID int64 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist | ||||||
|  | func IsErrProjectBoardNotExist(err error) bool { | ||||||
|  | 	_, ok := err.(ErrProjectBoardNotExist) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (err ErrProjectBoardNotExist) Error() string { | ||||||
|  | 	return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Project represents a project board | // Project represents a project board | ||||||
| type Project struct { | type Project struct { | ||||||
| 	ID          int64  `xorm:"pk autoincr"` | 	ID          int64  `xorm:"pk autoincr"` | ||||||
| @@ -46,8 +78,8 @@ type Project struct { | |||||||
| 	RepoID      int64  `xorm:"INDEX"` | 	RepoID      int64  `xorm:"INDEX"` | ||||||
| 	CreatorID   int64  `xorm:"NOT NULL"` | 	CreatorID   int64  `xorm:"NOT NULL"` | ||||||
| 	IsClosed    bool   `xorm:"INDEX"` | 	IsClosed    bool   `xorm:"INDEX"` | ||||||
| 	BoardType   ProjectBoardType | 	BoardType   BoardType | ||||||
| 	Type        ProjectType | 	Type        Type | ||||||
| 
 | 
 | ||||||
| 	RenderedContent string `xorm:"-"` | 	RenderedContent string `xorm:"-"` | ||||||
| 
 | 
 | ||||||
| @@ -63,37 +95,39 @@ func init() { | |||||||
| // GetProjectsConfig retrieves the types of configurations projects could have | // GetProjectsConfig retrieves the types of configurations projects could have | ||||||
| func GetProjectsConfig() []ProjectsConfig { | func GetProjectsConfig() []ProjectsConfig { | ||||||
| 	return []ProjectsConfig{ | 	return []ProjectsConfig{ | ||||||
| 		{ProjectBoardTypeNone, "repo.projects.type.none"}, | 		{BoardTypeNone, "repo.projects.type.none"}, | ||||||
| 		{ProjectBoardTypeBasicKanban, "repo.projects.type.basic_kanban"}, | 		{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"}, | ||||||
| 		{ProjectBoardTypeBugTriage, "repo.projects.type.bug_triage"}, | 		{BoardTypeBugTriage, "repo.projects.type.bug_triage"}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // IsProjectTypeValid checks if a project type is valid | // IsTypeValid checks if a project type is valid | ||||||
| func IsProjectTypeValid(p ProjectType) bool { | func IsTypeValid(p Type) bool { | ||||||
| 	switch p { | 	switch p { | ||||||
| 	case ProjectTypeRepository: | 	case TypeRepository: | ||||||
| 		return true | 		return true | ||||||
| 	default: | 	default: | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ProjectSearchOptions are options for GetProjects | // SearchOptions are options for GetProjects | ||||||
| type ProjectSearchOptions struct { | type SearchOptions struct { | ||||||
| 	RepoID   int64 | 	RepoID   int64 | ||||||
| 	Page     int | 	Page     int | ||||||
| 	IsClosed util.OptionalBool | 	IsClosed util.OptionalBool | ||||||
| 	SortType string | 	SortType string | ||||||
| 	Type     ProjectType | 	Type     Type | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetProjects returns a list of all projects that have been created in the repository | // GetProjects returns a list of all projects that have been created in the repository | ||||||
| func GetProjects(opts ProjectSearchOptions) ([]*Project, int64, error) { | func GetProjects(opts SearchOptions) ([]*Project, int64, error) { | ||||||
| 	return getProjects(db.GetEngine(db.DefaultContext), opts) | 	return GetProjectsCtx(db.DefaultContext, opts) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func getProjects(e db.Engine, opts ProjectSearchOptions) ([]*Project, int64, error) { | // GetProjectsCtx returns a list of all projects that have been created in the repository | ||||||
|  | func GetProjectsCtx(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) { | ||||||
|  | 	e := db.GetEngine(ctx) | ||||||
| 	projects := make([]*Project, 0, setting.UI.IssuePagingNum) | 	projects := make([]*Project, 0, setting.UI.IssuePagingNum) | ||||||
| 
 | 
 | ||||||
| 	var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID} | 	var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID} | ||||||
| @@ -135,11 +169,11 @@ func getProjects(e db.Engine, opts ProjectSearchOptions) ([]*Project, int64, err | |||||||
| 
 | 
 | ||||||
| // NewProject creates a new Project | // NewProject creates a new Project | ||||||
| func NewProject(p *Project) error { | func NewProject(p *Project) error { | ||||||
| 	if !IsProjectBoardTypeValid(p.BoardType) { | 	if !IsBoardTypeValid(p.BoardType) { | ||||||
| 		p.BoardType = ProjectBoardTypeNone | 		p.BoardType = BoardTypeNone | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !IsProjectTypeValid(p.Type) { | 	if !IsTypeValid(p.Type) { | ||||||
| 		return errors.New("project type is not valid") | 		return errors.New("project type is not valid") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -157,7 +191,7 @@ func NewProject(p *Project) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := createBoardsForProjectsType(db.GetEngine(ctx), p); err != nil { | 	if err := createBoardsForProjectsType(ctx, p); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -200,7 +234,7 @@ func updateRepositoryProjectCount(e db.Engine, repoID int64) error { | |||||||
| 		builder.Eq{ | 		builder.Eq{ | ||||||
| 			"`num_projects`": builder.Select("count(*)").From("`project`"). | 			"`num_projects`": builder.Select("count(*)").From("`project`"). | ||||||
| 				Where(builder.Eq{"`project`.`repo_id`": repoID}. | 				Where(builder.Eq{"`project`.`repo_id`": repoID}. | ||||||
| 					And(builder.Eq{"`project`.`type`": ProjectTypeRepository})), | 					And(builder.Eq{"`project`.`type`": TypeRepository})), | ||||||
| 		}).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { | 		}).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -209,7 +243,7 @@ func updateRepositoryProjectCount(e db.Engine, repoID int64) error { | |||||||
| 		builder.Eq{ | 		builder.Eq{ | ||||||
| 			"`num_closed_projects`": builder.Select("count(*)").From("`project`"). | 			"`num_closed_projects`": builder.Select("count(*)").From("`project`"). | ||||||
| 				Where(builder.Eq{"`project`.`repo_id`": repoID}. | 				Where(builder.Eq{"`project`.`repo_id`": repoID}. | ||||||
| 					And(builder.Eq{"`project`.`type`": ProjectTypeRepository}). | 					And(builder.Eq{"`project`.`type`": TypeRepository}). | ||||||
| 					And(builder.Eq{"`project`.`is_closed`": true})), | 					And(builder.Eq{"`project`.`is_closed`": true})), | ||||||
| 		}).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { | 		}).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -224,18 +258,17 @@ func ChangeProjectStatusByRepoIDAndID(repoID, projectID int64, isClosed bool) er | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	defer committer.Close() | 	defer committer.Close() | ||||||
| 	sess := db.GetEngine(ctx) |  | ||||||
| 
 | 
 | ||||||
| 	p := new(Project) | 	p := new(Project) | ||||||
| 
 | 
 | ||||||
| 	has, err := sess.ID(projectID).Where("repo_id = ?", repoID).Get(p) | 	has, err := db.GetEngine(ctx).ID(projectID).Where("repo_id = ?", repoID).Get(p) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} else if !has { | 	} else if !has { | ||||||
| 		return ErrProjectNotExist{ID: projectID, RepoID: repoID} | 		return ErrProjectNotExist{ID: projectID, RepoID: repoID} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := changeProjectStatus(sess, p, isClosed); err != nil { | 	if err := changeProjectStatus(ctx, p, isClosed); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -250,16 +283,17 @@ func ChangeProjectStatus(p *Project, isClosed bool) error { | |||||||
| 	} | 	} | ||||||
| 	defer committer.Close() | 	defer committer.Close() | ||||||
| 
 | 
 | ||||||
| 	if err := changeProjectStatus(db.GetEngine(ctx), p, isClosed); err != nil { | 	if err := changeProjectStatus(ctx, p, isClosed); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return committer.Commit() | 	return committer.Commit() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func changeProjectStatus(e db.Engine, p *Project, isClosed bool) error { | func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { | ||||||
| 	p.IsClosed = isClosed | 	p.IsClosed = isClosed | ||||||
| 	p.ClosedDateUnix = timeutil.TimeStampNow() | 	p.ClosedDateUnix = timeutil.TimeStampNow() | ||||||
|  | 	e := db.GetEngine(ctx) | ||||||
| 	count, err := e.ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) | 	count, err := e.ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -279,14 +313,16 @@ func DeleteProjectByID(id int64) error { | |||||||
| 	} | 	} | ||||||
| 	defer committer.Close() | 	defer committer.Close() | ||||||
| 
 | 
 | ||||||
| 	if err := deleteProjectByID(db.GetEngine(ctx), id); err != nil { | 	if err := DeleteProjectByIDCtx(ctx, id); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return committer.Commit() | 	return committer.Commit() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func deleteProjectByID(e db.Engine, id int64) error { | // DeleteProjectByIDCtx deletes a project from a repository. | ||||||
|  | func DeleteProjectByIDCtx(ctx context.Context, id int64) error { | ||||||
|  | 	e := db.GetEngine(ctx) | ||||||
| 	p, err := getProjectByID(e, id) | 	p, err := getProjectByID(e, id) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if IsErrProjectNotExist(err) { | 		if IsErrProjectNotExist(err) { | ||||||
| @@ -299,7 +335,7 @@ func deleteProjectByID(e db.Engine, id int64) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := deleteProjectBoardByProjectID(e, id); err != nil { | 	if err := deleteBoardByProjectID(e, id); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -2,7 +2,7 @@ | |||||||
| // Use of this source code is governed by a MIT-style | // Use of this source code is governed by a MIT-style | ||||||
| // license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||||
| 
 | 
 | ||||||
| package models | package project | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| @@ -14,33 +14,33 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestIsProjectTypeValid(t *testing.T) { | func TestIsProjectTypeValid(t *testing.T) { | ||||||
| 	const UnknownType ProjectType = 15 | 	const UnknownType Type = 15 | ||||||
| 
 | 
 | ||||||
| 	cases := []struct { | 	cases := []struct { | ||||||
| 		typ   ProjectType | 		typ   Type | ||||||
| 		valid bool | 		valid bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ProjectTypeIndividual, false}, | 		{TypeIndividual, false}, | ||||||
| 		{ProjectTypeRepository, true}, | 		{TypeRepository, true}, | ||||||
| 		{ProjectTypeOrganization, false}, | 		{TypeOrganization, false}, | ||||||
| 		{UnknownType, false}, | 		{UnknownType, false}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, v := range cases { | 	for _, v := range cases { | ||||||
| 		assert.Equal(t, v.valid, IsProjectTypeValid(v.typ)) | 		assert.Equal(t, v.valid, IsTypeValid(v.typ)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestGetProjects(t *testing.T) { | func TestGetProjects(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
| 
 | 
 | ||||||
| 	projects, _, err := GetProjects(ProjectSearchOptions{RepoID: 1}) | 	projects, _, err := GetProjects(SearchOptions{RepoID: 1}) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 	// 1 value for this repo exists in the fixtures | 	// 1 value for this repo exists in the fixtures | ||||||
| 	assert.Len(t, projects, 1) | 	assert.Len(t, projects, 1) | ||||||
| 
 | 
 | ||||||
| 	projects, _, err = GetProjects(ProjectSearchOptions{RepoID: 3}) | 	projects, _, err = GetProjects(SearchOptions{RepoID: 3}) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 	// 1 value for this repo exists in the fixtures | 	// 1 value for this repo exists in the fixtures | ||||||
| @@ -51,8 +51,8 @@ func TestProject(t *testing.T) { | |||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
| 
 | 
 | ||||||
| 	project := &Project{ | 	project := &Project{ | ||||||
| 		Type:        ProjectTypeRepository, | 		Type:        TypeRepository, | ||||||
| 		BoardType:   ProjectBoardTypeBasicKanban, | 		BoardType:   BoardTypeBasicKanban, | ||||||
| 		Title:       "New Project", | 		Title:       "New Project", | ||||||
| 		RepoID:      1, | 		RepoID:      1, | ||||||
| 		CreatedUnix: timeutil.TimeStampNow(), | 		CreatedUnix: timeutil.TimeStampNow(), | ||||||
| @@ -1,321 +0,0 @@ | |||||||
| // Copyright 2020 The Gitea Authors. All rights reserved. |  | ||||||
| // Use of this source code is governed by a MIT-style |  | ||||||
| // license that can be found in the LICENSE file. |  | ||||||
|  |  | ||||||
| package models |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"regexp" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" |  | ||||||
|  |  | ||||||
| 	"xorm.io/builder" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type ( |  | ||||||
| 	// ProjectBoardType is used to represent a project board type |  | ||||||
| 	ProjectBoardType uint8 |  | ||||||
|  |  | ||||||
| 	// ProjectBoardList is a list of all project boards in a repository |  | ||||||
| 	ProjectBoardList []*ProjectBoard |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	// ProjectBoardTypeNone is a project board type that has no predefined columns |  | ||||||
| 	ProjectBoardTypeNone ProjectBoardType = iota |  | ||||||
|  |  | ||||||
| 	// ProjectBoardTypeBasicKanban is a project board type that has basic predefined columns |  | ||||||
| 	ProjectBoardTypeBasicKanban |  | ||||||
|  |  | ||||||
| 	// ProjectBoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs |  | ||||||
| 	ProjectBoardTypeBugTriage |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // BoardColorPattern is a regexp witch can validate BoardColor |  | ||||||
| var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") |  | ||||||
|  |  | ||||||
| // ProjectBoard is used to represent boards on a project |  | ||||||
| type ProjectBoard struct { |  | ||||||
| 	ID      int64 `xorm:"pk autoincr"` |  | ||||||
| 	Title   string |  | ||||||
| 	Default bool   `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board |  | ||||||
| 	Sorting int8   `xorm:"NOT NULL DEFAULT 0"` |  | ||||||
| 	Color   string `xorm:"VARCHAR(7)"` |  | ||||||
|  |  | ||||||
| 	ProjectID int64 `xorm:"INDEX NOT NULL"` |  | ||||||
| 	CreatorID int64 `xorm:"NOT NULL"` |  | ||||||
|  |  | ||||||
| 	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` |  | ||||||
| 	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` |  | ||||||
|  |  | ||||||
| 	Issues []*Issue `xorm:"-"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	db.RegisterModel(new(ProjectBoard)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // IsProjectBoardTypeValid checks if the project board type is valid |  | ||||||
| func IsProjectBoardTypeValid(p ProjectBoardType) bool { |  | ||||||
| 	switch p { |  | ||||||
| 	case ProjectBoardTypeNone, ProjectBoardTypeBasicKanban, ProjectBoardTypeBugTriage: |  | ||||||
| 		return true |  | ||||||
| 	default: |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func createBoardsForProjectsType(sess db.Engine, project *Project) error { |  | ||||||
| 	var items []string |  | ||||||
|  |  | ||||||
| 	switch project.BoardType { |  | ||||||
|  |  | ||||||
| 	case ProjectBoardTypeBugTriage: |  | ||||||
| 		items = setting.Project.ProjectBoardBugTriageType |  | ||||||
|  |  | ||||||
| 	case ProjectBoardTypeBasicKanban: |  | ||||||
| 		items = setting.Project.ProjectBoardBasicKanbanType |  | ||||||
|  |  | ||||||
| 	case ProjectBoardTypeNone: |  | ||||||
| 		fallthrough |  | ||||||
| 	default: |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(items) == 0 { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	boards := make([]ProjectBoard, 0, len(items)) |  | ||||||
|  |  | ||||||
| 	for _, v := range items { |  | ||||||
| 		boards = append(boards, ProjectBoard{ |  | ||||||
| 			CreatedUnix: timeutil.TimeStampNow(), |  | ||||||
| 			CreatorID:   project.CreatorID, |  | ||||||
| 			Title:       v, |  | ||||||
| 			ProjectID:   project.ID, |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err := sess.Insert(boards) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // NewProjectBoard adds a new project board to a given project |  | ||||||
| func NewProjectBoard(board *ProjectBoard) error { |  | ||||||
| 	if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { |  | ||||||
| 		return fmt.Errorf("bad color code: %s", board.Color) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err := db.GetEngine(db.DefaultContext).Insert(board) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DeleteProjectBoardByID removes all issues references to the project board. |  | ||||||
| func DeleteProjectBoardByID(boardID int64) error { |  | ||||||
| 	ctx, committer, err := db.TxContext() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer committer.Close() |  | ||||||
|  |  | ||||||
| 	if err := deleteProjectBoardByID(db.GetEngine(ctx), boardID); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return committer.Commit() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func deleteProjectBoardByID(e db.Engine, boardID int64) error { |  | ||||||
| 	board, err := getProjectBoard(e, boardID) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if IsErrProjectBoardNotExist(err) { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err = board.removeIssues(e); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if _, err := e.ID(board.ID).Delete(board); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func deleteProjectBoardByProjectID(e db.Engine, projectID int64) error { |  | ||||||
| 	_, err := e.Where("project_id=?", projectID).Delete(&ProjectBoard{}) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetProjectBoard fetches the current board of a project |  | ||||||
| func GetProjectBoard(boardID int64) (*ProjectBoard, error) { |  | ||||||
| 	return getProjectBoard(db.GetEngine(db.DefaultContext), boardID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func getProjectBoard(e db.Engine, boardID int64) (*ProjectBoard, error) { |  | ||||||
| 	board := new(ProjectBoard) |  | ||||||
|  |  | ||||||
| 	has, err := e.ID(boardID).Get(board) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} else if !has { |  | ||||||
| 		return nil, ErrProjectBoardNotExist{BoardID: boardID} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return board, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UpdateProjectBoard updates a project board |  | ||||||
| func UpdateProjectBoard(board *ProjectBoard) error { |  | ||||||
| 	return updateProjectBoard(db.GetEngine(db.DefaultContext), board) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func updateProjectBoard(e db.Engine, board *ProjectBoard) error { |  | ||||||
| 	var fieldToUpdate []string |  | ||||||
|  |  | ||||||
| 	if board.Sorting != 0 { |  | ||||||
| 		fieldToUpdate = append(fieldToUpdate, "sorting") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if board.Title != "" { |  | ||||||
| 		fieldToUpdate = append(fieldToUpdate, "title") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { |  | ||||||
| 		return fmt.Errorf("bad color code: %s", board.Color) |  | ||||||
| 	} |  | ||||||
| 	fieldToUpdate = append(fieldToUpdate, "color") |  | ||||||
|  |  | ||||||
| 	_, err := e.ID(board.ID).Cols(fieldToUpdate...).Update(board) |  | ||||||
|  |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetProjectBoards fetches all boards related to a project |  | ||||||
| // if no default board set, first board is a temporary "Uncategorized" board |  | ||||||
| func GetProjectBoards(projectID int64) (ProjectBoardList, error) { |  | ||||||
| 	return getProjectBoards(db.GetEngine(db.DefaultContext), projectID) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func getProjectBoards(e db.Engine, projectID int64) ([]*ProjectBoard, error) { |  | ||||||
| 	boards := make([]*ProjectBoard, 0, 5) |  | ||||||
|  |  | ||||||
| 	if err := e.Where("project_id=? AND `default`=?", projectID, false).OrderBy("Sorting").Find(&boards); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	defaultB, err := getDefaultBoard(e, projectID) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return append([]*ProjectBoard{defaultB}, boards...), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // getDefaultBoard return default board and create a dummy if none exist |  | ||||||
| func getDefaultBoard(e db.Engine, projectID int64) (*ProjectBoard, error) { |  | ||||||
| 	var board ProjectBoard |  | ||||||
| 	exist, err := e.Where("project_id=? AND `default`=?", projectID, true).Get(&board) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if exist { |  | ||||||
| 		return &board, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// represents a board for issues not assigned to one |  | ||||||
| 	return &ProjectBoard{ |  | ||||||
| 		ProjectID: projectID, |  | ||||||
| 		Title:     "Uncategorized", |  | ||||||
| 		Default:   true, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetDefaultBoard represents a board for issues not assigned to one |  | ||||||
| // if boardID is 0 unset default |  | ||||||
| func SetDefaultBoard(projectID, boardID int64) error { |  | ||||||
| 	_, err := db.GetEngine(db.DefaultContext).Where(builder.Eq{ |  | ||||||
| 		"project_id": projectID, |  | ||||||
| 		"`default`":  true, |  | ||||||
| 	}).Cols("`default`").Update(&ProjectBoard{Default: false}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if boardID > 0 { |  | ||||||
| 		_, err = db.GetEngine(db.DefaultContext).ID(boardID).Where(builder.Eq{"project_id": projectID}). |  | ||||||
| 			Cols("`default`").Update(&ProjectBoard{Default: true}) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // LoadIssues load issues assigned to this board |  | ||||||
| func (b *ProjectBoard) LoadIssues() (IssueList, error) { |  | ||||||
| 	issueList := make([]*Issue, 0, 10) |  | ||||||
|  |  | ||||||
| 	if b.ID != 0 { |  | ||||||
| 		issues, err := Issues(&IssuesOptions{ |  | ||||||
| 			ProjectBoardID: b.ID, |  | ||||||
| 			ProjectID:      b.ProjectID, |  | ||||||
| 			SortType:       "project-column-sorting", |  | ||||||
| 		}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		issueList = issues |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if b.Default { |  | ||||||
| 		issues, err := Issues(&IssuesOptions{ |  | ||||||
| 			ProjectBoardID: -1, // Issues without ProjectBoardID |  | ||||||
| 			ProjectID:      b.ProjectID, |  | ||||||
| 			SortType:       "project-column-sorting", |  | ||||||
| 		}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		issueList = append(issueList, issues...) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := IssueList(issueList).LoadComments(); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	b.Issues = issueList |  | ||||||
| 	return issueList, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // LoadIssues load issues assigned to the boards |  | ||||||
| func (bs ProjectBoardList) LoadIssues() (IssueList, error) { |  | ||||||
| 	issues := make(IssueList, 0, len(bs)*10) |  | ||||||
| 	for i := range bs { |  | ||||||
| 		il, err := bs[i].LoadIssues() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		bs[i].Issues = il |  | ||||||
| 		issues = append(issues, il...) |  | ||||||
| 	} |  | ||||||
| 	return issues, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UpdateProjectBoardSorting update project board sorting |  | ||||||
| func UpdateProjectBoardSorting(bs ProjectBoardList) error { |  | ||||||
| 	for i := range bs { |  | ||||||
| 		_, err := db.GetEngine(db.DefaultContext).ID(bs[i].ID).Cols( |  | ||||||
| 			"sorting", |  | ||||||
| 		).Update(bs[i]) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @@ -1,218 +0,0 @@ | |||||||
| // Copyright 2020 The Gitea Authors. All rights reserved. |  | ||||||
| // Use of this source code is governed by a MIT-style |  | ||||||
| // license that can be found in the LICENSE file. |  | ||||||
|  |  | ||||||
| package models |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" |  | ||||||
| 	user_model "code.gitea.io/gitea/models/user" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // ProjectIssue saves relation from issue to a project |  | ||||||
| type ProjectIssue struct { |  | ||||||
| 	ID        int64 `xorm:"pk autoincr"` |  | ||||||
| 	IssueID   int64 `xorm:"INDEX"` |  | ||||||
| 	ProjectID int64 `xorm:"INDEX"` |  | ||||||
|  |  | ||||||
| 	// If 0, then it has not been added to a specific board in the project |  | ||||||
| 	ProjectBoardID int64 `xorm:"INDEX"` |  | ||||||
| 	Sorting        int64 `xorm:"NOT NULL DEFAULT 0"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	db.RegisterModel(new(ProjectIssue)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func deleteProjectIssuesByProjectID(e db.Engine, projectID int64) error { |  | ||||||
| 	_, err := e.Where("project_id=?", projectID).Delete(&ProjectIssue{}) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //  ___ |  | ||||||
| // |_ _|___ ___ _   _  ___ |  | ||||||
| //  | |/ __/ __| | | |/ _ \ |  | ||||||
| //  | |\__ \__ \ |_| |  __/ |  | ||||||
| // |___|___/___/\__,_|\___| |  | ||||||
|  |  | ||||||
| // LoadProject load the project the issue was assigned to |  | ||||||
| func (i *Issue) LoadProject() (err error) { |  | ||||||
| 	return i.loadProject(db.GetEngine(db.DefaultContext)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (i *Issue) loadProject(e db.Engine) (err error) { |  | ||||||
| 	if i.Project == nil { |  | ||||||
| 		var p Project |  | ||||||
| 		if _, err = e.Table("project"). |  | ||||||
| 			Join("INNER", "project_issue", "project.id=project_issue.project_id"). |  | ||||||
| 			Where("project_issue.issue_id = ?", i.ID). |  | ||||||
| 			Get(&p); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		i.Project = &p |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ProjectID return project id if issue was assigned to one |  | ||||||
| func (i *Issue) ProjectID() int64 { |  | ||||||
| 	return i.projectID(db.GetEngine(db.DefaultContext)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (i *Issue) projectID(e db.Engine) int64 { |  | ||||||
| 	var ip ProjectIssue |  | ||||||
| 	has, err := e.Where("issue_id=?", i.ID).Get(&ip) |  | ||||||
| 	if err != nil || !has { |  | ||||||
| 		return 0 |  | ||||||
| 	} |  | ||||||
| 	return ip.ProjectID |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ProjectBoardID return project board id if issue was assigned to one |  | ||||||
| func (i *Issue) ProjectBoardID() int64 { |  | ||||||
| 	return i.projectBoardID(db.GetEngine(db.DefaultContext)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (i *Issue) projectBoardID(e db.Engine) int64 { |  | ||||||
| 	var ip ProjectIssue |  | ||||||
| 	has, err := e.Where("issue_id=?", i.ID).Get(&ip) |  | ||||||
| 	if err != nil || !has { |  | ||||||
| 		return 0 |  | ||||||
| 	} |  | ||||||
| 	return ip.ProjectBoardID |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //  ____            _           _ |  | ||||||
| // |  _ \ _ __ ___ (_) ___  ___| |_ |  | ||||||
| // | |_) | '__/ _ \| |/ _ \/ __| __| |  | ||||||
| // |  __/| | | (_) | |  __/ (__| |_ |  | ||||||
| // |_|   |_|  \___// |\___|\___|\__| |  | ||||||
| //               |__/ |  | ||||||
|  |  | ||||||
| // NumIssues return counter of all issues assigned to a project |  | ||||||
| func (p *Project) NumIssues() int { |  | ||||||
| 	c, err := db.GetEngine(db.DefaultContext).Table("project_issue"). |  | ||||||
| 		Where("project_id=?", p.ID). |  | ||||||
| 		GroupBy("issue_id"). |  | ||||||
| 		Cols("issue_id"). |  | ||||||
| 		Count() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0 |  | ||||||
| 	} |  | ||||||
| 	return int(c) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // NumClosedIssues return counter of closed issues assigned to a project |  | ||||||
| func (p *Project) NumClosedIssues() int { |  | ||||||
| 	c, err := db.GetEngine(db.DefaultContext).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 0 |  | ||||||
| 	} |  | ||||||
| 	return int(c) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // NumOpenIssues return counter of open issues assigned to a project |  | ||||||
| func (p *Project) NumOpenIssues() int { |  | ||||||
| 	c, err := db.GetEngine(db.DefaultContext).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 0 |  | ||||||
| 	} |  | ||||||
| 	return int(c) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ChangeProjectAssign changes the project associated with an issue |  | ||||||
| func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error { |  | ||||||
| 	ctx, committer, err := db.TxContext() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer committer.Close() |  | ||||||
|  |  | ||||||
| 	if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return committer.Commit() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { |  | ||||||
| 	e := db.GetEngine(ctx) |  | ||||||
| 	oldProjectID := issue.projectID(e) |  | ||||||
|  |  | ||||||
| 	if _, err := e.Where("project_issue.issue_id=?", issue.ID).Delete(&ProjectIssue{}); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := issue.loadRepo(ctx); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if oldProjectID > 0 || newProjectID > 0 { |  | ||||||
| 		if _, err := createComment(ctx, &CreateCommentOptions{ |  | ||||||
| 			Type:         CommentTypeProject, |  | ||||||
| 			Doer:         doer, |  | ||||||
| 			Repo:         issue.Repo, |  | ||||||
| 			Issue:        issue, |  | ||||||
| 			OldProjectID: oldProjectID, |  | ||||||
| 			ProjectID:    newProjectID, |  | ||||||
| 		}); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err := e.Insert(&ProjectIssue{ |  | ||||||
| 		IssueID:   issue.ID, |  | ||||||
| 		ProjectID: newProjectID, |  | ||||||
| 	}) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //  ____            _           _   ____                      _ |  | ||||||
| // |  _ \ _ __ ___ (_) ___  ___| |_| __ )  ___   __ _ _ __ __| | |  | ||||||
| // | |_) | '__/ _ \| |/ _ \/ __| __|  _ \ / _ \ / _` | '__/ _` | |  | ||||||
| // |  __/| | | (_) | |  __/ (__| |_| |_) | (_) | (_| | | | (_| | |  | ||||||
| // |_|   |_|  \___// |\___|\___|\__|____/ \___/ \__,_|_|  \__,_| |  | ||||||
| //               |__/ |  | ||||||
|  |  | ||||||
| // MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column |  | ||||||
| func MoveIssuesOnProjectBoard(board *ProjectBoard, sortedIssueIDs map[int64]int64) error { |  | ||||||
| 	return db.WithTx(func(ctx context.Context) error { |  | ||||||
| 		sess := db.GetEngine(ctx) |  | ||||||
|  |  | ||||||
| 		issueIDs := make([]int64, 0, len(sortedIssueIDs)) |  | ||||||
| 		for _, issueID := range sortedIssueIDs { |  | ||||||
| 			issueIDs = append(issueIDs, issueID) |  | ||||||
| 		} |  | ||||||
| 		count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		if int(count) != len(sortedIssueIDs) { |  | ||||||
| 			return fmt.Errorf("all issues have to be added to a project first") |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		for sorting, issueID := range sortedIssueIDs { |  | ||||||
| 			_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (pb *ProjectBoard) removeIssues(e db.Engine) error { |  | ||||||
| 	_, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0, sorting = 0 WHERE project_board_id = ? ", pb.ID) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| @@ -21,6 +21,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	"code.gitea.io/gitea/models/perm" | 	"code.gitea.io/gitea/models/perm" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -748,14 +749,14 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	projects, _, err := getProjects(sess, ProjectSearchOptions{ | 	projects, _, err := project_model.GetProjectsCtx(ctx, project_model.SearchOptions{ | ||||||
| 		RepoID: repoID, | 		RepoID: repoID, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("get projects: %v", err) | 		return fmt.Errorf("get projects: %v", err) | ||||||
| 	} | 	} | ||||||
| 	for i := range projects { | 	for i := range projects { | ||||||
| 		if err := deleteProjectByID(sess, projects[i].ID); err != nil { | 		if err := project_model.DeleteProjectByIDCtx(ctx, projects[i].ID); err != nil { | ||||||
| 			return fmt.Errorf("delete project [%d]: %v", projects[i].ID, err) | 			return fmt.Errorf("delete project [%d]: %v", projects[i].ID, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/auth" | 	"code.gitea.io/gitea/models/auth" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/models/webhook" | 	"code.gitea.io/gitea/models/webhook" | ||||||
| @@ -106,7 +107,7 @@ func GetStatistic() (stats Statistic) { | |||||||
| 	stats.Counter.HookTask, _ = e.Count(new(webhook.HookTask)) | 	stats.Counter.HookTask, _ = e.Count(new(webhook.HookTask)) | ||||||
| 	stats.Counter.Team, _ = e.Count(new(organization.Team)) | 	stats.Counter.Team, _ = e.Count(new(organization.Team)) | ||||||
| 	stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment)) | 	stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment)) | ||||||
| 	stats.Counter.Project, _ = e.Count(new(Project)) | 	stats.Counter.Project, _ = e.Count(new(project_model.Project)) | ||||||
| 	stats.Counter.ProjectBoard, _ = e.Count(new(ProjectBoard)) | 	stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board)) | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| @@ -336,9 +337,9 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") { | 	if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") { | ||||||
| 		projects, _, err := models.GetProjects(models.ProjectSearchOptions{ | 		projects, _, err := project_model.GetProjects(project_model.SearchOptions{ | ||||||
| 			RepoID:   repo.ID, | 			RepoID:   repo.ID, | ||||||
| 			Type:     models.ProjectTypeRepository, | 			Type:     project_model.TypeRepository, | ||||||
| 			IsClosed: util.OptionalBoolOf(isShowClosed), | 			IsClosed: util.OptionalBoolOf(isShowClosed), | ||||||
| 		}) | 		}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -446,22 +447,22 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R | |||||||
| func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { | func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { | ||||||
| 	var err error | 	var err error | ||||||
|  |  | ||||||
| 	ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{ | 	ctx.Data["OpenProjects"], _, err = project_model.GetProjects(project_model.SearchOptions{ | ||||||
| 		RepoID:   repo.ID, | 		RepoID:   repo.ID, | ||||||
| 		Page:     -1, | 		Page:     -1, | ||||||
| 		IsClosed: util.OptionalBoolFalse, | 		IsClosed: util.OptionalBoolFalse, | ||||||
| 		Type:     models.ProjectTypeRepository, | 		Type:     project_model.TypeRepository, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetProjects", err) | 		ctx.ServerError("GetProjects", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{ | 	ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(project_model.SearchOptions{ | ||||||
| 		RepoID:   repo.ID, | 		RepoID:   repo.ID, | ||||||
| 		Page:     -1, | 		Page:     -1, | ||||||
| 		IsClosed: util.OptionalBoolTrue, | 		IsClosed: util.OptionalBoolTrue, | ||||||
| 		Type:     models.ProjectTypeRepository, | 		Type:     project_model.TypeRepository, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetProjects", err) | 		ctx.ServerError("GetProjects", err) | ||||||
| @@ -814,7 +815,7 @@ func NewIssue(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	projectID := ctx.FormInt64("project") | 	projectID := ctx.FormInt64("project") | ||||||
| 	if projectID > 0 { | 	if projectID > 0 { | ||||||
| 		project, err := models.GetProjectByID(projectID) | 		project, err := project_model.GetProjectByID(projectID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("GetProjectByID: %d: %v", projectID, err) | 			log.Error("GetProjectByID: %d: %v", projectID, err) | ||||||
| 		} else if project.RepoID != ctx.Repo.Repository.ID { | 		} else if project.RepoID != ctx.Repo.Repository.ID { | ||||||
| @@ -926,7 +927,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if form.ProjectID > 0 { | 	if form.ProjectID > 0 { | ||||||
| 		p, err := models.GetProjectByID(form.ProjectID) | 		p, err := project_model.GetProjectByID(form.ProjectID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| 			return nil, nil, 0, 0 | 			return nil, nil, 0, 0 | ||||||
| @@ -1413,7 +1414,7 @@ func ViewIssue(ctx *context.Context) { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			ghostProject := &models.Project{ | 			ghostProject := &project_model.Project{ | ||||||
| 				ID:    -1, | 				ID:    -1, | ||||||
| 				Title: ctx.Tr("repo.issues.deleted_project"), | 				Title: ctx.Tr("repo.issues.deleted_project"), | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/models/perm" | 	"code.gitea.io/gitea/models/perm" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| @@ -69,12 +70,12 @@ func Projects(ctx *context.Context) { | |||||||
| 		total = repo.NumClosedProjects | 		total = repo.NumClosedProjects | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	projects, count, err := models.GetProjects(models.ProjectSearchOptions{ | 	projects, count, err := project_model.GetProjects(project_model.SearchOptions{ | ||||||
| 		RepoID:   repo.ID, | 		RepoID:   repo.ID, | ||||||
| 		Page:     page, | 		Page:     page, | ||||||
| 		IsClosed: util.OptionalBoolOf(isShowClosed), | 		IsClosed: util.OptionalBoolOf(isShowClosed), | ||||||
| 		SortType: sortType, | 		SortType: sortType, | ||||||
| 		Type:     models.ProjectTypeRepository, | 		Type:     project_model.TypeRepository, | ||||||
| 	}) | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetProjects", err) | 		ctx.ServerError("GetProjects", err) | ||||||
| @@ -122,7 +123,7 @@ func Projects(ctx *context.Context) { | |||||||
| // NewProject render creating a project page | // NewProject render creating a project page | ||||||
| func NewProject(ctx *context.Context) { | func NewProject(ctx *context.Context) { | ||||||
| 	ctx.Data["Title"] = ctx.Tr("repo.projects.new") | 	ctx.Data["Title"] = ctx.Tr("repo.projects.new") | ||||||
| 	ctx.Data["ProjectTypes"] = models.GetProjectsConfig() | 	ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() | ||||||
| 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | ||||||
| 	ctx.HTML(http.StatusOK, tplProjectsNew) | 	ctx.HTML(http.StatusOK, tplProjectsNew) | ||||||
| } | } | ||||||
| @@ -134,18 +135,18 @@ func NewProjectPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	if ctx.HasError() { | 	if ctx.HasError() { | ||||||
| 		ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | 		ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | ||||||
| 		ctx.Data["ProjectTypes"] = models.GetProjectsConfig() | 		ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() | ||||||
| 		ctx.HTML(http.StatusOK, tplProjectsNew) | 		ctx.HTML(http.StatusOK, tplProjectsNew) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := models.NewProject(&models.Project{ | 	if err := project_model.NewProject(&project_model.Project{ | ||||||
| 		RepoID:      ctx.Repo.Repository.ID, | 		RepoID:      ctx.Repo.Repository.ID, | ||||||
| 		Title:       form.Title, | 		Title:       form.Title, | ||||||
| 		Description: form.Content, | 		Description: form.Content, | ||||||
| 		CreatorID:   ctx.Doer.ID, | 		CreatorID:   ctx.Doer.ID, | ||||||
| 		BoardType:   form.BoardType, | 		BoardType:   form.BoardType, | ||||||
| 		Type:        models.ProjectTypeRepository, | 		Type:        project_model.TypeRepository, | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		ctx.ServerError("NewProject", err) | 		ctx.ServerError("NewProject", err) | ||||||
| 		return | 		return | ||||||
| @@ -168,8 +169,8 @@ func ChangeProjectStatus(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
| 	id := ctx.ParamsInt64(":id") | 	id := ctx.ParamsInt64(":id") | ||||||
|  |  | ||||||
| 	if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { | 	if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", err) | 			ctx.NotFound("", err) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err) | 			ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err) | ||||||
| @@ -181,9 +182,9 @@ func ChangeProjectStatus(ctx *context.Context) { | |||||||
|  |  | ||||||
| // DeleteProject delete a project | // DeleteProject delete a project | ||||||
| func DeleteProject(ctx *context.Context) { | func DeleteProject(ctx *context.Context) { | ||||||
| 	p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	p, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -195,7 +196,7 @@ func DeleteProject(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := models.DeleteProjectByID(p.ID); err != nil { | 	if err := project_model.DeleteProjectByID(p.ID); err != nil { | ||||||
| 		ctx.Flash.Error("DeleteProjectByID: " + err.Error()) | 		ctx.Flash.Error("DeleteProjectByID: " + err.Error()) | ||||||
| 	} else { | 	} else { | ||||||
| 		ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success")) | 		ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success")) | ||||||
| @@ -212,9 +213,9 @@ func EditProject(ctx *context.Context) { | |||||||
| 	ctx.Data["PageIsEditProjects"] = true | 	ctx.Data["PageIsEditProjects"] = true | ||||||
| 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | ||||||
|  |  | ||||||
| 	p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	p, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -244,9 +245,9 @@ func EditProjectPost(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	p, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -260,7 +261,7 @@ func EditProjectPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	p.Title = form.Title | 	p.Title = form.Title | ||||||
| 	p.Description = form.Content | 	p.Description = form.Content | ||||||
| 	if err = models.UpdateProject(p); err != nil { | 	if err = project_model.UpdateProject(p); err != nil { | ||||||
| 		ctx.ServerError("UpdateProjects", err) | 		ctx.ServerError("UpdateProjects", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -271,9 +272,9 @@ func EditProjectPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| // ViewProject renders the project board for a project | // ViewProject renders the project board for a project | ||||||
| func ViewProject(ctx *context.Context) { | func ViewProject(ctx *context.Context) { | ||||||
| 	project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -285,7 +286,7 @@ func ViewProject(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	boards, err := models.GetProjectBoards(project.ID) | 	boards, err := project_model.GetBoards(project.ID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetProjectBoards", err) | 		ctx.ServerError("GetProjectBoards", err) | ||||||
| 		return | 		return | ||||||
| @@ -295,27 +296,29 @@ func ViewProject(ctx *context.Context) { | |||||||
| 		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") | 		boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	issueList, err := boards.LoadIssues() | 	issuesMap, err := models.LoadIssuesFromBoardList(boards) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("LoadIssuesOfBoards", err) | 		ctx.ServerError("LoadIssuesOfBoards", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	linkedPrsMap := make(map[int64][]*models.Issue) | 	linkedPrsMap := make(map[int64][]*models.Issue) | ||||||
| 	for _, issue := range issueList { | 	for _, issuesList := range issuesMap { | ||||||
| 		var referencedIds []int64 | 		for _, issue := range issuesList { | ||||||
| 		for _, comment := range issue.Comments { | 			var referencedIds []int64 | ||||||
| 			if comment.RefIssueID != 0 && comment.RefIsPull { | 			for _, comment := range issue.Comments { | ||||||
| 				referencedIds = append(referencedIds, comment.RefIssueID) | 				if comment.RefIssueID != 0 && comment.RefIsPull { | ||||||
|  | 					referencedIds = append(referencedIds, comment.RefIssueID) | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if len(referencedIds) > 0 { | 			if len(referencedIds) > 0 { | ||||||
| 			if linkedPrs, err := models.Issues(&models.IssuesOptions{ | 				if linkedPrs, err := models.Issues(&models.IssuesOptions{ | ||||||
| 				IssueIDs: referencedIds, | 					IssueIDs: referencedIds, | ||||||
| 				IsPull:   util.OptionalBoolTrue, | 					IsPull:   util.OptionalBoolTrue, | ||||||
| 			}); err == nil { | 				}); err == nil { | ||||||
| 				linkedPrsMap[issue.ID] = linkedPrs | 					linkedPrsMap[issue.ID] = linkedPrs | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -335,6 +338,7 @@ func ViewProject(ctx *context.Context) { | |||||||
| 	ctx.Data["IsProjectsPage"] = true | 	ctx.Data["IsProjectsPage"] = true | ||||||
| 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | ||||||
| 	ctx.Data["Project"] = project | 	ctx.Data["Project"] = project | ||||||
|  | 	ctx.Data["IssuesMap"] = issuesMap | ||||||
| 	ctx.Data["Boards"] = boards | 	ctx.Data["Boards"] = boards | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplProjectsView) | 	ctx.HTML(http.StatusOK, tplProjectsView) | ||||||
| @@ -381,9 +385,9 @@ func DeleteProjectBoard(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -391,7 +395,7 @@ func DeleteProjectBoard(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID")) | 	pb, err := project_model.GetBoard(ctx.ParamsInt64(":boardID")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetProjectBoard", err) | 		ctx.ServerError("GetProjectBoard", err) | ||||||
| 		return | 		return | ||||||
| @@ -410,7 +414,7 @@ func DeleteProjectBoard(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil { | 	if err := project_model.DeleteBoardByID(ctx.ParamsInt64(":boardID")); err != nil { | ||||||
| 		ctx.ServerError("DeleteProjectBoardByID", err) | 		ctx.ServerError("DeleteProjectBoardByID", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -430,9 +434,9 @@ func AddBoardToProjectPost(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -440,7 +444,7 @@ func AddBoardToProjectPost(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := models.NewProjectBoard(&models.ProjectBoard{ | 	if err := project_model.NewBoard(&project_model.Board{ | ||||||
| 		ProjectID: project.ID, | 		ProjectID: project.ID, | ||||||
| 		Title:     form.Title, | 		Title:     form.Title, | ||||||
| 		Color:     form.Color, | 		Color:     form.Color, | ||||||
| @@ -455,7 +459,7 @@ func AddBoardToProjectPost(ctx *context.Context) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) { | func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { | ||||||
| 	if ctx.Doer == nil { | 	if ctx.Doer == nil { | ||||||
| 		ctx.JSON(http.StatusForbidden, map[string]string{ | 		ctx.JSON(http.StatusForbidden, map[string]string{ | ||||||
| 			"message": "Only signed in users are allowed to perform this action.", | 			"message": "Only signed in users are allowed to perform this action.", | ||||||
| @@ -470,9 +474,9 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, | |||||||
| 		return nil, nil | 		return nil, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("", nil) | 			ctx.NotFound("", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -480,7 +484,7 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, | |||||||
| 		return nil, nil | 		return nil, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID")) | 	board, err := project_model.GetBoard(ctx.ParamsInt64(":boardID")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetProjectBoard", err) | 		ctx.ServerError("GetProjectBoard", err) | ||||||
| 		return nil, nil | 		return nil, nil | ||||||
| @@ -519,7 +523,7 @@ func EditProjectBoard(ctx *context.Context) { | |||||||
| 		board.Sorting = form.Sorting | 		board.Sorting = form.Sorting | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := models.UpdateProjectBoard(board); err != nil { | 	if err := project_model.UpdateBoard(board); err != nil { | ||||||
| 		ctx.ServerError("UpdateProjectBoard", err) | 		ctx.ServerError("UpdateProjectBoard", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -536,7 +540,7 @@ func SetDefaultProjectBoard(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := models.SetDefaultBoard(project.ID, board.ID); err != nil { | 	if err := project_model.SetDefaultBoard(project.ID, board.ID); err != nil { | ||||||
| 		ctx.ServerError("SetDefaultBoard", err) | 		ctx.ServerError("SetDefaultBoard", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -562,9 +566,9 @@ func MoveIssues(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) | 	project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrProjectNotExist(err) { | 		if project_model.IsErrProjectNotExist(err) { | ||||||
| 			ctx.NotFound("ProjectNotExist", nil) | 			ctx.NotFound("ProjectNotExist", nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetProjectByID", err) | 			ctx.ServerError("GetProjectByID", err) | ||||||
| @@ -576,19 +580,18 @@ func MoveIssues(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var board *models.ProjectBoard | 	var board *project_model.Board | ||||||
|  |  | ||||||
| 	if ctx.ParamsInt64(":boardID") == 0 { | 	if ctx.ParamsInt64(":boardID") == 0 { | ||||||
| 		board = &models.ProjectBoard{ | 		board = &project_model.Board{ | ||||||
| 			ID:        0, | 			ID:        0, | ||||||
| 			ProjectID: project.ID, | 			ProjectID: project.ID, | ||||||
| 			Title:     ctx.Tr("repo.projects.type.uncategorized"), | 			Title:     ctx.Tr("repo.projects.type.uncategorized"), | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		// column | 		board, err = project_model.GetBoard(ctx.ParamsInt64(":boardID")) | ||||||
| 		board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID")) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			if models.IsErrProjectBoardNotExist(err) { | 			if project_model.IsErrProjectBoardNotExist(err) { | ||||||
| 				ctx.NotFound("ProjectBoardNotExist", nil) | 				ctx.NotFound("ProjectBoardNotExist", nil) | ||||||
| 			} else { | 			} else { | ||||||
| 				ctx.ServerError("GetProjectBoard", err) | 				ctx.ServerError("GetProjectBoard", err) | ||||||
| @@ -634,7 +637,7 @@ func MoveIssues(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err = models.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { | 	if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { | ||||||
| 		ctx.ServerError("MoveIssuesOnProjectBoard", err) | 		ctx.ServerError("MoveIssuesOnProjectBoard", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -647,8 +650,43 @@ func MoveIssues(ctx *context.Context) { | |||||||
| // CreateProject renders the generic project creation page | // CreateProject renders the generic project creation page | ||||||
| func CreateProject(ctx *context.Context) { | func CreateProject(ctx *context.Context) { | ||||||
| 	ctx.Data["Title"] = ctx.Tr("repo.projects.new") | 	ctx.Data["Title"] = ctx.Tr("repo.projects.new") | ||||||
| 	ctx.Data["ProjectTypes"] = models.GetProjectsConfig() | 	ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() | ||||||
| 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplGenericProjectsNew) | 	ctx.HTML(http.StatusOK, tplGenericProjectsNew) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // CreateProjectPost creates an individual and/or organization project | ||||||
|  | func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) { | ||||||
|  | 	user := checkContextUser(ctx, form.UID) | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["ContextUser"] = user | ||||||
|  |  | ||||||
|  | 	if ctx.HasError() { | ||||||
|  | 		ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) | ||||||
|  | 		ctx.HTML(http.StatusOK, tplGenericProjectsNew) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	projectType := project_model.TypeIndividual | ||||||
|  | 	if user.IsOrganization() { | ||||||
|  | 		projectType = project_model.TypeOrganization | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := project_model.NewProject(&project_model.Project{ | ||||||
|  | 		Title:       form.Title, | ||||||
|  | 		Description: form.Content, | ||||||
|  | 		CreatorID:   user.ID, | ||||||
|  | 		BoardType:   form.BoardType, | ||||||
|  | 		Type:        projectType, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		ctx.ServerError("NewProject", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) | ||||||
|  | 	ctx.Redirect(setting.AppSubURL + "/") | ||||||
|  | } | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| @@ -216,10 +217,10 @@ func Profile(ctx *context.Context) { | |||||||
|  |  | ||||||
| 		total = int(count) | 		total = int(count) | ||||||
| 	case "projects": | 	case "projects": | ||||||
| 		ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{ | 		ctx.Data["OpenProjects"], _, err = project_model.GetProjects(project_model.SearchOptions{ | ||||||
| 			Page:     -1, | 			Page:     -1, | ||||||
| 			IsClosed: util.OptionalBoolFalse, | 			IsClosed: util.OptionalBoolFalse, | ||||||
| 			Type:     models.ProjectTypeIndividual, | 			Type:     project_model.TypeIndividual, | ||||||
| 		}) | 		}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.ServerError("GetProjects", err) | 			ctx.ServerError("GetProjects", err) | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	"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/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
| @@ -499,7 +500,7 @@ func (i IssueLockForm) HasValidReason() bool { | |||||||
| type CreateProjectForm struct { | type CreateProjectForm struct { | ||||||
| 	Title     string `binding:"Required;MaxSize(100)"` | 	Title     string `binding:"Required;MaxSize(100)"` | ||||||
| 	Content   string | 	Content   string | ||||||
| 	BoardType models.ProjectBoardType | 	BoardType project_model.BoardType | ||||||
| } | } | ||||||
|  |  | ||||||
| // UserCreateProjectForm is a from for creating an individual or organization | // UserCreateProjectForm is a from for creating an individual or organization | ||||||
| @@ -507,7 +508,7 @@ type CreateProjectForm struct { | |||||||
| type UserCreateProjectForm struct { | type UserCreateProjectForm struct { | ||||||
| 	Title     string `binding:"Required;MaxSize(100)"` | 	Title     string `binding:"Required;MaxSize(100)"` | ||||||
| 	Content   string | 	Content   string | ||||||
| 	BoardType models.ProjectBoardType | 	BoardType project_model.BoardType | ||||||
| 	UID       int64 `binding:"Required"` | 	UID       int64 `binding:"Required"` | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -84,7 +84,7 @@ | |||||||
| 				<div class="board-column-header df ac sb"> | 				<div class="board-column-header df ac sb"> | ||||||
| 					<div class="ui large label board-label py-2"> | 					<div class="ui large label board-label py-2"> | ||||||
| 						<div class="ui small circular grey label board-card-cnt"> | 						<div class="ui small circular grey label board-card-cnt"> | ||||||
| 							{{len .Issues}} | 							{{.NumIssues}} | ||||||
| 						</div> | 						</div> | ||||||
| 						{{.Title}} | 						{{.Title}} | ||||||
| 					</div> | 					</div> | ||||||
| @@ -175,7 +175,7 @@ | |||||||
|  |  | ||||||
| 				<div class="ui cards board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> | 				<div class="ui cards board" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> | ||||||
|  |  | ||||||
| 					{{ range .Issues }} | 					{{ range (index $.IssuesMap .ID) }} | ||||||
|  |  | ||||||
| 					<!-- start issue card --> | 					<!-- start issue card --> | ||||||
| 					<div class="card board-card" data-issue="{{.ID}}"> | 					<div class="card board-card" data-issue="{{.ID}}"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user