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