{{.locale.Tr "repo.projects.deletion_desc"}}
+{{.locale.Tr "repo.boards.deletion_desc"}}
diff --git a/models/activities/statistic.go b/models/activities/statistic.go index 138f4d8fe9e92..c6820c849d950 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -6,11 +6,11 @@ package activities import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" @@ -26,8 +26,8 @@ type Statistic struct { Comment, Oauth, Follow, Mirror, Release, AuthSource, Webhook, Milestone, Label, HookTask, - Team, UpdateTask, Project, - ProjectBoard, Attachment int64 + Team, UpdateTask, Board, + BoardColumn, Attachment int64 IssueByLabel []IssueByLabelCount IssueByRepository []IssueByRepositoryCount } @@ -108,7 +108,7 @@ func GetStatistic() (stats Statistic) { stats.Counter.HookTask, _ = e.Count(new(webhook.HookTask)) stats.Counter.Team, _ = e.Count(new(organization.Team)) stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment)) - stats.Counter.Project, _ = e.Count(new(project_model.Project)) - stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board)) + stats.Counter.Board, _ = e.Count(new(board_model.Board)) + stats.Counter.BoardColumn, _ = e.Count(new(board_model.Board)) return stats } diff --git a/models/board/board.go b/models/board/board.go new file mode 100644 index 0000000000000..428d8d24d22c6 --- /dev/null +++ b/models/board/board.go @@ -0,0 +1,369 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package board + +import ( + "context" + "errors" + "fmt" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +type ( + // BoardsConfig is used to identify the type of board that is being created + BoardsConfig struct { + ColumnType ColumnType + Translation string + } + + // Type is used to identify the type of board in question and ownership + Type uint8 + + // ColumnType is used to represent a board board type + ColumnType uint8 +) + +const ( + // TypeIndividual is a type of board board that is owned by an individual + TypeIndividual Type = iota + 1 + + // TypeRepository is a board that is tied to a repository + TypeRepository + + // TypeOrganization is a board that is tied to an organisation + TypeOrganization +) + +const ( + // BoardTypeNone is a board type that has no predefined columns + BoardTypeNone ColumnType = iota + + // ColumnTypeBasicKanban is a board board type that has basic predefined columns + BoardTypeBasicKanban + + // ColumnTypeBugTriage is a board board type that has predefined columns suited to hunting down bugs + BoardTypeBugTriage +) + +// IsBoardTypeValid checks if the board board type is valid +func IsBoardTypeValid(p ColumnType) bool { + switch p { + case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage: + return true + default: + return false + } +} + +// ErrBoardNotExist represents a "BoardNotExist" kind of error. +type ErrBoardNotExist struct { + ID int64 + RepoID int64 +} + +// IsErrBoardNotExist checks if an error is a ErrBoardNotExist +func IsErrBoardNotExist(err error) bool { + _, ok := err.(ErrBoardNotExist) + return ok +} + +func (err ErrBoardNotExist) Error() string { + return fmt.Sprintf("board does not exist [id: %d]", err.ID) +} + +func (err ErrBoardNotExist) Unwrap() error { + return util.ErrNotExist +} + +// Board represents a board +type Board struct { + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"INDEX NOT NULL"` + Description string `xorm:"TEXT"` + RepoID int64 `xorm:"INDEX"` + CreatorID int64 `xorm:"NOT NULL"` + IsClosed bool `xorm:"INDEX"` + ColumnType ColumnType + Type Type + + RenderedContent string `xorm:"-"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + ClosedDateUnix timeutil.TimeStamp +} + +func init() { + db.RegisterModel(new(Board)) +} + +// GetBoardsConfig retrieves the types of configurations boards could have +func GetBoardsConfig() []BoardsConfig { + return []BoardsConfig{ + {BoardTypeNone, "repo.boards.type.none"}, + {BoardTypeBasicKanban, "repo.boards.type.basic_kanban"}, + {BoardTypeBugTriage, "repo.boards.type.bug_triage"}, + } +} + +// IsTypeValid checks if a board type is valid +func IsTypeValid(p Type) bool { + switch p { + case TypeRepository: + return true + default: + return false + } +} + +// SearchOptions are options for FindBoards +type SearchOptions struct { + RepoID int64 + Page int + IsClosed util.OptionalBool + SortType string + Type Type +} + +// FindBoards returns a list of all boards that have been created in the repository +func FindBoards(ctx context.Context, opts SearchOptions) ([]*Board, int64, error) { + e := db.GetEngine(ctx) + boards := make([]*Board, 0, setting.UI.IssuePagingNum) + + var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID} + switch opts.IsClosed { + case util.OptionalBoolTrue: + cond = cond.And(builder.Eq{"is_closed": true}) + case util.OptionalBoolFalse: + cond = cond.And(builder.Eq{"is_closed": false}) + } + + if opts.Type > 0 { + cond = cond.And(builder.Eq{"type": opts.Type}) + } + + count, err := e.Where(cond).Count(new(Board)) + if err != nil { + return nil, 0, fmt.Errorf("Count: %w", err) + } + + e = e.Where(cond) + + if opts.Page > 0 { + e = e.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum) + } + + switch opts.SortType { + case "oldest": + e.Desc("created_unix") + case "recentupdate": + e.Desc("updated_unix") + case "leastupdate": + e.Asc("updated_unix") + default: + e.Asc("created_unix") + } + + return boards, count, e.Find(&boards) +} + +// NewBoard creates a new board +func NewBoard(p *Board) error { + if !IsBoardTypeValid(p.ColumnType) { + p.ColumnType = BoardTypeNone + } + + if !IsTypeValid(p.Type) { + return errors.New("board type is not valid") + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err := db.Insert(ctx, p); err != nil { + return err + } + + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_boards = num_boards + 1 WHERE id = ?", p.RepoID); err != nil { + return err + } + + if err := createColumnsForBoardType(ctx, p); err != nil { + return err + } + + return committer.Commit() +} + +// GetBoardByID returns the borad in a repository +func GetBoardByID(ctx context.Context, id int64) (*Board, error) { + p := new(Board) + + has, err := db.GetEngine(ctx).ID(id).Get(p) + if err != nil { + return nil, err + } else if !has { + return nil, ErrBoardNotExist{ID: id} + } + + return p, nil +} + +// UpdateBoard updates board properties +func UpdateBoard(ctx context.Context, p *Board) error { + _, err := db.GetEngine(ctx).ID(p.ID).Cols( + "title", + "description", + ).Update(p) + return err +} + +func updateRepositoryBoardCount(ctx context.Context, repoID int64) error { + if _, err := db.GetEngine(ctx).Exec(builder.Update( + builder.Eq{ + "`num_boards`": builder.Select("count(*)").From("`board`"). + Where(builder.Eq{"`board`.`repo_id`": repoID}. + And(builder.Eq{"`board`.`type`": TypeRepository})), + }).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { + return err + } + + if _, err := db.GetEngine(ctx).Exec(builder.Update( + builder.Eq{ + "`num_closed_boards`": builder.Select("count(*)").From("`board`"). + Where(builder.Eq{"`board`.`repo_id`": repoID}. + And(builder.Eq{"`board`.`type`": TypeRepository}). + And(builder.Eq{"`board`.`is_closed`": true})), + }).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { + return err + } + return nil +} + +// ChangeBoardStatusByRepoIDAndID toggles a board between opened and closed +func ChangeBoardStatusByRepoIDAndID(repoID, boardID int64, isClosed bool) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + p := new(Board) + + has, err := db.GetEngine(ctx).ID(boardID).Where("repo_id = ?", repoID).Get(p) + if err != nil { + return err + } else if !has { + return ErrBoardNotExist{ID: boardID, RepoID: repoID} + } + + if err := changeBoardStatus(ctx, p, isClosed); err != nil { + return err + } + + return committer.Commit() +} + +// ChangeBoardStatus toggle a board between opened and closed +func ChangeBoardStatus(p *Board, isClosed bool) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err := changeBoardStatus(ctx, p, isClosed); err != nil { + return err + } + + return committer.Commit() +} + +func changeBoardStatus(ctx context.Context, p *Board, isClosed bool) error { + p.IsClosed = isClosed + p.ClosedDateUnix = timeutil.TimeStampNow() + count, err := db.GetEngine(ctx).ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) + if err != nil { + return err + } + if count < 1 { + return nil + } + + return updateRepositoryBoardCount(ctx, p.RepoID) +} + +// DeleteBoardByID deletes a board from a repository. if it's not in a database +// transaction, it will start a new database transaction +func DeleteBoardByID(ctx context.Context, id int64) error { + return db.AutoTx(ctx, func(ctx context.Context) error { + p, err := GetBoardByID(ctx, id) + if err != nil { + if IsErrBoardNotExist(err) { + return nil + } + return err + } + + if err := deleteBoardIssuesByBoardID(ctx, id); err != nil { + return err + } + + if err := deleteColumnsByBoardID(ctx, id); err != nil { + return err + } + + if _, err = db.GetEngine(ctx).ID(p.ID).Delete(new(Board)); err != nil { + return err + } + + return updateRepositoryBoardCount(ctx, p.RepoID) + }) +} + +func DeleteBoardByRepoID(ctx context.Context, repoID int64) error { + switch { + case setting.Database.UseSQLite3: + if _, err := db.GetEngine(ctx).Exec("DELETE FROM board_issue WHERE board_issue.id IN (SELECT board_issue.id FROM board_issue INNER JOIN board WHERE board.id = board_issue.board_id AND board.repo_id = ?)", repoID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Exec("DELETE FROM board_column WHERE board_column.id IN (SELECT board_column.id FROM board_column INNER JOIN board WHERE board.id = board_column.board_id AND board.repo_id = ?)", repoID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Table("board").Where("repo_id = ? ", repoID).Delete(&Board{}); err != nil { + return err + } + case setting.Database.UsePostgreSQL: + if _, err := db.GetEngine(ctx).Exec("DELETE FROM board_issue USING board WHERE board.id = board_issue.board_id AND board.repo_id = ? ", repoID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Exec("DELETE FROM board_column USING board WHERE board.id = board_column.board_id AND board.repo_id = ? ", repoID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Table("board").Where("repo_id = ? ", repoID).Delete(&Board{}); err != nil { + return err + } + default: + if _, err := db.GetEngine(ctx).Exec("DELETE board_issue FROM board_issue INNER JOIN board ON board.id = board_issue.board_id WHERE board.repo_id = ? ", repoID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Exec("DELETE board_column FROM board_column INNER JOIN board ON board.id = board_column.board_id WHERE board.repo_id = ? ", repoID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Table("board").Where("repo_id = ? ", repoID).Delete(&Board{}); err != nil { + return err + } + } + + return updateRepositoryBoardCount(ctx, repoID) +} diff --git a/models/project/project_test.go b/models/board/board_test.go similarity index 53% rename from models/project/project_test.go rename to models/board/board_test.go index 4fde0fc7ce3a4..5f26235d327f6 100644 --- a/models/project/project_test.go +++ b/models/board/board_test.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package project +package board import ( "testing" @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIsProjectTypeValid(t *testing.T) { +func TestIsBoardTypeValid(t *testing.T) { const UnknownType Type = 15 cases := []struct { @@ -31,53 +31,53 @@ func TestIsProjectTypeValid(t *testing.T) { } } -func TestGetProjects(t *testing.T) { +func TestFindBoards(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - projects, _, err := GetProjects(db.DefaultContext, SearchOptions{RepoID: 1}) + boards, _, err := FindBoards(db.DefaultContext, SearchOptions{RepoID: 1}) assert.NoError(t, err) // 1 value for this repo exists in the fixtures - assert.Len(t, projects, 1) + assert.Len(t, boards, 1) - projects, _, err = GetProjects(db.DefaultContext, SearchOptions{RepoID: 3}) + boards, _, err = FindBoards(db.DefaultContext, SearchOptions{RepoID: 3}) assert.NoError(t, err) // 1 value for this repo exists in the fixtures - assert.Len(t, projects, 1) + assert.Len(t, boards, 1) } -func TestProject(t *testing.T) { +func TestBoard(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - project := &Project{ + board := &Board{ Type: TypeRepository, - BoardType: BoardTypeBasicKanban, - Title: "New Project", + ColumnType: BoardTypeBasicKanban, + Title: "New Board", RepoID: 1, CreatedUnix: timeutil.TimeStampNow(), CreatorID: 2, } - assert.NoError(t, NewProject(project)) + assert.NoError(t, NewBoard(board)) - _, err := GetProjectByID(db.DefaultContext, project.ID) + _, err := GetBoardByID(db.DefaultContext, board.ID) assert.NoError(t, err) - // Update project - project.Title = "Updated title" - assert.NoError(t, UpdateProject(db.DefaultContext, project)) + // Update board + board.Title = "Updated title" + assert.NoError(t, UpdateBoard(db.DefaultContext, board)) - projectFromDB, err := GetProjectByID(db.DefaultContext, project.ID) + boardFromDB, err := GetBoardByID(db.DefaultContext, board.ID) assert.NoError(t, err) - assert.Equal(t, project.Title, projectFromDB.Title) + assert.Equal(t, board.Title, boardFromDB.Title) - assert.NoError(t, ChangeProjectStatus(project, true)) + assert.NoError(t, ChangeBoardStatus(board, true)) // Retrieve from DB afresh to check if it is truly closed - projectFromDB, err = GetProjectByID(db.DefaultContext, project.ID) + boardFromDB, err = GetBoardByID(db.DefaultContext, board.ID) assert.NoError(t, err) - assert.True(t, projectFromDB.IsClosed) + assert.True(t, boardFromDB.IsClosed) } diff --git a/models/board/column.go b/models/board/column.go new file mode 100644 index 0000000000000..8e6bf8a68b446 --- /dev/null +++ b/models/board/column.go @@ -0,0 +1,269 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package board + +import ( + "context" + "fmt" + "regexp" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// ColumnColorPattern is a regexp witch can validate BoardColor +var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") + +// ErrColumnNotExist represents a "ColumnNotExist" kind of error. +type ErrColumnNotExist struct { + ColumnID int64 +} + +// IsErrColumnNotExist checks if an error is a ErrColumnNotExist +func IsErrColumnNotExist(err error) bool { + _, ok := err.(ErrColumnNotExist) + return ok +} + +func (err ErrColumnNotExist) Error() string { + return fmt.Sprintf("board column does not exist [id: %d]", err.ColumnID) +} + +func (err ErrColumnNotExist) Unwrap() error { + return util.ErrNotExist +} + +// Column is used to represent columns on a board +type Column 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)"` + + BoardID 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 (Column) TableName() string { + return "board_column" +} + +// NumIssues return counter of all issues assigned to the board +func (b *Column) NumIssues() int { + c, err := db.GetEngine(db.DefaultContext).Table("board_issue"). + Where("board_id=?", b.BoardID). + And("board_column_id=?", b.ID). + GroupBy("issue_id"). + Cols("issue_id"). + Count() + if err != nil { + return 0 + } + return int(c) +} + +func init() { + db.RegisterModel(new(Column)) +} + +func createColumnsForBoardType(ctx context.Context, board *Board) error { + var items []string + + switch board.ColumnType { + + case BoardTypeBugTriage: + items = setting.Board.ProjectBoardBugTriageType + + case BoardTypeBasicKanban: + items = setting.Board.ProjectBoardBasicKanbanType + + case BoardTypeNone: + fallthrough + default: + return nil + } + + if len(items) == 0 { + return nil + } + + columns := make([]Column, 0, len(items)) + + for _, v := range items { + columns = append(columns, Column{ + CreatedUnix: timeutil.TimeStampNow(), + CreatorID: board.CreatorID, + Title: v, + ID: board.ID, + }) + } + + return db.Insert(ctx, columns) +} + +// NewColumn adds a new board column to a given board +func NewColumn(board *Column) error { + if len(board.Color) != 0 && !ColumnColorPattern.MatchString(board.Color) { + return fmt.Errorf("bad color code: %s", board.Color) + } + + _, err := db.GetEngine(db.DefaultContext).Insert(board) + return err +} + +// DeleteColumnByID removes all issues references to the board column. +func DeleteColumnByID(boardID int64) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if err := deleteColumnByID(ctx, boardID); err != nil { + return err + } + + return committer.Commit() +} + +func deleteColumnByID(ctx context.Context, columnID int64) error { + column, err := GetColumn(ctx, columnID) + if err != nil { + if IsErrColumnNotExist(err) { + return nil + } + + return err + } + + if err = column.removeIssues(ctx); err != nil { + return err + } + + if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil { + return err + } + return nil +} + +func deleteColumnsByBoardID(ctx context.Context, boardID int64) error { + _, err := db.GetEngine(ctx).Where("board_id=?", boardID).Delete(&Column{}) + return err +} + +// GetColumn fetches the current column of a board +func GetColumn(ctx context.Context, columnID int64) (*Column, error) { + column := new(Column) + + has, err := db.GetEngine(ctx).ID(columnID).Get(column) + if err != nil { + return nil, err + } else if !has { + return nil, ErrColumnNotExist{ColumnID: columnID} + } + + return column, nil +} + +// UpdateColumn updates a board column +func UpdateColumn(ctx context.Context, column *Column) error { + var fieldToUpdate []string + + if column.Sorting != 0 { + fieldToUpdate = append(fieldToUpdate, "sorting") + } + + if column.Title != "" { + fieldToUpdate = append(fieldToUpdate, "title") + } + + if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) { + return fmt.Errorf("bad color code: %s", column.Color) + } + fieldToUpdate = append(fieldToUpdate, "color") + + _, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column) + + return err +} + +// ColumnList is a list of all board columns in a repository +type ColumnList []*Column + +// FindColumns fetches all columns related to a board +// if no default board set, first board is a temporary "Uncategorized" board +func FindColumns(ctx context.Context, boardID int64) (ColumnList, error) { + columns := make([]*Column, 0, 5) + + if err := db.GetEngine(ctx).Where("board_id=? AND `default`=?", boardID, false).OrderBy("Sorting").Find(&columns); err != nil { + return nil, err + } + + defaultB, err := getDefaultColumn(ctx, boardID) + if err != nil { + return nil, err + } + + return append([]*Column{defaultB}, columns...), nil +} + +// getDefaultColumn return default column and create a dummy if none exist +func getDefaultColumn(ctx context.Context, boardID int64) (*Column, error) { + var board Column + exist, err := db.GetEngine(ctx).Where("board_id=? AND `default`=?", boardID, true).Get(&board) + if err != nil { + return nil, err + } + if exist { + return &board, nil + } + + // represents a board for issues not assigned to one + return &Column{ + BoardID: boardID, + Title: "Uncategorized", + Default: true, + }, nil +} + +// SetDefaultColumn represents a board for issues not assigned to one +// if boardID is 0 unset default +func SetDefaultColumn(boardID, columnID int64) error { + _, err := db.GetEngine(db.DefaultContext).Where(builder.Eq{ + "board_id": boardID, + "`default`": true, + }).Cols("`default`").Update(&Column{Default: false}) + if err != nil { + return err + } + + if boardID > 0 { + _, err = db.GetEngine(db.DefaultContext).ID(boardID).Where(builder.Eq{"board_id": boardID}). + Cols("`default`").Update(&Column{Default: true}) + } + + return err +} + +// UpdateColumnSorting update board column sorting +func UpdateColumnSorting(bs ColumnList) 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 +} diff --git a/models/board/issue.go b/models/board/issue.go new file mode 100644 index 0000000000000..d219c63cae436 --- /dev/null +++ b/models/board/issue.go @@ -0,0 +1,108 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package board + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" +) + +// BoardIssue saves relation from issue to a board +type BoardIssue struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX"` + BoardID int64 `xorm:"INDEX"` + + // If 0, then it has not been added to a specific board in the board + BoardColumnID int64 `xorm:"INDEX"` + + // the sorting order on the board + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` +} + +func init() { + db.RegisterModel(new(BoardIssue)) +} + +func deleteBoardIssuesByBoardID(ctx context.Context, boardID int64) error { + _, err := db.GetEngine(ctx).Where("board_id=?", boardID).Delete(&BoardIssue{}) + return err +} + +// NumIssues return counter of all issues assigned to a board +func (p *Board) NumIssues() int { + c, err := db.GetEngine(db.DefaultContext).Table("board_issue"). + Where("board_id=?", p.ID). + GroupBy("issue_id"). + Cols("issue_id"). + Count() + if err != nil { + log.Error("NumIssues: %v", err) + return 0 + } + return int(c) +} + +// NumClosedIssues return counter of closed issues assigned to a board +func (p *Board) NumClosedIssues() int { + c, err := db.GetEngine(db.DefaultContext).Table("board_issue"). + Join("INNER", "issue", "board_issue.issue_id=issue.id"). + Where("board_issue.board_id=? AND issue.is_closed=?", p.ID, true). + Cols("issue_id"). + Count() + if err != nil { + log.Error("NumClosedIssues: %v", err) + return 0 + } + return int(c) +} + +// NumOpenIssues return counter of open issues assigned to a board +func (p *Board) NumOpenIssues() int { + c, err := db.GetEngine(db.DefaultContext).Table("board_issue"). + Join("INNER", "issue", "board_issue.issue_id=issue.id"). + Where("board_issue.board_id=? AND issue.is_closed=?", p.ID, false). + Cols("issue_id"). + Count() + if err != nil { + log.Error("NumOpenIssues: %v", err) + return 0 + } + return int(c) +} + +// MoveIssuesOnBoardColumn moves or keeps issues in a column and sorts them inside that column +func MoveIssuesOnBoardColumn(column *Column, sortedIssueIDs map[int64]int64) error { + return db.WithTx(db.DefaultContext, 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(BoardIssue)).Where("board_id=?", column.BoardID).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 board first") + } + + for sorting, issueID := range sortedIssueIDs { + _, err = sess.Exec("UPDATE `board_issue` SET board_column_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) + if err != nil { + return err + } + } + return nil + }) +} + +func (b *Column) removeIssues(ctx context.Context) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `board_issue` SET board_column_id = 0 WHERE board_column_id = ? ", b.ID) + return err +} diff --git a/models/project/main_test.go b/models/board/main_test.go similarity index 82% rename from models/project/main_test.go rename to models/board/main_test.go index 816cbeb94a045..588c888d43858 100644 --- a/models/project/main_test.go +++ b/models/board/main_test.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package project +package board import ( "path/filepath" @@ -16,9 +16,9 @@ func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), FixtureFiles: []string{ - "project.yml", - "project_board.yml", - "project_issue.yml", + "board.yml", + "board_column.yml", + "board_issue.yml", "repository.yml", }, }) diff --git a/models/fixtures/project.yml b/models/fixtures/board.yml similarity index 72% rename from models/fixtures/project.yml rename to models/fixtures/board.yml index 3d42597c5e8ac..63cf9599b5a15 100644 --- a/models/fixtures/project.yml +++ b/models/fixtures/board.yml @@ -1,6 +1,6 @@ - id: 1 - title: First project + title: First board repo_id: 1 is_closed: false creator_id: 2 @@ -9,7 +9,7 @@ - id: 2 - title: second project + title: second board repo_id: 3 is_closed: false creator_id: 3 @@ -18,7 +18,7 @@ - id: 3 - title: project on repo with disabled project + title: board on repo with disabled board repo_id: 4 is_closed: true creator_id: 5 diff --git a/models/fixtures/project_board.yml b/models/fixtures/board_column.yml similarity index 85% rename from models/fixtures/project_board.yml rename to models/fixtures/board_column.yml index 9e06e8c23960b..558be3c0fbdf6 100644 --- a/models/fixtures/project_board.yml +++ b/models/fixtures/board_column.yml @@ -1,6 +1,6 @@ - id: 1 - project_id: 1 + board_id: 1 title: To Do creator_id: 2 created_unix: 1588117528 @@ -8,7 +8,7 @@ - id: 2 - project_id: 1 + board_id: 1 title: In Progress creator_id: 2 created_unix: 1588117528 @@ -16,7 +16,7 @@ - id: 3 - project_id: 1 + board_id: 1 title: Done creator_id: 2 created_unix: 1588117528 diff --git a/models/fixtures/board_issue.yml b/models/fixtures/board_issue.yml new file mode 100644 index 0000000000000..ab9254eb85269 --- /dev/null +++ b/models/fixtures/board_issue.yml @@ -0,0 +1,23 @@ +- + id: 1 + issue_id: 1 + board_id: 1 + board_column_id: 1 + +- + id: 2 + issue_id: 2 + board_id: 1 + board_column_id: 0 # no board assigned + +- + id: 3 + issue_id: 3 + board_id: 1 + board_column_id: 2 + +- + id: 4 + issue_id: 5 + board_id: 1 + board_column_id: 3 diff --git a/models/fixtures/project_issue.yml b/models/fixtures/project_issue.yml deleted file mode 100644 index b1af05908aafb..0000000000000 --- a/models/fixtures/project_issue.yml +++ /dev/null @@ -1,23 +0,0 @@ -- - id: 1 - issue_id: 1 - project_id: 1 - project_board_id: 1 - -- - id: 2 - issue_id: 2 - project_id: 1 - project_board_id: 0 # no board assigned - -- - id: 3 - issue_id: 3 - project_id: 1 - project_board_id: 2 - -- - id: 4 - issue_id: 5 - project_id: 1 - project_board_id: 3 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 19b243fb4e4e5..9e3d4e7f07684 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -13,8 +13,8 @@ num_closed_pulls: 0 num_milestones: 3 num_closed_milestones: 1 - num_projects: 1 - num_closed_projects: 0 + num_boards: 1 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -43,8 +43,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -73,8 +73,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 1 - num_closed_projects: 0 + num_boards: 1 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -103,8 +103,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 1 + num_boards: 0 + num_closed_boards: 1 is_private: false is_empty: false is_archived: false @@ -133,8 +133,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -163,8 +163,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -193,8 +193,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -223,8 +223,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -253,8 +253,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -283,8 +283,8 @@ num_closed_pulls: 0 num_milestones: 1 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -313,8 +313,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -343,8 +343,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -373,8 +373,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -404,8 +404,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -434,8 +434,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -464,8 +464,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -494,8 +494,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -524,8 +524,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -554,8 +554,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -584,8 +584,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -614,8 +614,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -644,8 +644,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -674,8 +674,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -704,8 +704,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -734,8 +734,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -764,8 +764,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -794,8 +794,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -824,8 +824,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -854,8 +854,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -884,8 +884,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -914,8 +914,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -944,8 +944,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -974,8 +974,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1004,8 +1004,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -1034,8 +1034,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -1064,8 +1064,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1094,8 +1094,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1124,8 +1124,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1154,8 +1154,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -1184,8 +1184,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1214,8 +1214,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: false is_archived: false @@ -1244,8 +1244,8 @@ num_closed_pulls: 0 num_milestones: 1 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1274,8 +1274,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -1304,8 +1304,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1334,8 +1334,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: true is_archived: false @@ -1364,8 +1364,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1394,8 +1394,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1424,8 +1424,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1454,8 +1454,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1484,8 +1484,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: false @@ -1514,8 +1514,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: false is_empty: false is_archived: true @@ -1544,8 +1544,8 @@ num_closed_pulls: 0 num_milestones: 0 num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 is_private: true is_empty: true is_archived: false @@ -1575,8 +1575,8 @@ num_milestones: 0 num_closed_milestones: 0 num_watches: 0 - num_projects: 0 - num_closed_projects: 0 + num_boards: 0 + num_closed_boards: 0 status: 0 is_fork: false fork_id: 0 diff --git a/models/issues/comment.go b/models/issues/comment.go index 612f17aa5af68..790f91eb95cea 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -13,10 +13,10 @@ import ( "strings" "unicode/utf8" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -122,10 +122,10 @@ const ( CommentTypeMergePull // 29 push to PR head branch CommentTypePullRequestPush - // 30 Project changed - CommentTypeProject - // 31 Project board changed - CommentTypeProjectBoard + // 30 Board changed + CommentTypeBoard + // 31 Board column changed + CommentTypeBoardColumn // 32 Dismiss Review CommentTypeDismissReview // 33 Change issue ref @@ -229,10 +229,10 @@ type Comment struct { Label *Label `xorm:"-"` AddedLabels []*Label `xorm:"-"` RemovedLabels []*Label `xorm:"-"` - OldProjectID int64 - ProjectID int64 - OldProject *project_model.Project `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OldBoardID int64 + BoardID int64 + OldBoard *board_model.Board `xorm:"-"` + Board *board_model.Board `xorm:"-"` OldMilestoneID int64 MilestoneID int64 OldMilestone *Milestone `xorm:"-"` @@ -490,25 +490,25 @@ func (c *Comment) LoadLabel() error { return nil } -// LoadProject if comment.Type is CommentTypeProject, then load project. -func (c *Comment) LoadProject() error { - if c.OldProjectID > 0 { - var oldProject project_model.Project - has, err := db.GetEngine(db.DefaultContext).ID(c.OldProjectID).Get(&oldProject) +// LoadBoard if comment.Type is CommentTypeBoard, then load boards. +func (c *Comment) LoadBoard() error { + if c.OldBoardID > 0 { + var oldBoard board_model.Board + has, err := db.GetEngine(db.DefaultContext).ID(c.OldBoardID).Get(&oldBoard) if err != nil { return err } else if has { - c.OldProject = &oldProject + c.OldBoard = &oldBoard } } - if c.ProjectID > 0 { - var project project_model.Project - has, err := db.GetEngine(db.DefaultContext).ID(c.ProjectID).Get(&project) + if c.BoardID > 0 { + var board board_model.Board + has, err := db.GetEngine(db.DefaultContext).ID(c.BoardID).Get(&board) if err != nil { return err } else if has { - c.Project = &project + c.Board = &board } } @@ -795,8 +795,8 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, LabelID: LabelID, OldMilestoneID: opts.OldMilestoneID, MilestoneID: opts.MilestoneID, - OldProjectID: opts.OldProjectID, - ProjectID: opts.ProjectID, + OldBoardID: opts.OldBoardID, + BoardID: opts.BoardID, TimeID: opts.TimeID, RemovedAssignee: opts.RemovedAssignee, AssigneeID: opts.AssigneeID, @@ -966,8 +966,8 @@ type CreateCommentOptions struct { DependentIssueID int64 OldMilestoneID int64 MilestoneID int64 - OldProjectID int64 - ProjectID int64 + OldBoardID int64 + BoardID int64 TimeID int64 AssigneeID int64 AssigneeTeamID int64 diff --git a/models/issues/issue.go b/models/issues/issue.go index f45e635c0ecea..14b957001e2ba 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -11,11 +11,11 @@ import ( "sort" "strings" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" @@ -110,14 +110,14 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` - Labels []*Label `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent string `xorm:"-"` + Labels []*Label `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` + Board *board_model.Board `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` @@ -347,7 +347,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { return } - if err = issue.loadProject(ctx); err != nil { + if err = issue.loadBoard(ctx); err != nil { return } @@ -1121,8 +1121,8 @@ type IssuesOptions struct { //nolint ReviewRequestedID int64 SubscriberID int64 MilestoneIDs []int64 - ProjectID int64 - ProjectBoardID int64 + BoardID int64 + BoardColumnID int64 IsClosed util.OptionalBool IsPull util.OptionalBool LabelIDs []int64 @@ -1181,8 +1181,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64 "ELSE 2 END ASC", priorityRepoID). Desc("issue.created_unix"). Desc("issue.id") - case "project-column-sorting": - sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") + case "board-column-sorting": + sess.Asc("board_issue.sorting").Desc("issue.created_unix").Desc("issue.id") default: sess.Desc("issue.created_unix").Desc("issue.id") } @@ -1248,16 +1248,16 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) } - if opts.ProjectID > 0 { - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) + if opts.BoardID > 0 { + sess.Join("INNER", "board_issue", "issue.id = board_issue.issue_id"). + And("board_issue.board_id=?", opts.BoardID) } - if opts.ProjectBoardID != 0 { - if opts.ProjectBoardID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID})) + if opts.BoardColumnID != 0 { + if opts.BoardColumnID > 0 { + sess.In("issue.id", builder.Select("issue_id").From("board_issue").Where(builder.Eq{"board_column_id": opts.BoardColumnID})) } else { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) + sess.In("issue.id", builder.Select("issue_id").From("board_issue").Where(builder.Eq{"board_column_id": 0})) } } @@ -2304,7 +2304,7 @@ func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths [] } if _, err = sess.In("issue_id", deleteCond). - Delete(&project_model.ProjectIssue{}); err != nil { + Delete(&board_model.BoardIssue{}); err != nil { return } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index e22e48c0bb4bd..3d353707e38a7 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -7,8 +7,8 @@ import ( "context" "fmt" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -214,39 +214,39 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { return nil } -func (issues IssueList) getProjectIDs() []int64 { +func (issues IssueList) getBoardIDs() []int64 { ids := make(container.Set[int64], len(issues)) for _, issue := range issues { - ids.Add(issue.ProjectID()) + ids.Add(issue.BoardID()) } return ids.Values() } -func (issues IssueList) loadProjects(ctx context.Context) error { - projectIDs := issues.getProjectIDs() - if len(projectIDs) == 0 { +func (issues IssueList) loadBoards(ctx context.Context) error { + boardIDs := issues.getBoardIDs() + if len(boardIDs) == 0 { return nil } - projectMaps := make(map[int64]*project_model.Project, len(projectIDs)) - left := len(projectIDs) + boardMaps := make(map[int64]*board_model.Board, len(boardIDs)) + left := len(boardIDs) for left > 0 { limit := db.DefaultMaxInSize if left < limit { limit = left } err := db.GetEngine(ctx). - In("id", projectIDs[:limit]). - Find(&projectMaps) + In("id", boardIDs[:limit]). + Find(&boardMaps) if err != nil { return err } left -= limit - projectIDs = projectIDs[limit:] + boardIDs = boardIDs[limit:] } for _, issue := range issues { - issue.Project = projectMaps[issue.ProjectID()] + issue.Board = boardMaps[issue.BoardID()] } return nil } @@ -526,8 +526,8 @@ func (issues IssueList) loadAttributes(ctx context.Context) error { return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err) } - if err := issues.loadProjects(ctx); err != nil { - return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err) + if err := issues.loadBoards(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadBoards: %w", err) } if err := issues.loadAssignees(ctx); err != nil { diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 8e559f13c92c5..a4630f1fa46d8 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -7,67 +7,67 @@ import ( "context" "fmt" + board_model "code.gitea.io/gitea/models/board" "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 (issue *Issue) LoadProject() (err error) { - return issue.loadProject(db.DefaultContext) +// LoadBoard load the board the issue was assigned to +func (issue *Issue) LoadBoard() (err error) { + return issue.loadBoard(db.DefaultContext) } -func (issue *Issue) loadProject(ctx context.Context) (err error) { - if issue.Project == nil { - var p project_model.Project - if _, err = db.GetEngine(ctx).Table("project"). - Join("INNER", "project_issue", "project.id=project_issue.project_id"). - Where("project_issue.issue_id = ?", issue.ID). +func (issue *Issue) loadBoard(ctx context.Context) (err error) { + if issue.Board == nil { + var p board_model.Board + if _, err = db.GetEngine(ctx).Table("board"). + Join("INNER", "board_issue", "board.id=board_issue.board_id"). + Where("board_issue.issue_id = ?", issue.ID). Get(&p); err != nil { return err } - issue.Project = &p + issue.Board = &p } return err } -// ProjectID return project id if issue was assigned to one -func (issue *Issue) ProjectID() int64 { - return issue.projectID(db.DefaultContext) +// BoardID return board id if issue was assigned to one +func (issue *Issue) BoardID() int64 { + return issue.boardID(db.DefaultContext) } -func (issue *Issue) projectID(ctx context.Context) int64 { - var ip project_model.ProjectIssue +func (issue *Issue) boardID(ctx context.Context) int64 { + var ip board_model.BoardIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) if err != nil || !has { return 0 } - return ip.ProjectID + return ip.BoardID } -// ProjectBoardID return project board id if issue was assigned to one -func (issue *Issue) ProjectBoardID() int64 { - return issue.projectBoardID(db.DefaultContext) +// BoardColumnID return board column id if issue was assigned to one +func (issue *Issue) BoardColumnID() int64 { + return issue.boardColumnID(db.DefaultContext) } -func (issue *Issue) projectBoardID(ctx context.Context) int64 { - var ip project_model.ProjectIssue +func (issue *Issue) boardColumnID(ctx context.Context) int64 { + var ip board_model.BoardIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) if err != nil || !has { return 0 } - return ip.ProjectBoardID + return ip.BoardColumnID } -// LoadIssuesFromBoard load issues assigned to this board -func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { +// LoadIssuesFromBoardColumn load issues assigned to this column +func LoadIssuesFromBoardColumn(ctx context.Context, b *board_model.Column) (IssueList, error) { issueList := make([]*Issue, 0, 10) if b.ID != 0 { issues, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: b.ID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", + BoardColumnID: b.ID, + BoardID: b.BoardID, + SortType: "board-column-sorting", }) if err != nil { return nil, err @@ -77,9 +77,9 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList if b.Default { issues, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: -1, // Issues without ProjectBoardID - ProjectID: b.ProjectID, - SortType: "project-column-sorting", + BoardColumnID: -1, // Issues without BoardColumnID + BoardID: b.BoardID, + SortType: "board-column-sorting", }) if err != nil { return nil, err @@ -95,10 +95,10 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList } // LoadIssuesFromBoardList load issues assigned to the boards -func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) { +func LoadIssuesFromBoardList(ctx context.Context, bs board_model.ColumnList) (map[int64]IssueList, error) { issuesMap := make(map[int64]IssueList, len(bs)) for i := range bs { - il, err := LoadIssuesFromBoard(ctx, bs[i]) + il, err := LoadIssuesFromBoardColumn(ctx, bs[i]) if err != nil { return nil, err } @@ -107,36 +107,36 @@ func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (m return issuesMap, nil } -// ChangeProjectAssign changes the project associated with an issue -func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error { +// ChangeBoardAssign changes the board associated with an issue +func ChangeBoardAssign(issue *Issue, doer *user_model.User, newBoardID int64) error { ctx, committer, err := db.TxContext(db.DefaultContext) if err != nil { return err } defer committer.Close() - if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil { + if err := addUpdateIssueBoard(ctx, issue, doer, newBoardID); err != nil { return err } return committer.Commit() } -func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error { - oldProjectID := issue.projectID(ctx) +func addUpdateIssueBoard(ctx context.Context, issue *Issue, doer *user_model.User, newBoardID int64) error { + oldBoardID := issue.boardID(ctx) - // Only check if we add a new project and not remove it. - if newProjectID > 0 { - newProject, err := project_model.GetProjectByID(ctx, newProjectID) + // Only check if we add a new board and not remove it. + if newBoardID > 0 { + newBoard, err := board_model.GetBoardByID(ctx, newBoardID) if err != nil { return err } - if newProject.RepoID != issue.RepoID { - return fmt.Errorf("issue's repository is not the same as project's repository") + if newBoard.RepoID != issue.RepoID { + return fmt.Errorf("issue's repository is not the same as board's repository") } } - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { + if _, err := db.GetEngine(ctx).Where("board_issue.issue_id=?", issue.ID).Delete(&board_model.BoardIssue{}); err != nil { return err } @@ -144,27 +144,27 @@ func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.U return err } - if oldProjectID > 0 || newProjectID > 0 { + if oldBoardID > 0 || newBoardID > 0 { if _, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeProject, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldProjectID: oldProjectID, - ProjectID: newProjectID, + Type: CommentTypeBoard, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldBoardID: oldBoardID, + BoardID: newBoardID, }); err != nil { return err } } - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, + return db.Insert(ctx, &board_model.BoardIssue{ + IssueID: issue.ID, + BoardID: newBoardID, }) } -// MoveIssueAcrossProjectBoards move a card from one board to another -func MoveIssueAcrossProjectBoards(issue *Issue, board *project_model.Board) error { +// MoveIssueAcrossBoardColumns move a card from one column to another +func MoveIssueAcrossBoardColumns(issue *Issue, board *board_model.Board) error { ctx, committer, err := db.TxContext(db.DefaultContext) if err != nil { return err @@ -172,18 +172,18 @@ func MoveIssueAcrossProjectBoards(issue *Issue, board *project_model.Board) erro defer committer.Close() sess := db.GetEngine(ctx) - var pis project_model.ProjectIssue + var pis board_model.BoardIssue 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") + return fmt.Errorf("issue has to be added to a board first") } - pis.ProjectBoardID = board.ID - if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil { + pis.BoardColumnID = board.ID + if _, err := sess.ID(pis.ID).Cols("board_column_id").Update(&pis); err != nil { return err } diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index dcf99c96ae81f..4a6134930cf25 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -631,3 +631,29 @@ func deleteDB() error { return nil } + +func RenameTable(x *xorm.Engine, oldTable, newTable string) error { + if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", oldTable, newTable)); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", oldTable, newTable, err) + return err + } + return nil +} + +func RenameColumn(sess *xorm.Session, table, oldColumn, newColumn, mySQLType string) error { + switch { + case setting.Database.UseMySQL: + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` CHANGE %s %s %s", table, oldColumn, newColumn, mySQLType)); err != nil { + return err + } + case setting.Database.UseMSSQL: + if _, err := sess.Exec(fmt.Sprintf("sp_rename '%s.%s', '%s', 'COLUMN'", table, oldColumn, newColumn)); err != nil { + return err + } + default: + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME COLUMN %s TO %s", table, oldColumn, newColumn)); err != nil { + return err + } + } + return nil +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9d9c8f5165e47..5a04361f913b6 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -446,6 +446,8 @@ var migrations = []Migration{ NewMigration("Create secrets table", v1_19.CreateSecretsTable), // v237 -> v238 NewMigration("Drop ForeignReference table", v1_19.DropForeignReferenceTable), + // v238 -> v239 + NewMigration("Rename projects related tables to boards", v1_19.RenameProjectsToBoards), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_13/v146.go b/models/migrations/v1_13/v146.go index 5db8b0a4374ac..b322c35918739 100644 --- a/models/migrations/v1_13/v146.go +++ b/models/migrations/v1_13/v146.go @@ -24,8 +24,8 @@ func AddProjectsInfo(x *xorm.Engine) error { CreatorID int64 `xorm:"NOT NULL"` IsClosed bool `xorm:"INDEX"` - BoardType ProjectBoardType - Type ProjectType + ColumnType ProjectBoardType + Type ProjectType ClosedDateUnix timeutil.TimeStamp CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` diff --git a/models/migrations/v1_15/v184.go b/models/migrations/v1_15/v184.go index 48f8b62165cc5..44a30533190f7 100644 --- a/models/migrations/v1_15/v184.go +++ b/models/migrations/v1_15/v184.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/migrations/base" - "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" ) @@ -53,19 +52,9 @@ func RenameTaskErrorsToMessage(x *xorm.Engine) error { } } - switch { - case setting.Database.UseMySQL: - if _, err := sess.Exec("ALTER TABLE `task` CHANGE errors message text"); err != nil { - return err - } - case setting.Database.UseMSSQL: - if _, err := sess.Exec("sp_rename 'task.errors', 'message', 'COLUMN'"); err != nil { - return err - } - default: - if _, err := sess.Exec("ALTER TABLE `task` RENAME COLUMN errors TO message"); err != nil { - return err - } + if err := base.RenameColumn(sess, "task", "errors", "message", "text"); err != nil { + return err } + return sess.Commit() } diff --git a/models/migrations/v1_19/v238.go b/models/migrations/v1_19/v238.go new file mode 100644 index 0000000000000..0f876fddaffe6 --- /dev/null +++ b/models/migrations/v1_19/v238.go @@ -0,0 +1,49 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_19 //nolint + +import ( + "code.gitea.io/gitea/models/migrations/base" + "xorm.io/xorm" +) + +func RenameProjectsToBoards(x *xorm.Engine) error { + if err := base.RenameTable(x, "project_board", "board_column"); err != nil { + return err + } + if err := base.RenameTable(x, "project_issue", "board_issue"); err != nil { + return err + } + if err := base.RenameTable(x, "project", "board"); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := base.RenameColumn(sess, "board_column", "project_id", "board_id", "bigint"); err != nil { + return err + } + if err := base.RenameColumn(sess, "board_column", "project_id", "board_id", "bigint"); err != nil { + return err + } + if err := base.RenameColumn(sess, "board_issue", "project_id", "board_id", "bigint"); err != nil { + return err + } + if err := base.RenameColumn(sess, "board_issue", "project_board_id", "board_column_id", "bigint"); err != nil { + return err + } + if err := base.RenameColumn(sess, "repository", "num_projects", "num_boards", "INT(11)"); err != nil { + return err + } + if err := base.RenameColumn(sess, "repository", "num_closed_projects", "num_closed_boards", "INT(11)"); err != nil { + return err + } + if err := base.RenameColumn(sess, "comment", "old_project_id", "old_board_id", "bigint"); err != nil { + return err + } + if err := base.RenameColumn(sess, "comment", "project_id", "board_id", "bigint"); err != nil { + return err + } + return nil +} diff --git a/models/project/board.go b/models/project/board.go deleted file mode 100644 index d8468f0cb55c6..0000000000000 --- a/models/project/board.go +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -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(db.DefaultContext) - 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 { - board, err := GetBoard(ctx, boardID) - if err != nil { - if IsErrProjectBoardNotExist(err) { - return nil - } - - return err - } - - if err = board.removeIssues(ctx); err != nil { - return err - } - - if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil { - return err - } - return nil -} - -func deleteBoardByProjectID(ctx context.Context, projectID int64) error { - _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{}) - return err -} - -// GetBoard fetches the current board of a project -func GetBoard(ctx context.Context, boardID int64) (*Board, error) { - board := new(Board) - - has, err := db.GetEngine(ctx).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(ctx context.Context, 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 := db.GetEngine(ctx).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(ctx context.Context, projectID int64) (BoardList, error) { - boards := make([]*Board, 0, 5) - - if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", projectID, false).OrderBy("Sorting").Find(&boards); err != nil { - return nil, err - } - - defaultB, err := getDefaultBoard(ctx, 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(ctx context.Context, projectID int64) (*Board, error) { - var board Board - exist, err := db.GetEngine(ctx).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 -} diff --git a/models/project/issue.go b/models/project/issue.go deleted file mode 100644 index 3269197d6cde3..0000000000000 --- a/models/project/issue.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package project - -import ( - "context" - "fmt" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" -) - -// 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"` - - // the sorting order on the board - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` -} - -func init() { - db.RegisterModel(new(ProjectIssue)) -} - -func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error { - _, err := db.GetEngine(ctx).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 { - log.Error("NumIssues: %v", err) - 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 { - log.Error("NumClosedIssues: %v", err) - 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 { - log.Error("NumOpenIssues: %v", err) - 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(db.DefaultContext, 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 (b *Board) removeIssues(ctx context.Context) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", b.ID) - return err -} diff --git a/models/project/project.go b/models/project/project.go deleted file mode 100644 index bcf1166408f85..0000000000000 --- a/models/project/project.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package project - -import ( - "context" - "errors" - "fmt" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" - - "xorm.io/builder" -) - -type ( - // ProjectsConfig is used to identify the type of board that is being created - ProjectsConfig struct { - BoardType BoardType - Translation string - } - - // Type is used to identify the type of project in question and ownership - Type uint8 -) - -const ( - // TypeIndividual is a type of project board that is owned by an individual - TypeIndividual Type = iota + 1 - - // TypeRepository is a project that is tied to a repository - TypeRepository - - // TypeOrganization is a project that is tied to an organisation - 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) -} - -func (err ErrProjectNotExist) Unwrap() error { - return util.ErrNotExist -} - -// 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) -} - -func (err ErrProjectBoardNotExist) Unwrap() error { - return util.ErrNotExist -} - -// Project represents a project board -type Project struct { - ID int64 `xorm:"pk autoincr"` - Title string `xorm:"INDEX NOT NULL"` - Description string `xorm:"TEXT"` - RepoID int64 `xorm:"INDEX"` - CreatorID int64 `xorm:"NOT NULL"` - IsClosed bool `xorm:"INDEX"` - BoardType BoardType - Type Type - - RenderedContent string `xorm:"-"` - - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - ClosedDateUnix timeutil.TimeStamp -} - -func init() { - db.RegisterModel(new(Project)) -} - -// GetProjectsConfig retrieves the types of configurations projects could have -func GetProjectsConfig() []ProjectsConfig { - return []ProjectsConfig{ - {BoardTypeNone, "repo.projects.type.none"}, - {BoardTypeBasicKanban, "repo.projects.type.basic_kanban"}, - {BoardTypeBugTriage, "repo.projects.type.bug_triage"}, - } -} - -// IsTypeValid checks if a project type is valid -func IsTypeValid(p Type) bool { - switch p { - case TypeRepository: - return true - default: - return false - } -} - -// SearchOptions are options for GetProjects -type SearchOptions struct { - RepoID int64 - Page int - IsClosed util.OptionalBool - SortType string - Type Type -} - -// GetProjects returns a list of all projects that have been created in the repository -func GetProjects(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) { - e := db.GetEngine(ctx) - projects := make([]*Project, 0, setting.UI.IssuePagingNum) - - var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID} - switch opts.IsClosed { - case util.OptionalBoolTrue: - cond = cond.And(builder.Eq{"is_closed": true}) - case util.OptionalBoolFalse: - cond = cond.And(builder.Eq{"is_closed": false}) - } - - if opts.Type > 0 { - cond = cond.And(builder.Eq{"type": opts.Type}) - } - - count, err := e.Where(cond).Count(new(Project)) - if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) - } - - e = e.Where(cond) - - if opts.Page > 0 { - e = e.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum) - } - - switch opts.SortType { - case "oldest": - e.Desc("created_unix") - case "recentupdate": - e.Desc("updated_unix") - case "leastupdate": - e.Asc("updated_unix") - default: - e.Asc("created_unix") - } - - return projects, count, e.Find(&projects) -} - -// NewProject creates a new Project -func NewProject(p *Project) error { - if !IsBoardTypeValid(p.BoardType) { - p.BoardType = BoardTypeNone - } - - if !IsTypeValid(p.Type) { - return errors.New("project type is not valid") - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err := db.Insert(ctx, p); err != nil { - return err - } - - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil { - return err - } - - if err := createBoardsForProjectsType(ctx, p); err != nil { - return err - } - - return committer.Commit() -} - -// GetProjectByID returns the projects in a repository -func GetProjectByID(ctx context.Context, id int64) (*Project, error) { - p := new(Project) - - has, err := db.GetEngine(ctx).ID(id).Get(p) - if err != nil { - return nil, err - } else if !has { - return nil, ErrProjectNotExist{ID: id} - } - - return p, nil -} - -// UpdateProject updates project properties -func UpdateProject(ctx context.Context, p *Project) error { - _, err := db.GetEngine(ctx).ID(p.ID).Cols( - "title", - "description", - ).Update(p) - return err -} - -func updateRepositoryProjectCount(ctx context.Context, repoID int64) error { - if _, err := db.GetEngine(ctx).Exec(builder.Update( - builder.Eq{ - "`num_projects`": builder.Select("count(*)").From("`project`"). - Where(builder.Eq{"`project`.`repo_id`": repoID}. - And(builder.Eq{"`project`.`type`": TypeRepository})), - }).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { - return err - } - - if _, err := db.GetEngine(ctx).Exec(builder.Update( - builder.Eq{ - "`num_closed_projects`": builder.Select("count(*)").From("`project`"). - Where(builder.Eq{"`project`.`repo_id`": repoID}. - And(builder.Eq{"`project`.`type`": TypeRepository}). - And(builder.Eq{"`project`.`is_closed`": true})), - }).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil { - return err - } - return nil -} - -// ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed -func ChangeProjectStatusByRepoIDAndID(repoID, projectID int64, isClosed bool) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - p := new(Project) - - has, err := db.GetEngine(ctx).ID(projectID).Where("repo_id = ?", repoID).Get(p) - if err != nil { - return err - } else if !has { - return ErrProjectNotExist{ID: projectID, RepoID: repoID} - } - - if err := changeProjectStatus(ctx, p, isClosed); err != nil { - return err - } - - return committer.Commit() -} - -// ChangeProjectStatus toggle a project between opened and closed -func ChangeProjectStatus(p *Project, isClosed bool) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err := changeProjectStatus(ctx, p, isClosed); err != nil { - return err - } - - return committer.Commit() -} - -func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { - p.IsClosed = isClosed - p.ClosedDateUnix = timeutil.TimeStampNow() - count, err := db.GetEngine(ctx).ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) - if err != nil { - return err - } - if count < 1 { - return nil - } - - return updateRepositoryProjectCount(ctx, p.RepoID) -} - -// DeleteProjectByID deletes a project from a repository. if it's not in a database -// transaction, it will start a new database transaction -func DeleteProjectByID(ctx context.Context, id int64) error { - return db.AutoTx(ctx, func(ctx context.Context) error { - p, err := GetProjectByID(ctx, id) - if err != nil { - if IsErrProjectNotExist(err) { - return nil - } - return err - } - - if err := deleteProjectIssuesByProjectID(ctx, id); err != nil { - return err - } - - if err := deleteBoardByProjectID(ctx, id); err != nil { - return err - } - - if _, err = db.GetEngine(ctx).ID(p.ID).Delete(new(Project)); err != nil { - return err - } - - return updateRepositoryProjectCount(ctx, p.RepoID) - }) -} - -func DeleteProjectByRepoID(ctx context.Context, repoID int64) error { - switch { - case setting.Database.UseSQLite3: - if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue WHERE project_issue.id IN (SELECT project_issue.id FROM project_issue INNER JOIN project WHERE project.id = project_issue.project_id AND project.repo_id = ?)", repoID); err != nil { - return err - } - if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)", repoID); err != nil { - return err - } - if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil { - return err - } - case setting.Database.UsePostgreSQL: - if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? ", repoID); err != nil { - return err - } - if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? ", repoID); err != nil { - return err - } - if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil { - return err - } - default: - if _, err := db.GetEngine(ctx).Exec("DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? ", repoID); err != nil { - return err - } - if _, err := db.GetEngine(ctx).Exec("DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? ", repoID); err != nil { - return err - } - if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil { - return err - } - } - - return updateRepositoryProjectCount(ctx, repoID) -} diff --git a/models/repo.go b/models/repo.go index e95887077c955..70896d22a54ad 100644 --- a/models/repo.go +++ b/models/repo.go @@ -14,12 +14,12 @@ import ( activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" secret_model "code.gitea.io/gitea/models/secret" system_model "code.gitea.io/gitea/models/system" @@ -193,8 +193,8 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { } } - if err := project_model.DeleteProjectByRepoID(ctx, repoID); err != nil { - return fmt.Errorf("unable to delete projects for repo[%d]: %w", repoID, err) + if err := board_model.DeleteBoardByRepoID(ctx, repoID); err != nil { + return fmt.Errorf("unable to delete boards for repo[%d]: %w", repoID, err) } // Remove LFS objects diff --git a/models/repo/repo.go b/models/repo/repo.go index e5e1ac43b41fd..4c619f6ab952b 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -138,9 +138,9 @@ type Repository struct { NumMilestones int `xorm:"NOT NULL DEFAULT 0"` NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` NumOpenMilestones int `xorm:"-"` - NumProjects int `xorm:"NOT NULL DEFAULT 0"` - NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"` - NumOpenProjects int `xorm:"-"` + NumBoards int `xorm:"NOT NULL DEFAULT 0"` + NumClosedBoards int `xorm:"NOT NULL DEFAULT 0"` + NumOpenBoards int `xorm:"-"` IsPrivate bool `xorm:"INDEX"` IsEmpty bool `xorm:"INDEX"` @@ -232,7 +232,7 @@ func (repo *Repository) AfterLoad() { repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones - repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects + repo.NumOpenBoards = repo.NumBoards - repo.NumClosedBoards } // LoadAttributes loads attributes of the repository. diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index e20d03e2c5ae3..dd728907a6c67 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -174,7 +174,7 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(PullRequestsConfig) case unit.TypeIssues: r.Config = new(IssuesConfig) - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages: + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeBoards, unit.TypePackages: fallthrough default: r.Config = new(UnitConfig) diff --git a/models/unit/unit.go b/models/unit/unit.go index c4743dbdb4079..7e9fa9648f48c 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -25,7 +25,7 @@ const ( TypeWiki // 5 Wiki TypeExternalWiki // 6 ExternalWiki TypeExternalTracker // 7 ExternalTracker - TypeProjects // 8 Kanban board + TypeBoards // 8 Kanban board TypePackages // 9 Packages ) @@ -50,8 +50,8 @@ func (u Type) String() string { return "TypeExternalWiki" case TypeExternalTracker: return "TypeExternalTracker" - case TypeProjects: - return "TypeProjects" + case TypeBoards: + return "TypeBoards" case TypePackages: return "TypePackages" } @@ -75,7 +75,7 @@ var ( TypeWiki, TypeExternalWiki, TypeExternalTracker, - TypeProjects, + TypeBoards, TypePackages, } @@ -86,7 +86,7 @@ var ( TypePullRequests, TypeReleases, TypeWiki, - TypeProjects, + TypeBoards, TypePackages, } @@ -270,11 +270,11 @@ var ( perm.AccessModeRead, } - UnitProjects = Unit{ - TypeProjects, - "repo.projects", - "/projects", - "repo.projects.desc", + UnitBoards = Unit{ + TypeBoards, + "repo.boards", + "/boards", + "repo.boards.desc", 5, perm.AccessModeOwner, } @@ -297,7 +297,7 @@ var ( TypeReleases: UnitReleases, TypeWiki: UnitWiki, TypeExternalWiki: UnitExternalWiki, - TypeProjects: UnitProjects, + TypeBoards: UnitBoards, TypePackages: UnitPackages, } ) diff --git a/modules/context/context.go b/modules/context/context.go index 0fe00bf787e3d..44e8b787512b6 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -805,7 +805,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["UnitWikiGlobalDisabled"] = unit.TypeWiki.UnitGlobalDisabled() ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled() ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled() - ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() + ctx.Data["UnitBoardsGlobalDisabled"] = unit.TypeBoards.UnitGlobalDisabled() ctx.Data["locale"] = locale ctx.Data["AllLangs"] = translation.AllLangs() diff --git a/modules/context/repo.go b/modules/context/repo.go index 71a2b3c0c6ef2..e209f49557409 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -1040,7 +1040,7 @@ func UnitTypes() func(ctx *Context) { ctx.Data["UnitTypeWiki"] = unit_model.TypeWiki ctx.Data["UnitTypeExternalWiki"] = unit_model.TypeExternalWiki ctx.Data["UnitTypeExternalTracker"] = unit_model.TypeExternalTracker - ctx.Data["UnitTypeProjects"] = unit_model.TypeProjects + ctx.Data["UnitTypeBoards"] = unit_model.TypeBoards ctx.Data["UnitTypePackages"] = unit_model.TypePackages } } diff --git a/modules/convert/issue_comment.go b/modules/convert/issue_comment.go index 6044cbcf61343..a51ae3217d1e6 100644 --- a/modules/convert/issue_comment.go +++ b/modules/convert/issue_comment.go @@ -77,8 +77,8 @@ func ToTimelineComment(ctx context.Context, c *issues_model.Comment, doer *user_ Created: c.CreatedUnix.AsTime(), Updated: c.UpdatedUnix.AsTime(), - OldProjectID: c.OldProjectID, - ProjectID: c.ProjectID, + OldBoardID: c.OldBoardID, + BoardID: c.BoardID, OldTitle: c.OldTitle, NewTitle: c.NewTitle, diff --git a/modules/convert/repository.go b/modules/convert/repository.go index ce53a6669237c..7fd12ce0e64ca 100644 --- a/modules/convert/repository.go +++ b/modules/convert/repository.go @@ -93,9 +93,9 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge defaultMergeStyle = config.GetDefaultMergeStyle() } - hasProjects := false - if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil { - hasProjects = true + hasBoards := false + if _, err := repo.GetUnit(ctx, unit_model.TypeBoards); err == nil { + hasBoards = true } if err := repo.GetOwner(ctx); err != nil { @@ -171,7 +171,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc ExternalTracker: externalTracker, InternalTracker: internalTracker, HasWiki: hasWiki, - HasProjects: hasProjects, + HasBoards: hasBoards, ExternalWiki: externalWiki, HasPullRequests: hasPullRequests, IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts, diff --git a/modules/doctor/fix16961.go b/modules/doctor/fix16961.go index ea14a9b2c4164..cc7f228454bb2 100644 --- a/modules/doctor/fix16961.go +++ b/modules/doctor/fix16961.go @@ -216,7 +216,7 @@ func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed boo } switch repoUnit.Type { - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects: + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeBoards: cfg := &repo_model.UnitConfig{} repoUnit.Config = cfg if fixed, err := fixUnitConfig16961(bs, cfg); !fixed { diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 17f8dd133fcd1..66be420b185a9 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -320,12 +320,12 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric( c.Projects, prometheus.GaugeValue, - float64(stats.Counter.Project), + float64(stats.Counter.Board), ) ch <- prometheus.MustNewConstMetric( c.ProjectBoards, prometheus.GaugeValue, - float64(stats.Counter.ProjectBoard), + float64(stats.Counter.BoardColumn), ) ch <- prometheus.MustNewConstMetric( c.PublicKeys, diff --git a/modules/setting/project.go b/modules/setting/board.go similarity index 70% rename from modules/setting/project.go rename to modules/setting/board.go index 53e09e8dad82a..b93420d33df54 100644 --- a/modules/setting/project.go +++ b/modules/setting/board.go @@ -5,9 +5,9 @@ package setting import "code.gitea.io/gitea/modules/log" -// Project settings +// Board settings var ( - Project = struct { + Board = struct { ProjectBoardBasicKanbanType []string ProjectBoardBugTriageType []string }{ @@ -16,8 +16,8 @@ var ( } ) -func newProject() { - if err := Cfg.Section("project").MapTo(&Project); err != nil { - log.Fatal("Failed to map Project settings: %v", err) +func newBoard() { + if err := Cfg.Section("project").MapTo(&Board); err != nil { + log.Fatal("Failed to map Board settings: %v", err) } } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 47e0ae2cda1f5..3673d3f9541f6 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1346,7 +1346,7 @@ func NewServices() { newIndexerService() newTaskService() NewQueueService() - newProject() + newBoard() newMimeTypeMap() newFederationService() } diff --git a/modules/structs/issue_comment.go b/modules/structs/issue_comment.go index 9e8f5c4bf3321..85d597ef5de35 100644 --- a/modules/structs/issue_comment.go +++ b/modules/structs/issue_comment.go @@ -51,8 +51,8 @@ type TimelineComment struct { // swagger:strfmt date-time Updated time.Time `json:"updated_at"` - OldProjectID int64 `json:"old_project_id"` - ProjectID int64 `json:"project_id"` + OldBoardID int64 `json:"old_project_id"` + BoardID int64 `json:"project_id"` OldMilestone *Milestone `json:"old_milestone"` Milestone *Milestone `json:"milestone"` TrackedTime *TrackedTime `json:"tracked_time"` diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 16f3d9dd26b7b..91a4a8b0ab353 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -86,7 +86,7 @@ type Repository struct { HasWiki bool `json:"has_wiki"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` HasPullRequests bool `json:"has_pull_requests"` - HasProjects bool `json:"has_projects"` + HasBoards bool `json:"has_projects"` IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"` AllowMerge bool `json:"allow_merge_commits"` AllowRebase bool `json:"allow_rebase"` @@ -164,8 +164,8 @@ type EditRepoOption struct { DefaultBranch *string `json:"default_branch,omitempty"` // either `true` to allow pull requests, or `false` to prevent pull request. HasPullRequests *bool `json:"has_pull_requests,omitempty"` - // either `true` to enable project unit, or `false` to disable them. - HasProjects *bool `json:"has_projects,omitempty"` + // either `true` to enable board unit, or `false` to disable them. + HasBoards *bool `json:"has_projects,omitempty"` // either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace_conflicts,omitempty"` // either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 62471abe6f19c..d8617467caf85 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -530,7 +530,7 @@ followers = Followers starred = Starred Repositories watched = Watched Repositories code = Code -projects = Projects +boards = Projects following = Following follow = Follow unfollow = Unfollow @@ -1184,41 +1184,41 @@ commit.cherry-pick-content = Select branch to cherry-pick onto: ext_issues = Access to External Issues ext_issues.desc = Link to an external issue tracker. -projects = Projects -projects.desc = Manage issues and pulls in project boards. -projects.description = Description (optional) -projects.description_placeholder = Description -projects.create = Create Project -projects.title = Title -projects.new = New project -projects.new_subheader = Coordinate, track, and update your work in one place, so projects stay transparent and on schedule. -projects.create_success = The project '%s' has been created. -projects.deletion = Delete Project -projects.deletion_desc = Deleting a project removes it from all related issues. Continue? -projects.deletion_success = The project has been deleted. -projects.edit = Edit Projects -projects.edit_subheader = Projects organize issues and track progress. -projects.modify = Update Project -projects.edit_success = Project '%s' has been updated. -projects.type.none = "None" -projects.type.basic_kanban = "Basic Kanban" -projects.type.bug_triage = "Bug Triage" -projects.template.desc = "Project template" -projects.template.desc_helper = "Select a project template to get started" -projects.type.uncategorized = Uncategorized -projects.board.edit = "Edit board" -projects.board.edit_title = "New Board Name" -projects.board.new_title = "New Board Name" -projects.board.new_submit = "Submit" -projects.board.new = "New Board" -projects.board.set_default = "Set Default" -projects.board.set_default_desc = "Set this board as default for uncategorized issues and pulls" -projects.board.delete = "Delete Board" -projects.board.deletion_desc = "Deleting a project board moves all related issues to 'Uncategorized'. Continue?" -projects.board.color = "Color" -projects.open = Open -projects.close = Close -projects.board.assigned_to = Assigned to +boards = Boards +boards.desc = Manage issues and pulls in project boards. +boards.description = Description (optional) +boards.description_placeholder = Description +boards.create = Create Project +boards.title = Title +boards.new = New project +boards.new_subheader = Coordinate, track, and update your work in one place, so boards stay transparent and on schedule. +boards.create_success = The project '%s' has been created. +boards.deletion = Delete Project +boards.deletion_desc = Deleting a project removes it from all related issues. Continue? +boards.deletion_success = The project has been deleted. +boards.edit = Edit Projects +boards.edit_subheader = Projects organize issues and track progress. +boards.modify = Update Project +boards.edit_success = Project '%s' has been updated. +boards.type.none = "None" +boards.type.basic_kanban = "Basic Kanban" +boards.type.bug_triage = "Bug Triage" +boards.template.desc = "Project template" +boards.template.desc_helper = "Select a project template to get started" +boards.type.uncategorized = Uncategorized +boards.column.edit = "Edit Column" +boards.column.edit_title = "New Column Name" +boards.column.new_title = "New Column Name" +boards.column.new_submit = "Submit" +boards.column.new = "New Column" +boards.column.set_default = "Set Default" +boards.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" +boards.column.delete = "Delete Column" +boards.column.deletion_desc = "Deleting a board column moves all related issues to 'Uncategorized'. Continue?" +boards.column.color = "Color" +boards.open = Open +boards.close = Close +boards.column.assigned_to = Assigned to issues.desc = Organize bug reports, tasks and milestones. issues.filter_assignees = Filter Assignee @@ -1232,9 +1232,9 @@ issues.new.labels = Labels issues.new.add_labels_title = Apply labels issues.new.no_label = No Label issues.new.clear_labels = Clear labels -issues.new.projects = Projects +issues.new.boards = Projects issues.new.add_project_title = Set Project -issues.new.clear_projects = Clear projects +issues.new.clear_projects = Clear boards issues.new.no_projects = No project issues.new.open_projects = Open Projects issues.new.closed_projects = Closed Projects @@ -1279,7 +1279,7 @@ issues.change_project_at = `modified the project from %s to %s %s` issues.remove_milestone_at = `removed this from the %s milestone %s` issues.remove_project_at = `removed this from the %s project %s` issues.deleted_milestone = `(deleted)` -issues.deleted_project = `(deleted)` +issues.deleted_board = `(deleted)` issues.self_assign_at = `self-assigned this %s` issues.add_assignee_at = `was assigned by %s %s` issues.remove_assignee_at = `was unassigned by %s %s` diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 349040507810b..636be1f7107af 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -909,14 +909,14 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } } - if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() { - if *opts.HasProjects { + if opts.HasBoards != nil && !unit_model.TypeBoards.UnitGlobalDisabled() { + if *opts.HasBoards { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, - Type: unit_model.TypeProjects, + Type: unit_model.TypeBoards, }) } else { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeBoards) } } diff --git a/routers/web/repo/boards.go b/routers/web/repo/boards.go new file mode 100644 index 0000000000000..98ce954e79e41 --- /dev/null +++ b/routers/web/repo/boards.go @@ -0,0 +1,655 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + board_model "code.gitea.io/gitea/models/board" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +const ( + tplBoards base.TplName = "repo/boards/list" + tplBoardsNew base.TplName = "repo/boards/new" + tplBoardsView base.TplName = "repo/boards/view" + tplGenericBoardsNew base.TplName = "user/board" +) + +// MustEnableBoards check if boards are enabled in settings +func MustEnableBoards(ctx *context.Context) { + if unit.TypeBoards.UnitGlobalDisabled() { + ctx.NotFound("EnableKanbanBoard", nil) + return + } + + if ctx.Repo.Repository != nil { + if !ctx.Repo.CanRead(unit.TypeBoards) { + ctx.NotFound("MustEnableBoards", nil) + return + } + } +} + +// Boards renders the home page of boards +func Boards(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.board_column") + + sortType := ctx.FormTrim("sort") + + isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" + repo := ctx.Repo.Repository + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + ctx.Data["OpenCount"] = repo.NumOpenBoards + ctx.Data["ClosedCount"] = repo.NumClosedBoards + + var total int + if !isShowClosed { + total = repo.NumOpenBoards + } else { + total = repo.NumClosedBoards + } + + boards, count, err := board_model.FindBoards(ctx, board_model.SearchOptions{ + RepoID: repo.ID, + Page: page, + IsClosed: util.OptionalBoolOf(isShowClosed), + SortType: sortType, + Type: board_model.TypeRepository, + }) + if err != nil { + ctx.ServerError("FindBoards", err) + return + } + + for i := range boards { + boards[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + URLPrefix: ctx.Repo.RepoLink, + Metas: ctx.Repo.Repository.ComposeMetas(), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, boards[i].Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + } + + ctx.Data["Boards"] = boards + + if isShowClosed { + ctx.Data["State"] = "closed" + } else { + ctx.Data["State"] = "open" + } + + numPages := 0 + if count > 0 { + numPages = (int(count) - 1/setting.UI.IssuePagingNum) + } + + pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) + pager.AddParam(ctx, "state", "State") + ctx.Data["Page"] = pager + + ctx.Data["CanWriteBoards"] = ctx.Repo.Permission.CanWrite(unit.TypeBoards) + ctx.Data["IsShowClosed"] = isShowClosed + ctx.Data["IsBoardsPage"] = true + ctx.Data["SortType"] = sortType + + ctx.HTML(http.StatusOK, tplBoards) +} + +// NewBoard render creating a board page +func NewBoard(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.boards.new") + ctx.Data["BoardTypes"] = board_model.GetBoardsConfig() + ctx.Data["CanWriteBoards"] = ctx.Repo.Permission.CanWrite(unit.TypeBoards) + ctx.HTML(http.StatusOK, tplBoardsNew) +} + +// NewBoardPost creates a new board +func NewBoardPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateBoardForm) + ctx.Data["Title"] = ctx.Tr("repo.boards.new") + + if ctx.HasError() { + ctx.Data["CanWriteBoards"] = ctx.Repo.Permission.CanWrite(unit.TypeBoards) + ctx.Data["BoardTypes"] = board_model.GetBoardsConfig() + ctx.HTML(http.StatusOK, tplBoardsNew) + return + } + + if err := board_model.NewBoard(&board_model.Board{ + RepoID: ctx.Repo.Repository.ID, + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + ColumnType: form.ColumnType, + Type: board_model.TypeRepository, + }); err != nil { + ctx.ServerError("NewBoard", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.boards.create_success", form.Title)) + ctx.Redirect(ctx.Repo.RepoLink + "/boards") +} + +// ChangeBoardStatus updates the status of a board between "open" and "close" +func ChangeBoardStatus(ctx *context.Context) { + toClose := false + switch ctx.Params(":action") { + case "open": + toClose = false + case "close": + toClose = true + default: + ctx.Redirect(ctx.Repo.RepoLink + "/boards") + } + id := ctx.ParamsInt64(":id") + + if err := board_model.ChangeBoardStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", err) + } else { + ctx.ServerError("ChangeBoardStatusByRepoIDAndID", err) + } + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/boards?state=" + url.QueryEscape(ctx.Params(":action"))) +} + +// DeleteBoard delete a board +func DeleteBoard(ctx *context.Context) { + p, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + if err := board_model.DeleteBoardByID(ctx, p.ID); err != nil { + ctx.Flash.Error("DeleteBoardByID: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.boards.deletion_success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": ctx.Repo.RepoLink + "/boards", + }) +} + +// EditBoard allows a board to be edited +func EditBoard(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.boards.edit") + ctx.Data["PageIsEditBoards"] = true + ctx.Data["CanWriteBoards"] = ctx.Repo.Permission.CanWrite(unit.TypeBoards) + + p, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + ctx.Data["title"] = p.Title + ctx.Data["content"] = p.Description + + ctx.HTML(http.StatusOK, tplBoardsNew) +} + +// EditBoardPost response for editing a board +func EditBoardPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateBoardForm) + ctx.Data["Title"] = ctx.Tr("repo.boards.edit") + ctx.Data["PageIsEditBoards"] = true + ctx.Data["CanWriteBoards"] = ctx.Repo.Permission.CanWrite(unit.TypeBoards) + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplBoardsNew) + return + } + + p, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + p.Title = form.Title + p.Description = form.Content + if err = board_model.UpdateBoard(ctx, p); err != nil { + ctx.ServerError("UpdateBoard", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.boards.edit_success", p.Title)) + ctx.Redirect(ctx.Repo.RepoLink + "/boards") +} + +// ViewBoard renders the columns for a board +func ViewBoard(ctx *context.Context) { + board, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + if board.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + boards, err := board_model.FindColumns(ctx, board.ID) + if err != nil { + ctx.ServerError("FindColumns", err) + return + } + + if boards[0].ID == 0 { + boards[0].Title = ctx.Tr("repo.boards.type.uncategorized") + } + + issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) + if err != nil { + ctx.ServerError("LoadIssuesFromBoardList", err) + return + } + + linkedPrsMap := make(map[int64][]*issues_model.Issue) + for _, issuesList := range issuesMap { + for _, issue := range issuesList { + var referencedIds []int64 + for _, comment := range issue.Comments { + if comment.RefIssueID != 0 && comment.RefIsPull { + referencedIds = append(referencedIds, comment.RefIssueID) + } + } + + if len(referencedIds) > 0 { + if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ + IssueIDs: referencedIds, + IsPull: util.OptionalBoolTrue, + }); err == nil { + linkedPrsMap[issue.ID] = linkedPrs + } + } + } + } + ctx.Data["LinkedPRs"] = linkedPrsMap + + board.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + URLPrefix: ctx.Repo.RepoLink, + Metas: ctx.Repo.Repository.ComposeMetas(), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, board.Description) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + + ctx.Data["IsBoardsPage"] = true + ctx.Data["CanWriteBoards"] = ctx.Repo.Permission.CanWrite(unit.TypeBoards) + ctx.Data["Board"] = board + ctx.Data["IssuesMap"] = issuesMap + ctx.Data["Boards"] = boards + + ctx.HTML(http.StatusOK, tplBoardsView) +} + +// UpdateIssueBoard change an issue's board +func UpdateIssueBoard(ctx *context.Context) { + issues := getActionIssues(ctx) + if ctx.Written() { + return + } + + boardID := ctx.FormInt64("id") + for _, issue := range issues { + oldBoardID := issue.BoardID() + if oldBoardID == boardID { + continue + } + + if err := issues_model.ChangeBoardAssign(issue, ctx.Doer, boardID); err != nil { + ctx.ServerError("ChangeBoardAssign", err) + return + } + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} + +// DeleteBoardColumn allows for the deletion of a board column +func DeleteBoardColumn(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeBoards) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + board, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + + pb, err := board_model.GetColumn(ctx, ctx.ParamsInt64(":boardID")) + if err != nil { + ctx.ServerError("GetColumn", err) + return + } + if pb.BoardID != ctx.ParamsInt64(":id") { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("BoardColumn[%d] is not in Board[%d] as expected", pb.ID, board.ID), + }) + return + } + + if board.RepoID != ctx.Repo.Repository.ID { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("BoardColumn[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID), + }) + return + } + + if err := board_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil { + ctx.ServerError("DeleteBoardByID", err) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} + +// AddBoardColumnPost allows a new column to be added to a board. +func AddBoardColumnPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditBoardColumnForm) + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeBoards) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + board, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + + if err := board_model.NewColumn(&board_model.Column{ + BoardID: board.ID, + Title: form.Title, + Color: form.Color, + CreatorID: ctx.Doer.ID, + }); err != nil { + ctx.ServerError("NewBoardBoard", err) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} + +func checkBoardColumnChangePermissions(ctx *context.Context) (*board_model.Board, *board_model.Column) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return nil, nil + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeBoards) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return nil, nil + } + + board, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return nil, nil + } + + column, err := board_model.GetColumn(ctx, ctx.ParamsInt64(":boardID")) + if err != nil { + ctx.ServerError("GetColumn", err) + return nil, nil + } + if column.BoardID != ctx.ParamsInt64(":id") { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("BoardColumn[%d] is not in Board[%d] as expected", column.ID, board.ID), + }) + return nil, nil + } + + if board.RepoID != ctx.Repo.Repository.ID { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("BoardColumn[%d] is not in Repository[%d] as expected", column.ID, ctx.Repo.Repository.ID), + }) + return nil, nil + } + return board, column +} + +// EditBoardColumn allows a board column's to be updated +func EditBoardColumn(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.EditBoardColumnForm) + _, column := checkBoardColumnChangePermissions(ctx) + if ctx.Written() { + return + } + + if form.Title != "" { + column.Title = form.Title + } + + column.Color = form.Color + + if form.Sorting != 0 { + column.Sorting = form.Sorting + } + + if err := board_model.UpdateColumn(ctx, column); err != nil { + ctx.ServerError("UpdateColumn", err) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} + +// SetDefaultBoardColumn set default board column for uncategorized issues/pulls +func SetDefaultBoardColumn(ctx *context.Context) { + board, column := checkBoardColumnChangePermissions(ctx) + if ctx.Written() { + return + } + + if err := board_model.SetDefaultColumn(board.ID, column.ID); err != nil { + ctx.ServerError("SetDefaultBoard", err) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} + +// MoveIssues moves or keeps issues in a column and sorts them inside that column +func MoveIssues(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeBoards) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + board, err := board_model.GetBoardByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if board_model.IsErrBoardNotExist(err) { + ctx.NotFound("BoardNotExist", nil) + } else { + ctx.ServerError("GetBoardByID", err) + } + return + } + if board.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("InvalidRepoID", nil) + return + } + + var column *board_model.Column + + if ctx.ParamsInt64(":boardID") == 0 { + column = &board_model.Column{ + ID: 0, + BoardID: board.ID, + Title: ctx.Tr("repo.boards.type.uncategorized"), + } + } else { + column, err = board_model.GetColumn(ctx, ctx.ParamsInt64(":boardID")) + if err != nil { + if board_model.IsErrColumnNotExist(err) { + ctx.NotFound("BoardBoardNotExist", nil) + } else { + ctx.ServerError("GetColumn", err) + } + return + } + if column.BoardID != board.ID { + ctx.NotFound("ColumnNotInBoard", nil) + return + } + } + + type movedIssuesForm struct { + Issues []struct { + IssueID int64 `json:"issueID"` + Sorting int64 `json:"sorting"` + } `json:"issues"` + } + + form := &movedIssuesForm{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedIssuesForm", err) + } + + issueIDs := make([]int64, 0, len(form.Issues)) + sortedIssueIDs := make(map[int64]int64) + for _, issue := range form.Issues { + issueIDs = append(issueIDs, issue.IssueID) + sortedIssueIDs[issue.Sorting] = issue.IssueID + } + movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.NotFound("IssueNotExisting", nil) + } else { + ctx.ServerError("GetIssueByID", err) + } + return + } + + if len(movedIssues) != len(form.Issues) { + ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) + return + } + + for _, issue := range movedIssues { + if issue.RepoID != board.RepoID { + ctx.ServerError("Some issue's repoID is not equal to board's repoID", errors.New("Some issue's repoID is not equal to board's repoID")) + return + } + } + + if err = board_model.MoveIssuesOnBoardColumn(column, sortedIssueIDs); err != nil { + ctx.ServerError("MoveIssuesOnBoardColumn", err) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/boards_test.go similarity index 68% rename from routers/web/repo/projects_test.go rename to routers/web/repo/boards_test.go index c712902ea9f1a..ddf7d45ac25f3 100644 --- a/routers/web/repo/projects_test.go +++ b/routers/web/repo/boards_test.go @@ -12,16 +12,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCheckProjectBoardChangePermissions(t *testing.T) { +func TestCheckBoardColumnChangePermissions(t *testing.T) { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/projects/1/2") + ctx := test.MockContext(t, "user2/repo1/boards/1/2") test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) ctx.SetParams(":id", "1") ctx.SetParams(":boardID", "2") - project, board := checkProjectBoardChangePermissions(ctx) - assert.NotNil(t, project) + board, column := checkBoardColumnChangePermissions(ctx) assert.NotNil(t, board) + assert.NotNil(t, column) assert.False(t, ctx.Written()) } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d315525dac879..9e6b18b30b243 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -18,12 +18,12 @@ import ( "time" activities_model "code.gitea.io/gitea/models/activities" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -134,7 +134,7 @@ func MustAllowPulls(ctx *context.Context) { } } -func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) { +func issues(ctx *context.Context, milestoneID, boardID int64, isPullOption util.OptionalBool) { var err error viewType := ctx.FormString("type") sortType := ctx.FormString("sort") @@ -255,7 +255,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti MentionedID: mentionedID, ReviewRequestedID: reviewRequestedID, MilestoneIDs: mileIDs, - ProjectID: projectID, + BoardID: boardID, IsClosed: util.OptionalBoolOf(isShowClosed), IsPull: isPullOption, LabelIDs: labelIDs, @@ -363,16 +363,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti } if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") { - projects, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{ + boards, _, err := board_model.FindBoards(ctx, board_model.SearchOptions{ RepoID: repo.ID, - Type: project_model.TypeRepository, + Type: board_model.TypeRepository, IsClosed: util.OptionalBoolOf(isShowClosed), }) if err != nil { - ctx.ServerError("GetProjects", err) + ctx.ServerError("FindBoards", err) return } - ctx.Data["Projects"] = projects + ctx.Data["Boards"] = boards } ctx.Data["IssueStats"] = issueStats @@ -422,7 +422,7 @@ func Issues(ctx *context.Context) { ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0 } - issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) + issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("board"), util.OptionalBoolOf(isPullList)) if ctx.Written() { return } @@ -472,28 +472,28 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R handleTeamMentions(ctx) } -func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { +func retrieveBoards(ctx *context.Context, repo *repo_model.Repository) { var err error - ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{ + ctx.Data["OpenBoards"], _, err = board_model.FindBoards(ctx, board_model.SearchOptions{ RepoID: repo.ID, Page: -1, IsClosed: util.OptionalBoolFalse, - Type: project_model.TypeRepository, + Type: board_model.TypeRepository, }) if err != nil { - ctx.ServerError("GetProjects", err) + ctx.ServerError("FindBoards", err) return } - ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{ + ctx.Data["ClosedBoards"], _, err = board_model.FindBoards(ctx, board_model.SearchOptions{ RepoID: repo.ID, Page: -1, IsClosed: util.OptionalBoolTrue, - Type: project_model.TypeRepository, + Type: board_model.TypeRepository, }) if err != nil { - ctx.ServerError("GetProjects", err) + ctx.ServerError("FindBoards", err) return } } @@ -716,7 +716,7 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull return nil } - retrieveProjects(ctx, repo) + retrieveBoards(ctx, repo) if ctx.Written() { return nil } @@ -808,8 +808,8 @@ func NewIssue(ctx *context.Context) { body := ctx.FormString("body") ctx.Data["BodyQuery"] = body - isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects) - ctx.Data["IsProjectsEnabled"] = isProjectsEnabled + isBoardsEnabled := ctx.Repo.CanRead(unit.TypeBoards) + ctx.Data["IsBoardsEnabled"] = isBoardsEnabled ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -824,20 +824,20 @@ func NewIssue(ctx *context.Context) { } } - projectID := ctx.FormInt64("project") - if projectID > 0 && isProjectsEnabled { - project, err := project_model.GetProjectByID(ctx, projectID) + boardID := ctx.FormInt64("board") + if boardID > 0 && isBoardsEnabled { + board, err := board_model.GetBoardByID(ctx, boardID) if err != nil { - log.Error("GetProjectByID: %d: %v", projectID, err) - } else if project.RepoID != ctx.Repo.Repository.ID { - log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID)) + log.Error("GetBoardByID: %d: %v", boardID, err) + } else if board.RepoID != ctx.Repo.Repository.ID { + log.Error("GetBoardByID: %d: %v", boardID, fmt.Errorf("board[%d] not in repo [%d]", board.ID, ctx.Repo.Repository.ID)) } else { - ctx.Data["project_id"] = projectID - ctx.Data["Project"] = project + ctx.Data["board_id"] = boardID + ctx.Data["Board"] = board } - if len(ctx.Req.URL.Query().Get("project")) > 0 { - ctx.Data["redirect_after_creation"] = "project" + if len(ctx.Req.URL.Query().Get("board")) > 0 { + ctx.Data["redirect_after_creation"] = "board" } } @@ -899,13 +899,13 @@ func NewIssueChooseTemplate(ctx *context.Context) { } if len(issueTemplates) == 0 { - // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters. + // The "issues/new" and "issues/new/choose" share the same query parameters "board" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters. ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.HTMLURL(), ctx.Req.URL.RawQuery), http.StatusSeeOther) return } ctx.Data["milestone"] = ctx.FormInt64("milestone") - ctx.Data["project"] = ctx.FormInt64("project") + ctx.Data["board"] = ctx.FormInt64("board") ctx.HTML(http.StatusOK, tplIssueChoose) } @@ -981,10 +981,10 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull ctx.Data["milestone_id"] = milestoneID } - if form.ProjectID > 0 { - p, err := project_model.GetProjectByID(ctx, form.ProjectID) + if form.BoardID > 0 { + p, err := board_model.GetBoardByID(ctx, form.BoardID) if err != nil { - ctx.ServerError("GetProjectByID", err) + ctx.ServerError("GetBoardByID", err) return nil, nil, 0, 0 } if p.RepoID != ctx.Repo.Repository.ID { @@ -992,8 +992,8 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull return nil, nil, 0, 0 } - ctx.Data["Project"] = p - ctx.Data["project_id"] = form.ProjectID + ctx.Data["Board"] = p + ctx.Data["board_id"] = form.BoardID } // Check assignees @@ -1030,7 +1030,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assigneeIDs = append(assigneeIDs, form.AssigneeID) } - return labelIDs, assigneeIDs, milestoneID, form.ProjectID + return labelIDs, assigneeIDs, milestoneID, form.BoardID } // NewIssuePost response for creating new issue @@ -1048,7 +1048,7 @@ func NewIssuePost(ctx *context.Context) { attachments []string ) - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) + labelIDs, assigneeIDs, milestoneID, boardID := ValidateRepoMetas(ctx, *form, false) if ctx.Written() { return } @@ -1094,21 +1094,21 @@ func NewIssuePost(ctx *context.Context) { return } - if projectID > 0 { - if !ctx.Repo.CanRead(unit.TypeProjects) { - // User must also be able to see the project. - ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") + if boardID > 0 { + if !ctx.Repo.CanRead(unit.TypeBoards) { + // User must also be able to see the board. + ctx.Error(http.StatusBadRequest, "user hasn't permissions to read boards") return } - if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) + if err := issues_model.ChangeBoardAssign(issue, ctx.Doer, boardID); err != nil { + ctx.ServerError("ChangeBoardAssign", err) return } } log.Trace("Issue created: %d/%d", repo.ID, issue.ID) - if ctx.FormString("redirect_after_creation") == "project" { - ctx.Redirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(form.ProjectID, 10)) + if ctx.FormString("redirect_after_creation") == "board" { + ctx.Redirect(ctx.Repo.RepoLink + "/boards/" + strconv.FormatInt(form.BoardID, 10)) } else { ctx.Redirect(issue.Link()) } @@ -1240,7 +1240,7 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["RequireTribute"] = true - ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects) + ctx.Data["IsBoardsEnabled"] = ctx.Repo.CanRead(unit.TypeBoards) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -1331,7 +1331,7 @@ func ViewIssue(ctx *context.Context) { // Check milestone and assignee. if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { RetrieveRepoMilestonesAndAssignees(ctx, repo) - retrieveProjects(ctx, repo) + retrieveBoards(ctx, repo) if ctx.Written() { return @@ -1483,24 +1483,24 @@ func ViewIssue(ctx *context.Context) { if comment.MilestoneID > 0 && comment.Milestone == nil { comment.Milestone = ghostMilestone } - } else if comment.Type == issues_model.CommentTypeProject { + } else if comment.Type == issues_model.CommentTypeBoard { - if err = comment.LoadProject(); err != nil { - ctx.ServerError("LoadProject", err) + if err = comment.LoadBoard(); err != nil { + ctx.ServerError("LoadBoard", err) return } - ghostProject := &project_model.Project{ + ghostBoard := &board_model.Board{ ID: -1, - Title: ctx.Tr("repo.issues.deleted_project"), + Title: ctx.Tr("repo.issues.deleted_board"), } - if comment.OldProjectID > 0 && comment.OldProject == nil { - comment.OldProject = ghostProject + if comment.OldBoardID > 0 && comment.OldBoard == nil { + comment.OldBoard = ghostBoard } - if comment.ProjectID > 0 && comment.Project == nil { - comment.Project = ghostProject + if comment.BoardID > 0 && comment.Board == nil { + comment.Board = ghostBoard } } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest { @@ -1775,7 +1775,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string)) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) - ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects) + ctx.Data["HasBoardsWritePermission"] = ctx.Repo.CanWrite(unit.TypeBoards) ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons ctx.Data["RefEndName"] = git.RefEndName(issue.Ref) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go deleted file mode 100644 index 75cd290b8f0cb..0000000000000 --- a/routers/web/repo/projects.go +++ /dev/null @@ -1,699 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "errors" - "fmt" - "net/http" - "net/url" - "strings" - - issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/perm" - project_model "code.gitea.io/gitea/models/project" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/markup/markdown" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/forms" -) - -const ( - tplProjects base.TplName = "repo/projects/list" - tplProjectsNew base.TplName = "repo/projects/new" - tplProjectsView base.TplName = "repo/projects/view" - tplGenericProjectsNew base.TplName = "user/project" -) - -// MustEnableProjects check if projects are enabled in settings -func MustEnableProjects(ctx *context.Context) { - if unit.TypeProjects.UnitGlobalDisabled() { - ctx.NotFound("EnableKanbanBoard", nil) - return - } - - if ctx.Repo.Repository != nil { - if !ctx.Repo.CanRead(unit.TypeProjects) { - ctx.NotFound("MustEnableProjects", nil) - return - } - } -} - -// Projects renders the home page of projects -func Projects(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.project_board") - - sortType := ctx.FormTrim("sort") - - isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" - repo := ctx.Repo.Repository - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } - - ctx.Data["OpenCount"] = repo.NumOpenProjects - ctx.Data["ClosedCount"] = repo.NumClosedProjects - - var total int - if !isShowClosed { - total = repo.NumOpenProjects - } else { - total = repo.NumClosedProjects - } - - projects, count, err := project_model.GetProjects(ctx, project_model.SearchOptions{ - RepoID: repo.ID, - Page: page, - IsClosed: util.OptionalBoolOf(isShowClosed), - SortType: sortType, - Type: project_model.TypeRepository, - }) - if err != nil { - ctx.ServerError("GetProjects", err) - return - } - - for i := range projects { - projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - URLPrefix: ctx.Repo.RepoLink, - Metas: ctx.Repo.Repository.ComposeMetas(), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, projects[i].Description) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - } - - ctx.Data["Projects"] = projects - - if isShowClosed { - ctx.Data["State"] = "closed" - } else { - ctx.Data["State"] = "open" - } - - numPages := 0 - if count > 0 { - numPages = (int(count) - 1/setting.UI.IssuePagingNum) - } - - pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) - pager.AddParam(ctx, "state", "State") - ctx.Data["Page"] = pager - - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - ctx.Data["IsShowClosed"] = isShowClosed - ctx.Data["IsProjectsPage"] = true - ctx.Data["SortType"] = sortType - - ctx.HTML(http.StatusOK, tplProjects) -} - -// NewProject render creating a project page -func NewProject(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.projects.new") - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - ctx.HTML(http.StatusOK, tplProjectsNew) -} - -// NewProjectPost creates a new project -func NewProjectPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateProjectForm) - ctx.Data["Title"] = ctx.Tr("repo.projects.new") - - if ctx.HasError() { - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() - ctx.HTML(http.StatusOK, tplProjectsNew) - return - } - - if err := project_model.NewProject(&project_model.Project{ - RepoID: ctx.Repo.Repository.ID, - Title: form.Title, - Description: form.Content, - CreatorID: ctx.Doer.ID, - BoardType: form.BoardType, - Type: project_model.TypeRepository, - }); err != nil { - ctx.ServerError("NewProject", err) - return - } - - ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) - ctx.Redirect(ctx.Repo.RepoLink + "/projects") -} - -// ChangeProjectStatus updates the status of a project between "open" and "close" -func ChangeProjectStatus(ctx *context.Context) { - toClose := false - switch ctx.Params(":action") { - case "open": - toClose = false - case "close": - toClose = true - default: - ctx.Redirect(ctx.Repo.RepoLink + "/projects") - } - id := ctx.ParamsInt64(":id") - - if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", err) - } else { - ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err) - } - return - } - ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + url.QueryEscape(ctx.Params(":action"))) -} - -// DeleteProject delete a project -func DeleteProject(ctx *context.Context) { - p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - if p.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound("", nil) - return - } - - if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil { - ctx.Flash.Error("DeleteProjectByID: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success")) - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/projects", - }) -} - -// EditProject allows a project to be edited -func EditProject(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.projects.edit") - ctx.Data["PageIsEditProjects"] = true - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - - p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - if p.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound("", nil) - return - } - - ctx.Data["title"] = p.Title - ctx.Data["content"] = p.Description - - ctx.HTML(http.StatusOK, tplProjectsNew) -} - -// EditProjectPost response for editing a project -func EditProjectPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateProjectForm) - ctx.Data["Title"] = ctx.Tr("repo.projects.edit") - ctx.Data["PageIsEditProjects"] = true - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplProjectsNew) - return - } - - p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - if p.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound("", nil) - return - } - - p.Title = form.Title - p.Description = form.Content - if err = project_model.UpdateProject(ctx, p); err != nil { - ctx.ServerError("UpdateProjects", err) - return - } - - ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title)) - ctx.Redirect(ctx.Repo.RepoLink + "/projects") -} - -// ViewProject renders the project board for a project -func ViewProject(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - if project.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound("", nil) - return - } - - boards, err := project_model.GetBoards(ctx, project.ID) - if err != nil { - ctx.ServerError("GetProjectBoards", err) - return - } - - if boards[0].ID == 0 { - boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") - } - - issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) - if err != nil { - ctx.ServerError("LoadIssuesOfBoards", err) - return - } - - linkedPrsMap := make(map[int64][]*issues_model.Issue) - for _, issuesList := range issuesMap { - for _, issue := range issuesList { - var referencedIds []int64 - for _, comment := range issue.Comments { - if comment.RefIssueID != 0 && comment.RefIsPull { - referencedIds = append(referencedIds, comment.RefIssueID) - } - } - - if len(referencedIds) > 0 { - if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ - IssueIDs: referencedIds, - IsPull: util.OptionalBoolTrue, - }); err == nil { - linkedPrsMap[issue.ID] = linkedPrs - } - } - } - } - ctx.Data["LinkedPRs"] = linkedPrsMap - - project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - URLPrefix: ctx.Repo.RepoLink, - Metas: ctx.Repo.Repository.ComposeMetas(), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, project.Description) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - - ctx.Data["IsProjectsPage"] = true - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - ctx.Data["Project"] = project - ctx.Data["IssuesMap"] = issuesMap - ctx.Data["Boards"] = boards - - ctx.HTML(http.StatusOK, tplProjectsView) -} - -// UpdateIssueProject change an issue's project -func UpdateIssueProject(ctx *context.Context) { - issues := getActionIssues(ctx) - if ctx.Written() { - return - } - - projectID := ctx.FormInt64("id") - for _, issue := range issues { - oldProjectID := issue.ProjectID() - if oldProjectID == projectID { - continue - } - - if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { - ctx.ServerError("ChangeProjectAssign", err) - return - } - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) -} - -// DeleteProjectBoard allows for the deletion of a project board -func DeleteProjectBoard(ctx *context.Context) { - if ctx.Doer == nil { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only signed in users are allowed to perform this action.", - }) - return - } - - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized users are allowed to perform this action.", - }) - return - } - - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - - pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) - if err != nil { - ctx.ServerError("GetProjectBoard", err) - return - } - if pb.ProjectID != ctx.ParamsInt64(":id") { - ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), - }) - return - } - - if project.RepoID != ctx.Repo.Repository.ID { - ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID), - }) - return - } - - if err := project_model.DeleteBoardByID(ctx.ParamsInt64(":boardID")); err != nil { - ctx.ServerError("DeleteProjectBoardByID", err) - return - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) -} - -// AddBoardToProjectPost allows a new board to be added to a project. -func AddBoardToProjectPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditProjectBoardForm) - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized users are allowed to perform this action.", - }) - return - } - - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - - if err := project_model.NewBoard(&project_model.Board{ - ProjectID: project.ID, - Title: form.Title, - Color: form.Color, - CreatorID: ctx.Doer.ID, - }); err != nil { - ctx.ServerError("NewProjectBoard", err) - return - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) -} - -func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { - if ctx.Doer == nil { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only signed in users are allowed to perform this action.", - }) - return nil, nil - } - - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized users are allowed to perform this action.", - }) - return nil, nil - } - - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return nil, nil - } - - board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) - if err != nil { - ctx.ServerError("GetProjectBoard", err) - return nil, nil - } - if board.ProjectID != ctx.ParamsInt64(":id") { - ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), - }) - return nil, nil - } - - if project.RepoID != ctx.Repo.Repository.ID { - ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID), - }) - return nil, nil - } - return project, board -} - -// EditProjectBoard allows a project board's to be updated -func EditProjectBoard(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditProjectBoardForm) - _, board := checkProjectBoardChangePermissions(ctx) - if ctx.Written() { - return - } - - if form.Title != "" { - board.Title = form.Title - } - - board.Color = form.Color - - if form.Sorting != 0 { - board.Sorting = form.Sorting - } - - if err := project_model.UpdateBoard(ctx, board); err != nil { - ctx.ServerError("UpdateProjectBoard", err) - return - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) -} - -// SetDefaultProjectBoard set default board for uncategorized issues/pulls -func SetDefaultProjectBoard(ctx *context.Context) { - project, board := checkProjectBoardChangePermissions(ctx) - if ctx.Written() { - return - } - - if err := project_model.SetDefaultBoard(project.ID, board.ID); err != nil { - ctx.ServerError("SetDefaultBoard", err) - return - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) -} - -// MoveIssues moves or keeps issues in a column and sorts them inside that column -func MoveIssues(ctx *context.Context) { - if ctx.Doer == nil { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only signed in users are allowed to perform this action.", - }) - return - } - - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only authorized users are allowed to perform this action.", - }) - return - } - - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) - if err != nil { - if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("ProjectNotExist", nil) - } else { - ctx.ServerError("GetProjectByID", err) - } - return - } - if project.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound("InvalidRepoID", nil) - return - } - - var board *project_model.Board - - if ctx.ParamsInt64(":boardID") == 0 { - board = &project_model.Board{ - ID: 0, - ProjectID: project.ID, - Title: ctx.Tr("repo.projects.type.uncategorized"), - } - } else { - board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) - if err != nil { - if project_model.IsErrProjectBoardNotExist(err) { - ctx.NotFound("ProjectBoardNotExist", nil) - } else { - ctx.ServerError("GetProjectBoard", err) - } - return - } - if board.ProjectID != project.ID { - ctx.NotFound("BoardNotInProject", nil) - return - } - } - - type movedIssuesForm struct { - Issues []struct { - IssueID int64 `json:"issueID"` - Sorting int64 `json:"sorting"` - } `json:"issues"` - } - - form := &movedIssuesForm{} - if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("DecodeMovedIssuesForm", err) - } - - issueIDs := make([]int64, 0, len(form.Issues)) - sortedIssueIDs := make(map[int64]int64) - for _, issue := range form.Issues { - issueIDs = append(issueIDs, issue.IssueID) - sortedIssueIDs[issue.Sorting] = issue.IssueID - } - movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) - if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound("IssueNotExisting", nil) - } else { - ctx.ServerError("GetIssueByID", err) - } - return - } - - if len(movedIssues) != len(form.Issues) { - ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) - return - } - - for _, issue := range movedIssues { - if issue.RepoID != project.RepoID { - ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID")) - return - } - } - - if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { - ctx.ServerError("MoveIssuesOnProjectBoard", err) - return - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "ok": true, - }) -} - -// CreateProject renders the generic project creation page -func CreateProject(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.projects.new") - ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig() - ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) - - 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 + "/") -} diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 913ed6c7cb8e9..43dc160bfbb3f 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -480,13 +480,13 @@ func SettingsPost(ctx *context.Context) { } } - if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + if form.EnableBoards && !unit_model.TypeBoards.UnitGlobalDisabled() { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, - Type: unit_model.TypeProjects, + Type: unit_model.TypeBoards, }) - } else if !unit_model.TypeProjects.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) + } else if !unit_model.TypeBoards.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeBoards) } if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 0002d56de01ba..0859e19251dc5 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -10,9 +10,9 @@ import ( "strings" activities_model "code.gitea.io/gitea/models/activities" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -223,14 +223,14 @@ func Profile(ctx *context.Context) { } total = int(count) - case "projects": - ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{ + case "boards": + ctx.Data["OpenBoards"], _, err = board_model.FindBoards(ctx, board_model.SearchOptions{ Page: -1, IsClosed: util.OptionalBoolFalse, - Type: project_model.TypeIndividual, + Type: board_model.TypeIndividual, }) if err != nil { - ctx.ServerError("GetProjects", err) + ctx.ServerError("FindBoards", err) return } case "watching": @@ -284,7 +284,7 @@ func Profile(ctx *context.Context) { pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) pager.SetDefaultParams(ctx) pager.AddParam(ctx, "tab", "TabName") - if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { + if tab != "followers" && tab != "following" && tab != "activity" && tab != "boards" { pager.AddParam(ctx, "language", "Language") } ctx.Data["Page"] = pager diff --git a/routers/web/web.go b/routers/web/web.go index 31b3eb9baadac..cd018d075425f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -659,8 +659,8 @@ func RegisterRoutes(m *web.Route) { reqRepoPullsReader := context.RequireRepoReader(unit.TypePullRequests) reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(unit.TypeIssues, unit.TypePullRequests) reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests) - reqRepoProjectsReader := context.RequireRepoReader(unit.TypeProjects) - reqRepoProjectsWriter := context.RequireRepoWriter(unit.TypeProjects) + reqRepoBoardsReader := context.RequireRepoReader(unit.TypeBoards) + reqRepoBoardsWriter := context.RequireRepoWriter(unit.TypeBoards) reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { @@ -1015,7 +1015,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) - m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) + m.Post("/boards", reqRepoIssuesOrPullsWriter, reqRepoBoardsReader, repo.UpdateIssueBoard) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest) m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) @@ -1159,30 +1159,30 @@ func RegisterRoutes(m *web.Route) { m.Get("/packages", repo.Packages) } - m.Group("/projects", func() { - m.Get("", repo.Projects) - m.Get("/{id}", repo.ViewProject) + m.Group("/boards", func() { + m.Get("", repo.Boards) + m.Get("/{id}", repo.ViewBoard) m.Group("", func() { - m.Get("/new", repo.NewProject) - m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) + m.Get("/new", repo.NewBoard) + m.Post("/new", web.Bind(forms.CreateBoardForm{}), repo.NewBoardPost) m.Group("/{id}", func() { - m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost) - m.Post("/delete", repo.DeleteProject) + m.Post("", web.Bind(forms.EditBoardColumnForm{}), repo.AddBoardColumnPost) + m.Post("/delete", repo.DeleteBoard) - m.Get("/edit", repo.EditProject) - m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost) - m.Post("/{action:open|close}", repo.ChangeProjectStatus) + m.Get("/edit", repo.EditBoard) + m.Post("/edit", web.Bind(forms.CreateBoardForm{}), repo.EditBoardPost) + m.Post("/{action:open|close}", repo.ChangeBoardStatus) m.Group("/{boardID}", func() { - m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard) - m.Delete("", repo.DeleteProjectBoard) - m.Post("/default", repo.SetDefaultProjectBoard) + m.Put("", web.Bind(forms.EditBoardColumnForm{}), repo.EditBoardColumn) + m.Delete("", repo.DeleteBoardColumn) + m.Post("/default", repo.SetDefaultBoardColumn) m.Post("/move", repo.MoveIssues) }) }) - }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) - }, reqRepoProjectsReader, repo.MustEnableProjects) + }, reqRepoBoardsWriter, context.RepoMustNotBeArchived()) + }, reqRepoBoardsReader, repo.MustEnableBoards) m.Group("/wiki", func() { m.Combo("/"). diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 89a013d9af8d0..84e7c8b429b18 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -10,8 +10,8 @@ import ( "strings" "code.gitea.io/gitea/models" + board_model "code.gitea.io/gitea/models/board" issues_model "code.gitea.io/gitea/models/issues" - project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" @@ -145,7 +145,7 @@ type RepoSettingForm struct { TrackerIssueStyle string ExternalTrackerRegexpPattern string EnableCloseIssuesViaCommitInAnyBranch bool - EnableProjects bool + EnableBoards bool EnablePackages bool EnablePulls bool PullsIgnoreWhitespace bool @@ -434,7 +434,7 @@ type CreateIssueForm struct { AssigneeIDs string `form:"assignee_ids"` Ref string `form:"ref"` MilestoneID int64 - ProjectID int64 + BoardID int64 AssigneeID int64 Content string Files []string @@ -498,43 +498,29 @@ func (i IssueLockForm) HasValidReason() bool { return false } -// __________ __ __ -// \______ \_______ ____ |__| ____ _____/ |_ ______ -// | ___/\_ __ \/ _ \ | |/ __ \_/ ___\ __\/ ___/ -// | | | | \( <_> ) | \ ___/\ \___| | \___ \ -// |____| |__| \____/\__| |\___ >\___ >__| /____ > -// \______| \/ \/ \/ - -// CreateProjectForm form for creating a project -type CreateProjectForm struct { - Title string `binding:"Required;MaxSize(100)"` - Content string - BoardType project_model.BoardType +// CreateBoardForm form for creating a board +type CreateBoardForm struct { + Title string `binding:"Required;MaxSize(100)"` + Content string + ColumnType board_model.ColumnType } -// UserCreateProjectForm is a from for creating an individual or organization +// UserCreateBoardForm is a from for creating an individual or organization // form. -type UserCreateProjectForm struct { - Title string `binding:"Required;MaxSize(100)"` - Content string - BoardType project_model.BoardType - UID int64 `binding:"Required"` +type UserCreateBoardForm struct { + Title string `binding:"Required;MaxSize(100)"` + Content string + ColumnType board_model.ColumnType + UID int64 `binding:"Required"` } -// EditProjectBoardForm is a form for editing a project board -type EditProjectBoardForm struct { +// EditBoardColumnForm is a form for editing a board column +type EditBoardColumnForm struct { Title string `binding:"Required;MaxSize(100)"` Sorting int8 Color string `binding:"MaxSize(7)"` } -// _____ .__.__ __ -// / \ |__| | ____ _______/ |_ ____ ____ ____ -// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ -// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/ -// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ > -// \/ \/ \/ \/ \/ - // CreateMilestoneForm form for creating milestone type CreateMilestoneForm struct { Title string `binding:"Required;MaxSize(50)"` @@ -548,13 +534,6 @@ func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) b return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// .____ ___. .__ -// | | _____ \_ |__ ____ | | -// | | \__ \ | __ \_/ __ \| | -// | |___ / __ \| \_\ \ ___/| |__ -// |_______ (____ /___ /\___ >____/ -// \/ \/ \/ \/ - // CreateLabelForm form for creating label type CreateLabelForm struct { ID int64 @@ -580,13 +559,6 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// __________ .__ .__ __________ __ -// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - // MergePullRequestForm form for merging Pull Request // swagger:model MergePullRequestOption type MergePullRequestForm struct { diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go index 7eb800a020185..307ad341dfb86 100644 --- a/services/forms/user_form_hidden_comments.go +++ b/services/forms/user_form_hidden_comments.go @@ -64,8 +64,8 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{ /*29*/ issues_model.CommentTypePullRequestPush, }, "project": { - /*30*/ issues_model.CommentTypeProject, - /*31*/ issues_model.CommentTypeProjectBoard, + /*30*/ issues_model.CommentTypeBoard, + /*31*/ issues_model.CommentTypeBoardColumn, }, "issue_ref": { /*33*/ issues_model.CommentTypeChangeIssueRef, diff --git a/services/issue/issue.go b/services/issue/issue.go index b91ee4fc18b07..f0e6b87fdd7bb 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -7,10 +7,10 @@ import ( "fmt" activities_model "code.gitea.io/gitea/models/activities" + board_model "code.gitea.io/gitea/models/board" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" @@ -260,7 +260,7 @@ func deleteIssue(issue *issues_model.Issue) error { &issues_model.IssueWatch{}, &issues_model.Stopwatch{}, &issues_model.TrackedTime{}, - &project_model.ProjectIssue{}, + &board_model.BoardIssue{}, &repo_model.Attachment{}, &issues_model.PullRequest{}, ); err != nil { diff --git a/templates/repo/projects/list.tmpl b/templates/repo/boards/list.tmpl similarity index 85% rename from templates/repo/projects/list.tmpl rename to templates/repo/boards/list.tmpl index 274734f515a04..2b661cf0c04df 100644 --- a/templates/repo/projects/list.tmpl +++ b/templates/repo/boards/list.tmpl @@ -4,20 +4,20 @@