mirror of https://github.com/go-gitea/gitea
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>pull/19255/head^2
parent
ea6efba9b3
commit
bd97736b9c
@ -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() |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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", |
||||
) |
||||
} |
@ -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 |
||||
} |
Loading…
Reference in new issue