summaryrefslogtreecommitdiffstats
path: root/models/project
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2022-03-29 22:16:31 +0800
committerGitHub <noreply@github.com>2022-03-29 22:16:31 +0800
commitbd97736b9c7a16023bc9abf17be6157284f655b1 (patch)
treeeea5870d406bd00fa2e675901f24d9f1fbf1796f /models/project
parentea6efba9b3fd298c842a935266060e405e2ae4a3 (diff)
downloadgitea-bd97736b9c7a16023bc9abf17be6157284f655b1.tar.gz
gitea-bd97736b9c7a16023bc9abf17be6157284f655b1.zip
Move project files into models/project sub package (#17704)
* Move project files into models/project sub package * Fix test * Fix test * Fix test * Fix build * Fix test * Fix template bug * Fix bug * Fix lint * Fix test * Fix import * Improve codes Co-authored-by: 6543 <6543@obermui.de>
Diffstat (limited to 'models/project')
-rw-r--r--models/project/board.go289
-rw-r--r--models/project/issue.go100
-rw-r--r--models/project/main_test.go23
-rw-r--r--models/project/project.go347
-rw-r--r--models/project/project_test.go83
5 files changed, 842 insertions, 0 deletions
diff --git a/models/project/board.go b/models/project/board.go
new file mode 100644
index 0000000000..f770a18f59
--- /dev/null
+++ b/models/project/board.go
@@ -0,0 +1,289 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package project
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/builder"
+)
+
+type (
+ // BoardType is used to represent a project board type
+ BoardType uint8
+
+ // BoardList is a list of all project boards in a repository
+ BoardList []*Board
+)
+
+const (
+ // BoardTypeNone is a project board type that has no predefined columns
+ BoardTypeNone BoardType = iota
+
+ // BoardTypeBasicKanban is a project board type that has basic predefined columns
+ BoardTypeBasicKanban
+
+ // BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs
+ BoardTypeBugTriage
+)
+
+// BoardColorPattern is a regexp witch can validate BoardColor
+var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
+
+// Board is used to represent boards on a project
+type Board struct {
+ ID int64 `xorm:"pk autoincr"`
+ Title string
+ Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
+ Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
+ Color string `xorm:"VARCHAR(7)"`
+
+ ProjectID int64 `xorm:"INDEX NOT NULL"`
+ CreatorID int64 `xorm:"NOT NULL"`
+
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+}
+
+// TableName return the real table name
+func (Board) TableName() string {
+ return "project_board"
+}
+
+// NumIssues return counter of all issues assigned to the board
+func (b *Board) NumIssues() int {
+ c, err := db.GetEngine(db.DefaultContext).Table("project_issue").
+ Where("project_id=?", b.ProjectID).
+ And("project_board_id=?", b.ID).
+ GroupBy("issue_id").
+ Cols("issue_id").
+ Count()
+ if err != nil {
+ return 0
+ }
+ return int(c)
+}
+
+func init() {
+ db.RegisterModel(new(Board))
+}
+
+// IsBoardTypeValid checks if the project board type is valid
+func IsBoardTypeValid(p BoardType) bool {
+ switch p {
+ case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage:
+ return true
+ default:
+ return false
+ }
+}
+
+func createBoardsForProjectsType(ctx context.Context, project *Project) error {
+ var items []string
+
+ switch project.BoardType {
+
+ case BoardTypeBugTriage:
+ items = setting.Project.ProjectBoardBugTriageType
+
+ case BoardTypeBasicKanban:
+ items = setting.Project.ProjectBoardBasicKanbanType
+
+ case BoardTypeNone:
+ fallthrough
+ default:
+ return nil
+ }
+
+ if len(items) == 0 {
+ return nil
+ }
+
+ boards := make([]Board, 0, len(items))
+
+ for _, v := range items {
+ boards = append(boards, Board{
+ CreatedUnix: timeutil.TimeStampNow(),
+ CreatorID: project.CreatorID,
+ Title: v,
+ ProjectID: project.ID,
+ })
+ }
+
+ return db.Insert(ctx, boards)
+}
+
+// NewBoard adds a new project board to a given project
+func NewBoard(board *Board) error {
+ if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
+ return fmt.Errorf("bad color code: %s", board.Color)
+ }
+
+ _, err := db.GetEngine(db.DefaultContext).Insert(board)
+ return err
+}
+
+// DeleteBoardByID removes all issues references to the project board.
+func DeleteBoardByID(boardID int64) error {
+ ctx, committer, err := db.TxContext()
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := deleteBoardByID(ctx, boardID); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+func deleteBoardByID(ctx context.Context, boardID int64) error {
+ e := db.GetEngine(ctx)
+ board, err := getBoard(e, boardID)
+ if err != nil {
+ if IsErrProjectBoardNotExist(err) {
+ return nil
+ }
+
+ return err
+ }
+
+ if err = board.removeIssues(e); err != nil {
+ return err
+ }
+
+ if _, err := e.ID(board.ID).Delete(board); err != nil {
+ return err
+ }
+ return nil
+}
+
+func deleteBoardByProjectID(e db.Engine, projectID int64) error {
+ _, err := e.Where("project_id=?", projectID).Delete(&Board{})
+ return err
+}
+
+// GetBoard fetches the current board of a project
+func GetBoard(boardID int64) (*Board, error) {
+ return getBoard(db.GetEngine(db.DefaultContext), boardID)
+}
+
+func getBoard(e db.Engine, boardID int64) (*Board, error) {
+ board := new(Board)
+
+ has, err := e.ID(boardID).Get(board)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrProjectBoardNotExist{BoardID: boardID}
+ }
+
+ return board, nil
+}
+
+// UpdateBoard updates a project board
+func UpdateBoard(board *Board) error {
+ return updateBoard(db.GetEngine(db.DefaultContext), board)
+}
+
+func updateBoard(e db.Engine, board *Board) error {
+ var fieldToUpdate []string
+
+ if board.Sorting != 0 {
+ fieldToUpdate = append(fieldToUpdate, "sorting")
+ }
+
+ if board.Title != "" {
+ fieldToUpdate = append(fieldToUpdate, "title")
+ }
+
+ if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
+ return fmt.Errorf("bad color code: %s", board.Color)
+ }
+ fieldToUpdate = append(fieldToUpdate, "color")
+
+ _, err := e.ID(board.ID).Cols(fieldToUpdate...).Update(board)
+
+ return err
+}
+
+// GetBoards fetches all boards related to a project
+// if no default board set, first board is a temporary "Uncategorized" board
+func GetBoards(projectID int64) (BoardList, error) {
+ return getBoards(db.GetEngine(db.DefaultContext), projectID)
+}
+
+func getBoards(e db.Engine, projectID int64) ([]*Board, error) {
+ boards := make([]*Board, 0, 5)
+
+ if err := e.Where("project_id=? AND `default`=?", projectID, false).OrderBy("Sorting").Find(&boards); err != nil {
+ return nil, err
+ }
+
+ defaultB, err := getDefaultBoard(e, projectID)
+ if err != nil {
+ return nil, err
+ }
+
+ return append([]*Board{defaultB}, boards...), nil
+}
+
+// getDefaultBoard return default board and create a dummy if none exist
+func getDefaultBoard(e db.Engine, projectID int64) (*Board, error) {
+ var board Board
+ exist, err := e.Where("project_id=? AND `default`=?", projectID, true).Get(&board)
+ if err != nil {
+ return nil, err
+ }
+ if exist {
+ return &board, nil
+ }
+
+ // represents a board for issues not assigned to one
+ return &Board{
+ ProjectID: projectID,
+ Title: "Uncategorized",
+ Default: true,
+ }, nil
+}
+
+// SetDefaultBoard represents a board for issues not assigned to one
+// if boardID is 0 unset default
+func SetDefaultBoard(projectID, boardID int64) error {
+ _, err := db.GetEngine(db.DefaultContext).Where(builder.Eq{
+ "project_id": projectID,
+ "`default`": true,
+ }).Cols("`default`").Update(&Board{Default: false})
+ if err != nil {
+ return err
+ }
+
+ if boardID > 0 {
+ _, err = db.GetEngine(db.DefaultContext).ID(boardID).Where(builder.Eq{"project_id": projectID}).
+ Cols("`default`").Update(&Board{Default: true})
+ }
+
+ return err
+}
+
+// UpdateBoardSorting update project board sorting
+func UpdateBoardSorting(bs BoardList) error {
+ for i := range bs {
+ _, err := db.GetEngine(db.DefaultContext).ID(bs[i].ID).Cols(
+ "sorting",
+ ).Update(bs[i])
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/models/project/issue.go b/models/project/issue.go
new file mode 100644
index 0000000000..0976185c49
--- /dev/null
+++ b/models/project/issue.go
@@ -0,0 +1,100 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package project
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+)
+
+// ProjectIssue saves relation from issue to a project
+type ProjectIssue struct { //revive:disable-line:exported
+ ID int64 `xorm:"pk autoincr"`
+ IssueID int64 `xorm:"INDEX"`
+ ProjectID int64 `xorm:"INDEX"`
+
+ // If 0, then it has not been added to a specific board in the project
+ ProjectBoardID int64 `xorm:"INDEX"`
+}
+
+func init() {
+ db.RegisterModel(new(ProjectIssue))
+}
+
+func deleteProjectIssuesByProjectID(e db.Engine, projectID int64) error {
+ _, err := e.Where("project_id=?", projectID).Delete(&ProjectIssue{})
+ return err
+}
+
+// NumIssues return counter of all issues assigned to a project
+func (p *Project) NumIssues() int {
+ c, err := db.GetEngine(db.DefaultContext).Table("project_issue").
+ Where("project_id=?", p.ID).
+ GroupBy("issue_id").
+ Cols("issue_id").
+ Count()
+ if err != nil {
+ return 0
+ }
+ return int(c)
+}
+
+// NumClosedIssues return counter of closed issues assigned to a project
+func (p *Project) NumClosedIssues() int {
+ c, err := db.GetEngine(db.DefaultContext).Table("project_issue").
+ Join("INNER", "issue", "project_issue.issue_id=issue.id").
+ Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
+ Cols("issue_id").
+ Count()
+ if err != nil {
+ return 0
+ }
+ return int(c)
+}
+
+// NumOpenIssues return counter of open issues assigned to a project
+func (p *Project) NumOpenIssues() int {
+ c, err := db.GetEngine(db.DefaultContext).Table("project_issue").
+ Join("INNER", "issue", "project_issue.issue_id=issue.id").
+ Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).Count("issue.id")
+ if err != nil {
+ return 0
+ }
+ return int(c)
+}
+
+// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
+func MoveIssuesOnProjectBoard(board *Board, sortedIssueIDs map[int64]int64) error {
+ return db.WithTx(func(ctx context.Context) error {
+ sess := db.GetEngine(ctx)
+
+ issueIDs := make([]int64, 0, len(sortedIssueIDs))
+ for _, issueID := range sortedIssueIDs {
+ issueIDs = append(issueIDs, issueID)
+ }
+ count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
+ if err != nil {
+ return err
+ }
+ if int(count) != len(sortedIssueIDs) {
+ return fmt.Errorf("all issues have to be added to a project first")
+ }
+
+ for sorting, issueID := range sortedIssueIDs {
+ _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func (pb *Board) removeIssues(e db.Engine) error {
+ _, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", pb.ID)
+ return err
+}
diff --git a/models/project/main_test.go b/models/project/main_test.go
new file mode 100644
index 0000000000..5296a0f40e
--- /dev/null
+++ b/models/project/main_test.go
@@ -0,0 +1,23 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package project
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+
+ _ "code.gitea.io/gitea/models/repo"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m, filepath.Join("..", ".."),
+ "project.yml",
+ "project_board.yml",
+ "project_issue.yml",
+ "repository.yml",
+ )
+}
diff --git a/models/project/project.go b/models/project/project.go
new file mode 100644
index 0000000000..a639879e78
--- /dev/null
+++ b/models/project/project.go
@@ -0,0 +1,347 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package project
+
+import (
+ "context"
+ "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)
+}
+
+// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
+type ErrProjectBoardNotExist struct {
+ BoardID int64
+}
+
+// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
+func IsErrProjectBoardNotExist(err error) bool {
+ _, ok := err.(ErrProjectBoardNotExist)
+ return ok
+}
+
+func (err ErrProjectBoardNotExist) Error() string {
+ return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
+}
+
+// Project represents a project board
+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(opts SearchOptions) ([]*Project, int64, error) {
+ return GetProjectsCtx(db.DefaultContext, opts)
+}
+
+// GetProjectsCtx returns a list of all projects that have been created in the repository
+func GetProjectsCtx(ctx context.Context, opts SearchOptions) ([]*Project, int64, error) {
+ e := db.GetEngine(ctx)
+ projects := make([]*Project, 0, setting.UI.IssuePagingNum)
+
+ 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: %v", 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()
+ 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(id int64) (*Project, error) {
+ return getProjectByID(db.GetEngine(db.DefaultContext), id)
+}
+
+func getProjectByID(e db.Engine, id int64) (*Project, error) {
+ p := new(Project)
+
+ has, err := e.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(p *Project) error {
+ return updateProject(db.GetEngine(db.DefaultContext), p)
+}
+
+func updateProject(e db.Engine, p *Project) error {
+ _, err := e.ID(p.ID).Cols(
+ "title",
+ "description",
+ ).Update(p)
+ return err
+}
+
+func updateRepositoryProjectCount(e db.Engine, repoID int64) error {
+ if _, err := e.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 := e.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()
+ 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()
+ 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()
+ e := db.GetEngine(ctx)
+ count, err := e.ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p)
+ if err != nil {
+ return err
+ }
+ if count < 1 {
+ return nil
+ }
+
+ return updateRepositoryProjectCount(e, p.RepoID)
+}
+
+// DeleteProjectByID deletes a project from a repository.
+func DeleteProjectByID(id int64) error {
+ ctx, committer, err := db.TxContext()
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := DeleteProjectByIDCtx(ctx, id); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// DeleteProjectByIDCtx deletes a project from a repository.
+func DeleteProjectByIDCtx(ctx context.Context, id int64) error {
+ e := db.GetEngine(ctx)
+ p, err := getProjectByID(e, id)
+ if err != nil {
+ if IsErrProjectNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ if err := deleteProjectIssuesByProjectID(e, id); err != nil {
+ return err
+ }
+
+ if err := deleteBoardByProjectID(e, id); err != nil {
+ return err
+ }
+
+ if _, err = e.ID(p.ID).Delete(new(Project)); err != nil {
+ return err
+ }
+
+ return updateRepositoryProjectCount(e, p.RepoID)
+}
diff --git a/models/project/project_test.go b/models/project/project_test.go
new file mode 100644
index 0000000000..211a890874
--- /dev/null
+++ b/models/project/project_test.go
@@ -0,0 +1,83 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package project
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsProjectTypeValid(t *testing.T) {
+ const UnknownType Type = 15
+
+ cases := []struct {
+ typ Type
+ valid bool
+ }{
+ {TypeIndividual, false},
+ {TypeRepository, true},
+ {TypeOrganization, false},
+ {UnknownType, false},
+ }
+
+ for _, v := range cases {
+ assert.Equal(t, v.valid, IsTypeValid(v.typ))
+ }
+}
+
+func TestGetProjects(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ projects, _, err := GetProjects(SearchOptions{RepoID: 1})
+ assert.NoError(t, err)
+
+ // 1 value for this repo exists in the fixtures
+ assert.Len(t, projects, 1)
+
+ projects, _, err = GetProjects(SearchOptions{RepoID: 3})
+ assert.NoError(t, err)
+
+ // 1 value for this repo exists in the fixtures
+ assert.Len(t, projects, 1)
+}
+
+func TestProject(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ project := &Project{
+ Type: TypeRepository,
+ BoardType: BoardTypeBasicKanban,
+ Title: "New Project",
+ RepoID: 1,
+ CreatedUnix: timeutil.TimeStampNow(),
+ CreatorID: 2,
+ }
+
+ assert.NoError(t, NewProject(project))
+
+ _, err := GetProjectByID(project.ID)
+ assert.NoError(t, err)
+
+ // Update project
+ project.Title = "Updated title"
+ assert.NoError(t, UpdateProject(project))
+
+ projectFromDB, err := GetProjectByID(project.ID)
+ assert.NoError(t, err)
+
+ assert.Equal(t, project.Title, projectFromDB.Title)
+
+ assert.NoError(t, ChangeProjectStatus(project, true))
+
+ // Retrieve from DB afresh to check if it is truly closed
+ projectFromDB, err = GetProjectByID(project.ID)
+ assert.NoError(t, err)
+
+ assert.True(t, projectFromDB.IsClosed)
+}