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