2020-08-17 11:07:38 +08:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-28 02:20:29 +08:00
// SPDX-License-Identifier: MIT
2020-08-17 11:07:38 +08:00
2022-03-29 22:16:31 +08:00
package project
2020-08-17 11:07:38 +08:00
import (
2022-03-29 22:16:31 +08:00
"context"
2020-08-17 11:07:38 +08:00
"fmt"
2024-03-01 15:11:51 +08:00
"html/template"
2020-08-17 11:07:38 +08:00
2021-09-19 19:49:59 +08:00
"code.gitea.io/gitea/models/db"
2023-01-20 19:42:33 +08:00
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
2024-03-02 23:42:31 +08:00
"code.gitea.io/gitea/modules/optional"
2020-08-17 11:07:38 +08:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
type (
2023-02-11 16:12:41 +08:00
// BoardConfig is used to identify the type of board that is being created
BoardConfig struct {
2022-03-29 22:16:31 +08:00
BoardType BoardType
2020-08-17 11:07:38 +08:00
Translation string
}
2023-02-11 16:12:41 +08:00
// CardConfig is used to identify the type of board card that is being used
CardConfig struct {
CardType CardType
Translation string
}
2022-03-29 22:16:31 +08:00
// Type is used to identify the type of project in question and ownership
Type uint8
2020-08-17 11:07:38 +08:00
)
const (
2022-03-29 22:16:31 +08:00
// TypeIndividual is a type of project board that is owned by an individual
TypeIndividual Type = iota + 1
2020-08-17 11:07:38 +08:00
2022-03-29 22:16:31 +08:00
// TypeRepository is a project that is tied to a repository
TypeRepository
2020-08-17 11:07:38 +08:00
2022-03-29 22:16:31 +08:00
// TypeOrganization is a project that is tied to an organisation
TypeOrganization
2020-08-17 11:07:38 +08:00
)
2022-03-29 22:16:31 +08:00
// 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 )
}
2022-10-18 13:50:37 +08:00
func ( err ErrProjectNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-03-29 22:16:31 +08:00
// 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 )
}
2022-10-18 13:50:37 +08:00
func ( err ErrProjectBoardNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2020-08-17 11:07:38 +08:00
// Project represents a project board
type Project struct {
2023-01-20 19:42:33 +08:00
ID int64 ` xorm:"pk autoincr" `
Title string ` xorm:"INDEX NOT NULL" `
Description string ` xorm:"TEXT" `
OwnerID int64 ` xorm:"INDEX" `
Owner * user_model . User ` xorm:"-" `
RepoID int64 ` xorm:"INDEX" `
Repo * repo_model . Repository ` xorm:"-" `
CreatorID int64 ` xorm:"NOT NULL" `
IsClosed bool ` xorm:"INDEX" `
2022-03-29 22:16:31 +08:00
BoardType BoardType
2023-02-11 16:12:41 +08:00
CardType CardType
2022-03-29 22:16:31 +08:00
Type Type
2020-08-17 11:07:38 +08:00
2024-03-01 15:11:51 +08:00
RenderedContent template . HTML ` xorm:"-" `
2020-08-17 11:07:38 +08:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
ClosedDateUnix timeutil . TimeStamp
}
2023-01-20 19:42:33 +08:00
func ( p * Project ) LoadOwner ( ctx context . Context ) ( err error ) {
if p . Owner != nil {
return nil
}
p . Owner , err = user_model . GetUserByID ( ctx , p . OwnerID )
return err
}
func ( p * Project ) LoadRepo ( ctx context . Context ) ( err error ) {
if p . RepoID == 0 || p . Repo != nil {
return nil
}
p . Repo , err = repo_model . GetRepositoryByID ( ctx , p . RepoID )
return err
}
2023-02-07 02:09:18 +08:00
// Link returns the project's relative URL.
2023-09-29 20:12:54 +08:00
func ( p * Project ) Link ( ctx context . Context ) string {
2023-01-20 19:42:33 +08:00
if p . OwnerID > 0 {
2023-09-29 20:12:54 +08:00
err := p . LoadOwner ( ctx )
2023-01-20 19:42:33 +08:00
if err != nil {
log . Error ( "LoadOwner: %v" , err )
return ""
}
2023-01-24 05:51:18 +08:00
return fmt . Sprintf ( "%s/-/projects/%d" , p . Owner . HomeLink ( ) , p . ID )
2023-01-20 19:42:33 +08:00
}
if p . RepoID > 0 {
2023-09-29 20:12:54 +08:00
err := p . LoadRepo ( ctx )
2023-01-20 19:42:33 +08:00
if err != nil {
log . Error ( "LoadRepo: %v" , err )
return ""
}
2023-01-24 05:51:18 +08:00
return fmt . Sprintf ( "%s/projects/%d" , p . Repo . Link ( ) , p . ID )
2023-01-20 19:42:33 +08:00
}
return ""
}
2023-03-19 20:44:48 +08:00
func ( p * Project ) IconName ( ) string {
if p . IsRepositoryProject ( ) {
return "octicon-project"
}
return "octicon-project-symlink"
}
2023-01-20 19:42:33 +08:00
func ( p * Project ) IsOrganizationProject ( ) bool {
return p . Type == TypeOrganization
}
2023-03-19 20:44:48 +08:00
func ( p * Project ) IsRepositoryProject ( ) bool {
return p . Type == TypeRepository
}
2024-05-08 23:46:21 +08:00
func ( p * Project ) CanBeAccessedByOwnerRepo ( ownerID int64 , repo * repo_model . Repository ) bool {
if p . Type == TypeRepository {
return repo != nil && p . RepoID == repo . ID // if a project belongs to a repository, then its OwnerID is 0 and can be ignored
}
return p . OwnerID == ownerID && p . RepoID == 0
}
2021-09-19 19:49:59 +08:00
func init ( ) {
db . RegisterModel ( new ( Project ) )
}
2023-02-11 16:12:41 +08:00
// GetBoardConfig retrieves the types of configurations project boards could have
func GetBoardConfig ( ) [ ] BoardConfig {
return [ ] BoardConfig {
2022-03-29 22:16:31 +08:00
{ BoardTypeNone , "repo.projects.type.none" } ,
{ BoardTypeBasicKanban , "repo.projects.type.basic_kanban" } ,
{ BoardTypeBugTriage , "repo.projects.type.bug_triage" } ,
2020-08-17 11:07:38 +08:00
}
}
2023-02-11 16:12:41 +08:00
// GetCardConfig retrieves the types of configurations project board cards could have
func GetCardConfig ( ) [ ] CardConfig {
return [ ] CardConfig {
{ CardTypeTextOnly , "repo.projects.card_type.text_only" } ,
{ CardTypeImagesAndText , "repo.projects.card_type.images_and_text" } ,
}
}
2022-03-29 22:16:31 +08:00
// IsTypeValid checks if a project type is valid
func IsTypeValid ( p Type ) bool {
2020-08-17 11:07:38 +08:00
switch p {
2023-03-17 21:07:23 +08:00
case TypeIndividual , TypeRepository , TypeOrganization :
2020-08-17 11:07:38 +08:00
return true
default :
return false
}
}
2022-03-29 22:16:31 +08:00
// SearchOptions are options for GetProjects
type SearchOptions struct {
2023-11-24 11:49:41 +08:00
db . ListOptions
2023-01-20 19:42:33 +08:00
OwnerID int64
2020-08-17 11:07:38 +08:00
RepoID int64
2024-03-02 23:42:31 +08:00
IsClosed optional . Option [ bool ]
2023-07-12 02:47:50 +08:00
OrderBy db . SearchOrderBy
2022-03-29 22:16:31 +08:00
Type Type
2023-08-12 18:30:28 +08:00
Title string
2020-08-17 11:07:38 +08:00
}
2023-11-24 11:49:41 +08:00
func ( opts SearchOptions ) ToConds ( ) builder . Cond {
2023-01-20 19:42:33 +08:00
cond := builder . NewCond ( )
if opts . RepoID > 0 {
cond = cond . And ( builder . Eq { "repo_id" : opts . RepoID } )
}
2024-03-02 23:42:31 +08:00
if opts . IsClosed . Has ( ) {
cond = cond . And ( builder . Eq { "is_closed" : opts . IsClosed . Value ( ) } )
2020-08-17 11:07:38 +08:00
}
if opts . Type > 0 {
cond = cond . And ( builder . Eq { "type" : opts . Type } )
}
2023-01-20 19:42:33 +08:00
if opts . OwnerID > 0 {
cond = cond . And ( builder . Eq { "owner_id" : opts . OwnerID } )
}
2023-08-12 18:30:28 +08:00
if len ( opts . Title ) != 0 {
cond = cond . And ( db . BuildCaseInsensitiveLike ( "title" , opts . Title ) )
}
2023-01-20 19:42:33 +08:00
return cond
}
2023-11-24 11:49:41 +08:00
func ( opts SearchOptions ) ToOrders ( ) string {
return opts . OrderBy . String ( )
2023-01-20 19:42:33 +08:00
}
2023-07-12 02:47:50 +08:00
func GetSearchOrderByBySortType ( sortType string ) db . SearchOrderBy {
switch sortType {
case "oldest" :
return db . SearchOrderByOldest
case "recentupdate" :
return db . SearchOrderByRecentUpdated
case "leastupdate" :
return db . SearchOrderByLeastUpdated
default :
return db . SearchOrderByNewest
}
}
2020-08-17 11:07:38 +08:00
// NewProject creates a new Project
2023-09-29 20:12:54 +08:00
func NewProject ( ctx context . Context , p * Project ) error {
2022-03-29 22:16:31 +08:00
if ! IsBoardTypeValid ( p . BoardType ) {
p . BoardType = BoardTypeNone
2020-08-17 11:07:38 +08:00
}
2023-02-11 16:12:41 +08:00
if ! IsCardTypeValid ( p . CardType ) {
p . CardType = CardTypeTextOnly
}
2022-03-29 22:16:31 +08:00
if ! IsTypeValid ( p . Type ) {
2022-12-31 19:49:37 +08:00
return util . NewInvalidArgumentErrorf ( "project type is not valid" )
2020-08-17 11:07:38 +08:00
}
2023-09-29 20:12:54 +08:00
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 23:41:00 +08:00
if err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2021-11-21 23:41:00 +08:00
defer committer . Close ( )
2020-08-17 11:07:38 +08:00
2021-11-21 23:41:00 +08:00
if err := db . Insert ( ctx , p ) ; err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2023-01-20 19:42:33 +08:00
if p . RepoID > 0 {
if _ , err := db . Exec ( ctx , "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?" , p . RepoID ) ; err != nil {
return err
}
2020-08-17 11:07:38 +08:00
}
2022-03-29 22:16:31 +08:00
if err := createBoardsForProjectsType ( ctx , p ) ; err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2021-11-21 23:41:00 +08:00
return committer . Commit ( )
2020-08-17 11:07:38 +08:00
}
// GetProjectByID returns the projects in a repository
2022-05-20 22:08:52 +08:00
func GetProjectByID ( ctx context . Context , id int64 ) ( * Project , error ) {
2020-08-17 11:07:38 +08:00
p := new ( Project )
2022-05-20 22:08:52 +08:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( p )
2020-08-17 11:07:38 +08:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrProjectNotExist { ID : id }
}
return p , nil
}
2023-11-26 01:21:21 +08:00
// GetProjectForRepoByID returns the projects in a repository
func GetProjectForRepoByID ( ctx context . Context , repoID , id int64 ) ( * Project , error ) {
p := new ( Project )
has , err := db . GetEngine ( ctx ) . Where ( "id=? AND repo_id=?" , id , repoID ) . Get ( p )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrProjectNotExist { ID : id }
}
return p , nil
}
2020-08-17 11:07:38 +08:00
// UpdateProject updates project properties
2022-05-20 22:08:52 +08:00
func UpdateProject ( ctx context . Context , p * Project ) error {
2023-02-11 16:12:41 +08:00
if ! IsCardTypeValid ( p . CardType ) {
p . CardType = CardTypeTextOnly
}
2022-05-20 22:08:52 +08:00
_ , err := db . GetEngine ( ctx ) . ID ( p . ID ) . Cols (
2020-08-17 11:07:38 +08:00
"title" ,
"description" ,
2023-02-11 16:12:41 +08:00
"card_type" ,
2020-08-17 11:07:38 +08:00
) . Update ( p )
return err
}
2022-05-20 22:08:52 +08:00
func updateRepositoryProjectCount ( ctx context . Context , repoID int64 ) error {
if _ , err := db . GetEngine ( ctx ) . Exec ( builder . Update (
2020-08-17 11:07:38 +08:00
builder . Eq {
"`num_projects`" : builder . Select ( "count(*)" ) . From ( "`project`" ) .
Where ( builder . Eq { "`project`.`repo_id`" : repoID } .
2022-03-29 22:16:31 +08:00
And ( builder . Eq { "`project`.`type`" : TypeRepository } ) ) ,
2020-08-17 11:07:38 +08:00
} ) . From ( "`repository`" ) . Where ( builder . Eq { "id" : repoID } ) ) ; err != nil {
return err
}
2022-05-20 22:08:52 +08:00
if _ , err := db . GetEngine ( ctx ) . Exec ( builder . Update (
2020-08-17 11:07:38 +08:00
builder . Eq {
"`num_closed_projects`" : builder . Select ( "count(*)" ) . From ( "`project`" ) .
Where ( builder . Eq { "`project`.`repo_id`" : repoID } .
2022-03-29 22:16:31 +08:00
And ( builder . Eq { "`project`.`type`" : TypeRepository } ) .
2020-08-17 11:07:38 +08:00
And ( builder . Eq { "`project`.`is_closed`" : true } ) ) ,
} ) . From ( "`repository`" ) . Where ( builder . Eq { "id" : repoID } ) ) ; err != nil {
return err
}
return nil
}
// ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed
2023-09-29 20:12:54 +08:00
func ChangeProjectStatusByRepoIDAndID ( ctx context . Context , repoID , projectID int64 , isClosed bool ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 23:41:00 +08:00
if err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2021-11-21 23:41:00 +08:00
defer committer . Close ( )
2020-08-17 11:07:38 +08:00
p := new ( Project )
2022-03-29 22:16:31 +08:00
has , err := db . GetEngine ( ctx ) . ID ( projectID ) . Where ( "repo_id = ?" , repoID ) . Get ( p )
2020-08-17 11:07:38 +08:00
if err != nil {
return err
} else if ! has {
return ErrProjectNotExist { ID : projectID , RepoID : repoID }
}
2022-03-29 22:16:31 +08:00
if err := changeProjectStatus ( ctx , p , isClosed ) ; err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2021-11-21 23:41:00 +08:00
return committer . Commit ( )
2020-08-17 11:07:38 +08:00
}
// ChangeProjectStatus toggle a project between opened and closed
2023-09-29 20:12:54 +08:00
func ChangeProjectStatus ( ctx context . Context , p * Project , isClosed bool ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 23:41:00 +08:00
if err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2021-11-21 23:41:00 +08:00
defer committer . Close ( )
2020-08-17 11:07:38 +08:00
2022-03-29 22:16:31 +08:00
if err := changeProjectStatus ( ctx , p , isClosed ) ; err != nil {
2020-08-17 11:07:38 +08:00
return err
}
2021-11-21 23:41:00 +08:00
return committer . Commit ( )
2020-08-17 11:07:38 +08:00
}
2022-03-29 22:16:31 +08:00
func changeProjectStatus ( ctx context . Context , p * Project , isClosed bool ) error {
2020-08-17 11:07:38 +08:00
p . IsClosed = isClosed
p . ClosedDateUnix = timeutil . TimeStampNow ( )
2022-05-20 22:08:52 +08:00
count , err := db . GetEngine ( ctx ) . ID ( p . ID ) . Where ( "repo_id = ? AND is_closed = ?" , p . RepoID , ! isClosed ) . Cols ( "is_closed" , "closed_date_unix" ) . Update ( p )
2020-08-17 11:07:38 +08:00
if err != nil {
return err
}
if count < 1 {
return nil
}
2022-05-20 22:08:52 +08:00
return updateRepositoryProjectCount ( ctx , p . RepoID )
2020-08-17 11:07:38 +08:00
}
2022-12-10 10:46:31 +08:00
// DeleteProjectByID deletes a project from a repository. if it's not in a database
// transaction, it will start a new database transaction
func DeleteProjectByID ( ctx context . Context , id int64 ) error {
2023-01-08 09:34:58 +08:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
2022-12-10 10:46:31 +08:00
p , err := GetProjectByID ( ctx , id )
if err != nil {
if IsErrProjectNotExist ( err ) {
return nil
}
return err
2020-08-17 11:07:38 +08:00
}
2022-12-10 10:46:31 +08:00
if err := deleteProjectIssuesByProjectID ( ctx , id ) ; err != nil {
return err
}
2020-08-17 11:07:38 +08:00
2022-12-10 10:46:31 +08:00
if err := deleteBoardByProjectID ( ctx , id ) ; err != nil {
return err
}
2020-08-17 11:07:38 +08:00
2022-12-10 10:46:31 +08:00
if _ , err = db . GetEngine ( ctx ) . ID ( p . ID ) . Delete ( new ( Project ) ) ; err != nil {
return err
}
2020-08-17 11:07:38 +08:00
2022-12-10 10:46:31 +08:00
return updateRepositoryProjectCount ( ctx , p . RepoID )
} )
2020-08-17 11:07:38 +08:00
}
2022-07-14 15:22:09 +08:00
2022-12-03 10:48:26 +08:00
func DeleteProjectByRepoID ( ctx context . Context , repoID int64 ) error {
2022-07-14 15:22:09 +08:00
switch {
2023-03-07 18:51:06 +08:00
case setting . Database . Type . IsSQLite3 ( ) :
2022-07-14 15:22:09 +08:00
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_issue WHERE project_issue.id IN (SELECT project_issue.id FROM project_issue INNER JOIN project WHERE project.id = project_issue.project_id AND project.repo_id = ?)" , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)" , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Table ( "project" ) . Where ( "repo_id = ? " , repoID ) . Delete ( & Project { } ) ; err != nil {
return err
}
2023-03-07 18:51:06 +08:00
case setting . Database . Type . IsPostgreSQL ( ) :
2022-07-14 15:22:09 +08:00
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Table ( "project" ) . Where ( "repo_id = ? " , repoID ) . Delete ( & Project { } ) ; err != nil {
return err
}
default :
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Table ( "project" ) . Where ( "repo_id = ? " , repoID ) . Delete ( & Project { } ) ; err != nil {
return err
}
}
return updateRepositoryProjectCount ( ctx , repoID )
}