Kanban board (#8346)
Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: jaqra <48099350+jaqra@users.noreply.github.com> Co-authored-by: Kerry <flatline-studios@users.noreply.github.com> Co-authored-by: Jaqra <jaqra@hotmail.com> Co-authored-by: Kyle Evans <kevans91@users.noreply.github.com> Co-authored-by: Tsakiridis Ilias <TsakiDev@users.noreply.github.com> Co-authored-by: Ilias Tsakiridis <ilias.tsakiridis@outlook.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
d285b5d35a
commit
4027c5dd7c
75 changed files with 3569 additions and 58 deletions
|
@ -1586,6 +1586,44 @@ func (err ErrLabelNotExist) Error() string {
|
|||
return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
|
||||
}
|
||||
|
||||
// __________ __ __
|
||||
// \______ \_______ ____ |__| ____ _____/ |_ ______
|
||||
// | ___/\_ __ \/ _ \ | |/ __ \_/ ___\ __\/ ___/
|
||||
// | | | | \( <_> ) | \ ___/\ \___| | \___ \
|
||||
// |____| |__| \____/\__| |\___ >\___ >__| /____ >
|
||||
// \______| \/ \/ \/
|
||||
|
||||
// ErrProjectNotExist represents a "ProjectNotExist" kind of error.
|
||||
type ErrProjectNotExist struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
// IsErrProjectNotExist checks if an error is a ErrProjectNotExist
|
||||
func IsErrProjectNotExist(err error) bool {
|
||||
_, ok := err.(ErrProjectNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrProjectNotExist) Error() string {
|
||||
return fmt.Sprintf("projects does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
|
||||
type ErrProjectBoardNotExist struct {
|
||||
BoardID int64
|
||||
}
|
||||
|
||||
// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
|
||||
func IsErrProjectBoardNotExist(err error) bool {
|
||||
_, ok := err.(ErrProjectBoardNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrProjectBoardNotExist) Error() string {
|
||||
return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
|
||||
}
|
||||
|
||||
// _____ .__.__ __
|
||||
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
||||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
||||
|
|
26
models/fixtures/project.yml
Normal file
26
models/fixtures/project.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
-
|
||||
id: 1
|
||||
title: First project
|
||||
repo_id: 1
|
||||
is_closed: false
|
||||
creator_id: 2
|
||||
board_type: 1
|
||||
type: 2
|
||||
|
||||
-
|
||||
id: 2
|
||||
title: second project
|
||||
repo_id: 3
|
||||
is_closed: false
|
||||
creator_id: 3
|
||||
board_type: 1
|
||||
type: 2
|
||||
|
||||
-
|
||||
id: 3
|
||||
title: project on repo with disabled project
|
||||
repo_id: 4
|
||||
is_closed: true
|
||||
creator_id: 5
|
||||
board_type: 1
|
||||
type: 2
|
23
models/fixtures/project_board.yml
Normal file
23
models/fixtures/project_board.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
-
|
||||
id: 1
|
||||
project_id: 1
|
||||
title: To Do
|
||||
creator_id: 2
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 2
|
||||
project_id: 1
|
||||
title: In Progress
|
||||
creator_id: 2
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
||||
|
||||
-
|
||||
id: 3
|
||||
project_id: 1
|
||||
title: Done
|
||||
creator_id: 2
|
||||
created_unix: 1588117528
|
||||
updated_unix: 1588117528
|
23
models/fixtures/project_issue.yml
Normal file
23
models/fixtures/project_issue.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
-
|
||||
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
|
|
@ -514,3 +514,21 @@
|
|||
type: 3
|
||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 75
|
||||
repo_id: 1
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 76
|
||||
repo_id: 2
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 77
|
||||
repo_id: 3
|
||||
type: 8
|
||||
created_unix: 946684810
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
num_milestones: 3
|
||||
num_closed_milestones: 1
|
||||
num_watches: 4
|
||||
num_projects: 1
|
||||
num_closed_projects: 0
|
||||
status: 0
|
||||
|
||||
-
|
||||
|
@ -42,6 +44,8 @@
|
|||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_watches: 0
|
||||
num_projects: 1
|
||||
num_closed_projects: 0
|
||||
status: 0
|
||||
|
||||
-
|
||||
|
@ -56,6 +60,8 @@
|
|||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_stars: 1
|
||||
num_projects: 0
|
||||
num_closed_projects: 1
|
||||
status: 0
|
||||
|
||||
-
|
||||
|
|
|
@ -41,6 +41,7 @@ type Issue struct {
|
|||
Labels []*Label `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
Project *Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *User `xorm:"-"`
|
||||
|
@ -274,6 +275,10 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
if err = issue.loadProject(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue.loadAssignees(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -1062,6 +1067,8 @@ type IssuesOptions struct {
|
|||
PosterID int64
|
||||
MentionedID int64
|
||||
MilestoneIDs []int64
|
||||
ProjectID int64
|
||||
ProjectBoardID int64
|
||||
IsClosed util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
LabelIDs []int64
|
||||
|
@ -1147,6 +1154,19 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
|
|||
sess.In("issue.milestone_id", opts.MilestoneIDs)
|
||||
}
|
||||
|
||||
if opts.ProjectID > 0 {
|
||||
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
|
||||
And("project_issue.project_id=?", opts.ProjectID)
|
||||
}
|
||||
|
||||
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}))
|
||||
} else {
|
||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
|
||||
}
|
||||
}
|
||||
|
||||
switch opts.IsPull {
|
||||
case util.OptionalBoolTrue:
|
||||
sess.And("issue.is_pull=?", true)
|
||||
|
@ -1953,6 +1973,11 @@ func deleteIssuesByRepoID(sess Engine, repoID int64) (attachmentPaths []string,
|
|||
return
|
||||
}
|
||||
|
||||
if _, err = sess.In("issue_id", deleteCond).
|
||||
Delete(&ProjectIssue{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var attachments []*Attachment
|
||||
if err = sess.In("issue_id", deleteCond).
|
||||
Find(&attachments); err != nil {
|
||||
|
|
|
@ -97,6 +97,10 @@ const (
|
|||
CommentTypeMergePull
|
||||
// push to PR head branch
|
||||
CommentTypePullPush
|
||||
// Project changed
|
||||
CommentTypeProject
|
||||
// Project board changed
|
||||
CommentTypeProjectBoard
|
||||
)
|
||||
|
||||
// CommentTag defines comment tag type
|
||||
|
@ -122,6 +126,10 @@ type Comment struct {
|
|||
Issue *Issue `xorm:"-"`
|
||||
LabelID int64
|
||||
Label *Label `xorm:"-"`
|
||||
OldProjectID int64
|
||||
ProjectID int64
|
||||
OldProject *Project `xorm:"-"`
|
||||
Project *Project `xorm:"-"`
|
||||
OldMilestoneID int64
|
||||
MilestoneID int64
|
||||
OldMilestone *Milestone `xorm:"-"`
|
||||
|
@ -389,6 +397,32 @@ 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
|
||||
has, err := x.ID(c.OldProjectID).Get(&oldProject)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
c.OldProject = &oldProject
|
||||
}
|
||||
}
|
||||
|
||||
if c.ProjectID > 0 {
|
||||
var project Project
|
||||
has, err := x.ID(c.ProjectID).Get(&project)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
c.Project = &project
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
|
||||
func (c *Comment) LoadMilestone() error {
|
||||
if c.OldMilestoneID > 0 {
|
||||
|
@ -647,6 +681,8 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
|||
LabelID: LabelID,
|
||||
OldMilestoneID: opts.OldMilestoneID,
|
||||
MilestoneID: opts.MilestoneID,
|
||||
OldProjectID: opts.OldProjectID,
|
||||
ProjectID: opts.ProjectID,
|
||||
RemovedAssignee: opts.RemovedAssignee,
|
||||
AssigneeID: opts.AssigneeID,
|
||||
CommitID: opts.CommitID,
|
||||
|
@ -810,6 +846,8 @@ type CreateCommentOptions struct {
|
|||
DependentIssueID int64
|
||||
OldMilestoneID int64
|
||||
MilestoneID int64
|
||||
OldProjectID int64
|
||||
ProjectID int64
|
||||
AssigneeID int64
|
||||
RemovedAssignee bool
|
||||
OldTitle string
|
||||
|
|
|
@ -183,6 +183,33 @@ func updateMilestoneCompleteness(e Engine, milestoneID int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
|
||||
func ChangeMilestoneStatusByRepoIDAndID(repoID, milestoneID int64, isClosed bool) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := &Milestone{
|
||||
ID: milestoneID,
|
||||
RepoID: repoID,
|
||||
}
|
||||
|
||||
has, err := sess.ID(milestoneID).Where("repo_id = ?", repoID).Get(m)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrMilestoneNotExist{ID: milestoneID, RepoID: repoID}
|
||||
}
|
||||
|
||||
if err := changeMilestoneStatus(sess, m, isClosed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// ChangeMilestoneStatus changes the milestone open/closed status.
|
||||
func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
|
||||
sess := x.NewSession()
|
||||
|
@ -191,20 +218,27 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := changeMilestoneStatus(sess, m, isClosed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func changeMilestoneStatus(e Engine, m *Milestone, isClosed bool) error {
|
||||
m.IsClosed = isClosed
|
||||
if isClosed {
|
||||
m.ClosedDateUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
if _, err := sess.ID(m.ID).Cols("is_closed", "closed_date_unix").Update(m); err != nil {
|
||||
count, err := e.ID(m.ID).Where("repo_id = ? AND is_closed = ?", m.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil {
|
||||
return err
|
||||
if count < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
return updateRepoMilestoneNum(e, m.RepoID)
|
||||
}
|
||||
|
||||
func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
|
||||
|
|
|
@ -224,6 +224,8 @@ var migrations = []Migration{
|
|||
NewMigration("update Matrix Webhook http method to 'PUT'", updateMatrixWebhookHTTPMethod),
|
||||
// v145 -> v146
|
||||
NewMigration("Increase Language field to 50 in LanguageStats", increaseLanguageField),
|
||||
// v146 -> v147
|
||||
NewMigration("Add projects info to repository table", addProjectsInfo),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
85
models/migrations/v146.go
Normal file
85
models/migrations/v146.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// 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 migrations
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addProjectsInfo(x *xorm.Engine) error {
|
||||
|
||||
// Create new tables
|
||||
type (
|
||||
ProjectType uint8
|
||||
ProjectBoardType uint8
|
||||
)
|
||||
|
||||
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 ProjectBoardType
|
||||
Type ProjectType
|
||||
|
||||
ClosedDateUnix timeutil.TimeStamp
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Project)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
OldProjectID int64
|
||||
ProjectID int64
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Comment)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
ID int64
|
||||
NumProjects int `xorm:"NOT NULL DEFAULT 0"`
|
||||
NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Repository)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ProjectIssue saves relation from issue to a project
|
||||
type ProjectIssue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
ProjectBoardID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(ProjectIssue)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ProjectBoard struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Title string
|
||||
Default bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
ProjectID int64 `xorm:"INDEX NOT NULL"`
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(ProjectBoard))
|
||||
}
|
|
@ -45,6 +45,7 @@ type Engine interface {
|
|||
SQL(interface{}, ...interface{}) *xorm.Session
|
||||
Where(interface{}, ...interface{}) *xorm.Session
|
||||
Asc(colNames ...string) *xorm.Session
|
||||
Desc(colNames ...string) *xorm.Session
|
||||
Limit(limit int, start ...int) *xorm.Session
|
||||
SumInt(bean interface{}, columnName string) (res int64, err error)
|
||||
}
|
||||
|
@ -125,6 +126,9 @@ func init() {
|
|||
new(Task),
|
||||
new(LanguageStat),
|
||||
new(EmailHash),
|
||||
new(Project),
|
||||
new(ProjectBoard),
|
||||
new(ProjectIssue),
|
||||
)
|
||||
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
|
|
307
models/project.go
Normal file
307
models/project.go
Normal file
|
@ -0,0 +1,307 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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 ProjectBoardType
|
||||
Translation string
|
||||
}
|
||||
|
||||
// ProjectType is used to identify the type of project in question and ownership
|
||||
ProjectType uint8
|
||||
)
|
||||
|
||||
const (
|
||||
// ProjectTypeIndividual is a type of project board that is owned by an individual
|
||||
ProjectTypeIndividual ProjectType = iota + 1
|
||||
|
||||
// ProjectTypeRepository is a project that is tied to a repository
|
||||
ProjectTypeRepository
|
||||
|
||||
// ProjectTypeOrganization is a project that is tied to an organisation
|
||||
ProjectTypeOrganization
|
||||
)
|
||||
|
||||
// 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 ProjectBoardType
|
||||
Type ProjectType
|
||||
|
||||
RenderedContent string `xorm:"-"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ClosedDateUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
// GetProjectsConfig retrieves the types of configurations projects could have
|
||||
func GetProjectsConfig() []ProjectsConfig {
|
||||
return []ProjectsConfig{
|
||||
{ProjectBoardTypeNone, "repo.projects.type.none"},
|
||||
{ProjectBoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
|
||||
{ProjectBoardTypeBugTriage, "repo.projects.type.bug_triage"},
|
||||
}
|
||||
}
|
||||
|
||||
// IsProjectTypeValid checks if a project type is valid
|
||||
func IsProjectTypeValid(p ProjectType) bool {
|
||||
switch p {
|
||||
case ProjectTypeRepository:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ProjectSearchOptions are options for GetProjects
|
||||
type ProjectSearchOptions struct {
|
||||
RepoID int64
|
||||
Page int
|
||||
IsClosed util.OptionalBool
|
||||
SortType string
|
||||
Type ProjectType
|
||||
}
|
||||
|
||||
// GetProjects returns a list of all projects that have been created in the repository
|
||||
func GetProjects(opts ProjectSearchOptions) ([]*Project, int64, error) {
|
||||
return getProjects(x, opts)
|
||||
}
|
||||
|
||||
func getProjects(e Engine, opts ProjectSearchOptions) ([]*Project, int64, error) {
|
||||
|
||||
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 !IsProjectBoardTypeValid(p.BoardType) {
|
||||
p.BoardType = ProjectBoardTypeNone
|
||||
}
|
||||
|
||||
if !IsProjectTypeValid(p.Type) {
|
||||
return errors.New("project type is not valid")
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createBoardsForProjectsType(sess, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// GetProjectByID returns the projects in a repository
|
||||
func GetProjectByID(id int64) (*Project, error) {
|
||||
return getProjectByID(x, id)
|
||||
}
|
||||
|
||||
func getProjectByID(e 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(x, p)
|
||||
}
|
||||
|
||||
func updateProject(e Engine, p *Project) error {
|
||||
_, err := e.ID(p.ID).Cols(
|
||||
"title",
|
||||
"description",
|
||||
).Update(p)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateRepositoryProjectCount(e 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`": ProjectTypeRepository})),
|
||||
}).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`": ProjectTypeRepository}).
|
||||
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 {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := new(Project)
|
||||
|
||||
has, err := sess.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(sess, p, isClosed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// ChangeProjectStatus toggle a project between opened and closed
|
||||
func ChangeProjectStatus(p *Project, isClosed bool) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := changeProjectStatus(sess, p, isClosed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func changeProjectStatus(e Engine, p *Project, isClosed bool) error {
|
||||
p.IsClosed = isClosed
|
||||
p.ClosedDateUnix = timeutil.TimeStampNow()
|
||||
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 {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteProjectByID(sess, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func deleteProjectByID(e Engine, id int64) error {
|
||||
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 := deleteProjectBoardByProjectID(e, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = e.ID(p.ID).Delete(new(Project)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateRepositoryProjectCount(e, p.RepoID)
|
||||
}
|
220
models/project_board.go
Normal file
220
models/project_board.go
Normal file
|
@ -0,0 +1,220 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type (
|
||||
// ProjectBoardType is used to represent a project board type
|
||||
ProjectBoardType uint8
|
||||
|
||||
// ProjectBoardList is a list of all project boards in a repository
|
||||
ProjectBoardList []*ProjectBoard
|
||||
)
|
||||
|
||||
const (
|
||||
// ProjectBoardTypeNone is a project board type that has no predefined columns
|
||||
ProjectBoardTypeNone ProjectBoardType = iota
|
||||
|
||||
// ProjectBoardTypeBasicKanban is a project board type that has basic predefined columns
|
||||
ProjectBoardTypeBasicKanban
|
||||
|
||||
// ProjectBoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs
|
||||
ProjectBoardTypeBugTriage
|
||||
)
|
||||
|
||||
// ProjectBoard is used to represent boards on a project
|
||||
type ProjectBoard struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Title string
|
||||
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
|
||||
|
||||
ProjectID int64 `xorm:"INDEX NOT NULL"`
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
|
||||
Issues []*Issue `xorm:"-"`
|
||||
}
|
||||
|
||||
// IsProjectBoardTypeValid checks if the project board type is valid
|
||||
func IsProjectBoardTypeValid(p ProjectBoardType) bool {
|
||||
switch p {
|
||||
case ProjectBoardTypeNone, ProjectBoardTypeBasicKanban, ProjectBoardTypeBugTriage:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func createBoardsForProjectsType(sess *xorm.Session, project *Project) error {
|
||||
|
||||
var items []string
|
||||
|
||||
switch project.BoardType {
|
||||
|
||||
case ProjectBoardTypeBugTriage:
|
||||
items = setting.Project.ProjectBoardBugTriageType
|
||||
|
||||
case ProjectBoardTypeBasicKanban:
|
||||
items = setting.Project.ProjectBoardBasicKanbanType
|
||||
|
||||
case ProjectBoardTypeNone:
|
||||
fallthrough
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var boards = make([]ProjectBoard, 0, len(items))
|
||||
|
||||
for _, v := range items {
|
||||
boards = append(boards, ProjectBoard{
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
CreatorID: project.CreatorID,
|
||||
Title: v,
|
||||
ProjectID: project.ID,
|
||||
})
|
||||
}
|
||||
|
||||
_, err := sess.Insert(boards)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewProjectBoard adds a new project board to a given project
|
||||
func NewProjectBoard(board *ProjectBoard) error {
|
||||
_, err := x.Insert(board)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteProjectBoardByID removes all issues references to the project board.
|
||||
func DeleteProjectBoardByID(boardID int64) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteProjectBoardByID(sess, boardID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func deleteProjectBoardByID(e Engine, boardID int64) error {
|
||||
board, err := getProjectBoard(e, boardID)
|
||||
if err != nil {
|
||||
if IsErrProjectBoardNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err = board.removeIssues(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := e.ID(board.ID).Delete(board); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteProjectBoardByProjectID(e Engine, projectID int64) error {
|
||||
_, err := e.Where("project_id=?", projectID).Delete(&ProjectBoard{})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProjectBoard fetches the current board of a project
|
||||
func GetProjectBoard(boardID int64) (*ProjectBoard, error) {
|
||||
return getProjectBoard(x, boardID)
|
||||
}
|
||||
|
||||
func getProjectBoard(e Engine, boardID int64) (*ProjectBoard, error) {
|
||||
board := new(ProjectBoard)
|
||||
|
||||
has, err := e.ID(boardID).Get(board)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectBoardNotExist{BoardID: boardID}
|
||||
}
|
||||
|
||||
return board, nil
|
||||
}
|
||||
|
||||
// UpdateProjectBoard updates the title of a project board
|
||||
func UpdateProjectBoard(board *ProjectBoard) error {
|
||||
return updateProjectBoard(x, board)
|
||||
}
|
||||
|
||||
func updateProjectBoard(e Engine, board *ProjectBoard) error {
|
||||
_, err := e.ID(board.ID).Cols(
|
||||
"title",
|
||||
"default",
|
||||
).Update(board)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProjectBoards fetches all boards related to a project
|
||||
func GetProjectBoards(projectID int64) ([]*ProjectBoard, error) {
|
||||
|
||||
var boards = make([]*ProjectBoard, 0, 5)
|
||||
|
||||
sess := x.Where("project_id=?", projectID)
|
||||
return boards, sess.Find(&boards)
|
||||
}
|
||||
|
||||
// GetUncategorizedBoard represents a board for issues not assigned to one
|
||||
func GetUncategorizedBoard(projectID int64) (*ProjectBoard, error) {
|
||||
return &ProjectBoard{
|
||||
ProjectID: projectID,
|
||||
Title: "Uncategorized",
|
||||
Default: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadIssues load issues assigned to this board
|
||||
func (b *ProjectBoard) LoadIssues() (IssueList, error) {
|
||||
var boardID int64
|
||||
if !b.Default {
|
||||
boardID = b.ID
|
||||
|
||||
} else {
|
||||
// Issues without ProjectBoardID
|
||||
boardID = -1
|
||||
}
|
||||
issues, err := Issues(&IssuesOptions{
|
||||
ProjectBoardID: boardID,
|
||||
ProjectID: b.ProjectID,
|
||||
})
|
||||
b.Issues = issues
|
||||
return issues, err
|
||||
}
|
||||
|
||||
// LoadIssues load issues assigned to the boards
|
||||
func (bs ProjectBoardList) LoadIssues() (IssueList, error) {
|
||||
issues := make(IssueList, 0, len(bs)*10)
|
||||
for i := range bs {
|
||||
il, err := bs[i].LoadIssues()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bs[i].Issues = il
|
||||
issues = append(issues, il...)
|
||||
}
|
||||
return issues, nil
|
||||
}
|
210
models/project_issue.go
Normal file
210
models/project_issue.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// ProjectIssue saves relation from issue to a project
|
||||
type ProjectIssue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
|
||||
// If 0, then it has not been added to a specific board in the project
|
||||
ProjectBoardID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func deleteProjectIssuesByProjectID(e Engine, projectID int64) error {
|
||||
_, err := e.Where("project_id=?", projectID).Delete(&ProjectIssue{})
|
||||
return err
|
||||
}
|
||||
|
||||
// ___
|
||||
// |_ _|___ ___ _ _ ___
|
||||
// | |/ __/ __| | | |/ _ \
|
||||
// | |\__ \__ \ |_| | __/
|
||||
// |___|___/___/\__,_|\___|
|
||||
|
||||
// LoadProject load the project the issue was assigned to
|
||||
func (i *Issue) LoadProject() (err error) {
|
||||
return i.loadProject(x)
|
||||
}
|
||||
|
||||
func (i *Issue) loadProject(e Engine) (err error) {
|
||||
if i.Project == nil {
|
||||
var p Project
|
||||
if _, err = e.Table("project").
|
||||
Join("INNER", "project_issue", "project.id=project_issue.project_id").
|
||||
Where("project_issue.issue_id = ?", i.ID).
|
||||
Get(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Project = &p
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ProjectID return project id if issue was assigned to one
|
||||
func (i *Issue) ProjectID() int64 {
|
||||
return i.projectID(x)
|
||||
}
|
||||
|
||||
func (i *Issue) projectID(e Engine) int64 {
|
||||
var ip ProjectIssue
|
||||
has, err := e.Where("issue_id=?", i.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
}
|
||||
return ip.ProjectID
|
||||
}
|
||||
|
||||
// ProjectBoardID return project board id if issue was assigned to one
|
||||
func (i *Issue) ProjectBoardID() int64 {
|
||||
return i.projectBoardID(x)
|
||||
}
|
||||
|
||||
func (i *Issue) projectBoardID(e Engine) int64 {
|
||||
var ip ProjectIssue
|
||||
has, err := e.Where("issue_id=?", i.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
}
|
||||
return ip.ProjectBoardID
|
||||
}
|
||||
|
||||
// ____ _ _
|
||||
// | _ \ _ __ ___ (_) ___ ___| |_
|
||||
// | |_) | '__/ _ \| |/ _ \/ __| __|
|
||||
// | __/| | | (_) | | __/ (__| |_
|
||||
// |_| |_| \___// |\___|\___|\__|
|
||||
// |__/
|
||||
|
||||
// NumIssues return counter of all issues assigned to a project
|
||||
func (p *Project) NumIssues() int {
|
||||
c, err := x.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 := x.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 := x.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)
|
||||
}
|
||||
|
||||
// ChangeProjectAssign changes the project associated with an issue
|
||||
func ChangeProjectAssign(issue *Issue, doer *User, newProjectID int64) error {
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := addUpdateIssueProject(sess, issue, doer, newProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func addUpdateIssueProject(e *xorm.Session, issue *Issue, doer *User, newProjectID int64) error {
|
||||
|
||||
oldProjectID := issue.projectID(e)
|
||||
|
||||
if _, err := e.Where("project_issue.issue_id=?", issue.ID).Delete(&ProjectIssue{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := issue.loadRepo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldProjectID > 0 || newProjectID > 0 {
|
||||
if _, err := createComment(e, &CreateCommentOptions{
|
||||
Type: CommentTypeProject,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldProjectID: oldProjectID,
|
||||
ProjectID: newProjectID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := e.Insert(&ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ____ _ _ ____ _
|
||||
// | _ \ _ __ ___ (_) ___ ___| |_| __ ) ___ __ _ _ __ __| |
|
||||
// | |_) | '__/ _ \| |/ _ \/ __| __| _ \ / _ \ / _` | '__/ _` |
|
||||
// | __/| | | (_) | | __/ (__| |_| |_) | (_) | (_| | | | (_| |
|
||||
// |_| |_| \___// |\___|\___|\__|____/ \___/ \__,_|_| \__,_|
|
||||
// |__/
|
||||
|
||||
// MoveIssueAcrossProjectBoards move a card from one board to another
|
||||
func MoveIssueAcrossProjectBoards(issue *Issue, board *ProjectBoard) error {
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pis ProjectIssue
|
||||
has, err := sess.Where("issue_id=?", issue.ID).Get(&pis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has {
|
||||
return fmt.Errorf("issue has to be added to a project first")
|
||||
}
|
||||
|
||||
pis.ProjectBoardID = board.ID
|
||||
if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func (pb *ProjectBoard) removeIssues(e Engine) error {
|
||||
_, err := e.Exec("UPDATE `project_issue` SET project_board_id = 0 WHERE project_board_id = ? ", pb.ID)
|
||||
return err
|
||||
}
|
82
models/project_test.go
Normal file
82
models/project_test.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsProjectTypeValid(t *testing.T) {
|
||||
const UnknownType ProjectType = 15
|
||||
|
||||
var cases = []struct {
|
||||
typ ProjectType
|
||||
valid bool
|
||||
}{
|
||||
{ProjectTypeIndividual, false},
|
||||
{ProjectTypeRepository, true},
|
||||
{ProjectTypeOrganization, false},
|
||||
{UnknownType, false},
|
||||
}
|
||||
|
||||
for _, v := range cases {
|
||||
assert.Equal(t, v.valid, IsProjectTypeValid(v.typ))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProjects(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
|
||||
projects, _, err := GetProjects(ProjectSearchOptions{RepoID: 1})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 1 value for this repo exists in the fixtures
|
||||
assert.Len(t, projects, 1)
|
||||
|
||||
projects, _, err = GetProjects(ProjectSearchOptions{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, PrepareTestDatabase())
|
||||
|
||||
project := &Project{
|
||||
Type: ProjectTypeRepository,
|
||||
BoardType: ProjectBoardTypeBasicKanban,
|
||||
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)
|
||||
}
|
|
@ -168,6 +168,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:"-"`
|
||||
|
||||
IsPrivate bool `xorm:"INDEX"`
|
||||
IsEmpty bool `xorm:"INDEX"`
|
||||
|
@ -237,6 +240,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
|
||||
}
|
||||
|
||||
// MustOwner always returns a valid *User object to avoid
|
||||
|
@ -307,6 +311,8 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
|
|||
parent = repo.BaseRepo.innerAPIFormat(e, mode, true)
|
||||
}
|
||||
}
|
||||
|
||||
//check enabled/disabled units
|
||||
hasIssues := false
|
||||
var externalTracker *api.ExternalTracker
|
||||
var internalTracker *api.InternalTracker
|
||||
|
@ -353,6 +359,10 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
|
|||
allowRebaseMerge = config.AllowRebaseMerge
|
||||
allowSquash = config.AllowSquash
|
||||
}
|
||||
hasProjects := false
|
||||
if _, err := repo.getUnit(e, UnitTypeProjects); err == nil {
|
||||
hasProjects = true
|
||||
}
|
||||
|
||||
repo.mustOwner(e)
|
||||
|
||||
|
@ -390,6 +400,7 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
|
|||
ExternalTracker: externalTracker,
|
||||
InternalTracker: internalTracker,
|
||||
HasWiki: hasWiki,
|
||||
HasProjects: hasProjects,
|
||||
ExternalWiki: externalWiki,
|
||||
HasPullRequests: hasPullRequests,
|
||||
IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
|
||||
|
@ -1641,6 +1652,18 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
|
|||
}
|
||||
}
|
||||
|
||||
projects, _, err := getProjects(sess, ProjectSearchOptions{
|
||||
RepoID: repoID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get projects: %v", err)
|
||||
}
|
||||
for i := range projects {
|
||||
if err := deleteProjectByID(sess, projects[i].ID); err != nil {
|
||||
return fmt.Errorf("delete project [%d]: %v", projects[i].ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Remove repository files should be executed after transaction succeed.
|
||||
repoPath := repo.RepoPath()
|
||||
removeAllWithNotice(sess, "Delete repository files", repoPath)
|
||||
|
|
|
@ -118,7 +118,7 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
|||
switch colName {
|
||||
case "type":
|
||||
switch UnitType(Cell2Int64(val)) {
|
||||
case UnitTypeCode, UnitTypeReleases, UnitTypeWiki:
|
||||
case UnitTypeCode, UnitTypeReleases, UnitTypeWiki, UnitTypeProjects:
|
||||
r.Config = new(UnitConfig)
|
||||
case UnitTypeExternalWiki:
|
||||
r.Config = new(ExternalWikiConfig)
|
||||
|
|
|
@ -24,6 +24,7 @@ const (
|
|||
UnitTypeWiki // 5 Wiki
|
||||
UnitTypeExternalWiki // 6 ExternalWiki
|
||||
UnitTypeExternalTracker // 7 ExternalTracker
|
||||
UnitTypeProjects // 8 Kanban board
|
||||
)
|
||||
|
||||
// Value returns integer value for unit type
|
||||
|
@ -47,6 +48,8 @@ func (u UnitType) String() string {
|
|||
return "UnitTypeExternalWiki"
|
||||
case UnitTypeExternalTracker:
|
||||
return "UnitTypeExternalTracker"
|
||||
case UnitTypeProjects:
|
||||
return "UnitTypeProjects"
|
||||
}
|
||||
return fmt.Sprintf("Unknown UnitType %d", u)
|
||||
}
|
||||
|
@ -68,6 +71,7 @@ var (
|
|||
UnitTypeWiki,
|
||||
UnitTypeExternalWiki,
|
||||
UnitTypeExternalTracker,
|
||||
UnitTypeProjects,
|
||||
}
|
||||
|
||||
// DefaultRepoUnits contains the default unit types
|
||||
|
@ -77,6 +81,7 @@ var (
|
|||
UnitTypePullRequests,
|
||||
UnitTypeReleases,
|
||||
UnitTypeWiki,
|
||||
UnitTypeProjects,
|
||||
}
|
||||
|
||||
// NotAllowedDefaultRepoUnits contains units that can't be default
|
||||
|
@ -242,6 +247,14 @@ var (
|
|||
4,
|
||||
}
|
||||
|
||||
UnitProjects = Unit{
|
||||
UnitTypeProjects,
|
||||
"repo.projects",
|
||||
"/projects",
|
||||
"repo.projects.desc",
|
||||
5,
|
||||
}
|
||||
|
||||
// Units contains all the units
|
||||
Units = map[UnitType]Unit{
|
||||
UnitTypeCode: UnitCode,
|
||||
|
@ -251,6 +264,7 @@ var (
|
|||
UnitTypeReleases: UnitReleases,
|
||||
UnitTypeWiki: UnitWiki,
|
||||
UnitTypeExternalWiki: UnitExternalWiki,
|
||||
UnitTypeProjects: UnitProjects,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue