code.gitea.io/gitea@v1.22.3/models/project/project.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package project
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"html/template"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	repo_model "code.gitea.io/gitea/models/repo"
    13  	user_model "code.gitea.io/gitea/models/user"
    14  	"code.gitea.io/gitea/modules/log"
    15  	"code.gitea.io/gitea/modules/optional"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	"code.gitea.io/gitea/modules/timeutil"
    18  	"code.gitea.io/gitea/modules/util"
    19  
    20  	"xorm.io/builder"
    21  )
    22  
    23  type (
    24  	// BoardConfig is used to identify the type of board that is being created
    25  	BoardConfig struct {
    26  		BoardType   BoardType
    27  		Translation string
    28  	}
    29  
    30  	// CardConfig is used to identify the type of board card that is being used
    31  	CardConfig struct {
    32  		CardType    CardType
    33  		Translation string
    34  	}
    35  
    36  	// Type is used to identify the type of project in question and ownership
    37  	Type uint8
    38  )
    39  
    40  const (
    41  	// TypeIndividual is a type of project board that is owned by an individual
    42  	TypeIndividual Type = iota + 1
    43  
    44  	// TypeRepository is a project that is tied to a repository
    45  	TypeRepository
    46  
    47  	// TypeOrganization is a project that is tied to an organisation
    48  	TypeOrganization
    49  )
    50  
    51  // ErrProjectNotExist represents a "ProjectNotExist" kind of error.
    52  type ErrProjectNotExist struct {
    53  	ID     int64
    54  	RepoID int64
    55  }
    56  
    57  // IsErrProjectNotExist checks if an error is a ErrProjectNotExist
    58  func IsErrProjectNotExist(err error) bool {
    59  	_, ok := err.(ErrProjectNotExist)
    60  	return ok
    61  }
    62  
    63  func (err ErrProjectNotExist) Error() string {
    64  	return fmt.Sprintf("projects does not exist [id: %d]", err.ID)
    65  }
    66  
    67  func (err ErrProjectNotExist) Unwrap() error {
    68  	return util.ErrNotExist
    69  }
    70  
    71  // ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
    72  type ErrProjectBoardNotExist struct {
    73  	BoardID int64
    74  }
    75  
    76  // IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
    77  func IsErrProjectBoardNotExist(err error) bool {
    78  	_, ok := err.(ErrProjectBoardNotExist)
    79  	return ok
    80  }
    81  
    82  func (err ErrProjectBoardNotExist) Error() string {
    83  	return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
    84  }
    85  
    86  func (err ErrProjectBoardNotExist) Unwrap() error {
    87  	return util.ErrNotExist
    88  }
    89  
    90  // Project represents a project board
    91  type Project struct {
    92  	ID          int64                  `xorm:"pk autoincr"`
    93  	Title       string                 `xorm:"INDEX NOT NULL"`
    94  	Description string                 `xorm:"TEXT"`
    95  	OwnerID     int64                  `xorm:"INDEX"`
    96  	Owner       *user_model.User       `xorm:"-"`
    97  	RepoID      int64                  `xorm:"INDEX"`
    98  	Repo        *repo_model.Repository `xorm:"-"`
    99  	CreatorID   int64                  `xorm:"NOT NULL"`
   100  	IsClosed    bool                   `xorm:"INDEX"`
   101  	BoardType   BoardType
   102  	CardType    CardType
   103  	Type        Type
   104  
   105  	RenderedContent template.HTML `xorm:"-"`
   106  
   107  	CreatedUnix    timeutil.TimeStamp `xorm:"INDEX created"`
   108  	UpdatedUnix    timeutil.TimeStamp `xorm:"INDEX updated"`
   109  	ClosedDateUnix timeutil.TimeStamp
   110  }
   111  
   112  // Ghost Project is a project which has been deleted
   113  const GhostProjectID = -1
   114  
   115  func (p *Project) IsGhost() bool {
   116  	return p.ID == GhostProjectID
   117  }
   118  
   119  func (p *Project) LoadOwner(ctx context.Context) (err error) {
   120  	if p.Owner != nil {
   121  		return nil
   122  	}
   123  	p.Owner, err = user_model.GetUserByID(ctx, p.OwnerID)
   124  	return err
   125  }
   126  
   127  func (p *Project) LoadRepo(ctx context.Context) (err error) {
   128  	if p.RepoID == 0 || p.Repo != nil {
   129  		return nil
   130  	}
   131  	p.Repo, err = repo_model.GetRepositoryByID(ctx, p.RepoID)
   132  	return err
   133  }
   134  
   135  // Link returns the project's relative URL.
   136  func (p *Project) Link(ctx context.Context) string {
   137  	if p.OwnerID > 0 {
   138  		err := p.LoadOwner(ctx)
   139  		if err != nil {
   140  			log.Error("LoadOwner: %v", err)
   141  			return ""
   142  		}
   143  		return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID)
   144  	}
   145  	if p.RepoID > 0 {
   146  		err := p.LoadRepo(ctx)
   147  		if err != nil {
   148  			log.Error("LoadRepo: %v", err)
   149  			return ""
   150  		}
   151  		return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID)
   152  	}
   153  	return ""
   154  }
   155  
   156  func (p *Project) IconName() string {
   157  	if p.IsRepositoryProject() {
   158  		return "octicon-project"
   159  	}
   160  	return "octicon-project-symlink"
   161  }
   162  
   163  func (p *Project) IsOrganizationProject() bool {
   164  	return p.Type == TypeOrganization
   165  }
   166  
   167  func (p *Project) IsRepositoryProject() bool {
   168  	return p.Type == TypeRepository
   169  }
   170  
   171  func (p *Project) CanBeAccessedByOwnerRepo(ownerID int64, repo *repo_model.Repository) bool {
   172  	if p.Type == TypeRepository {
   173  		return repo != nil && p.RepoID == repo.ID // if a project belongs to a repository, then its OwnerID is 0 and can be ignored
   174  	}
   175  	return p.OwnerID == ownerID && p.RepoID == 0
   176  }
   177  
   178  func init() {
   179  	db.RegisterModel(new(Project))
   180  }
   181  
   182  // GetBoardConfig retrieves the types of configurations project boards could have
   183  func GetBoardConfig() []BoardConfig {
   184  	return []BoardConfig{
   185  		{BoardTypeNone, "repo.projects.type.none"},
   186  		{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
   187  		{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
   188  	}
   189  }
   190  
   191  // GetCardConfig retrieves the types of configurations project board cards could have
   192  func GetCardConfig() []CardConfig {
   193  	return []CardConfig{
   194  		{CardTypeTextOnly, "repo.projects.card_type.text_only"},
   195  		{CardTypeImagesAndText, "repo.projects.card_type.images_and_text"},
   196  	}
   197  }
   198  
   199  // IsTypeValid checks if a project type is valid
   200  func IsTypeValid(p Type) bool {
   201  	switch p {
   202  	case TypeIndividual, TypeRepository, TypeOrganization:
   203  		return true
   204  	default:
   205  		return false
   206  	}
   207  }
   208  
   209  // SearchOptions are options for GetProjects
   210  type SearchOptions struct {
   211  	db.ListOptions
   212  	OwnerID  int64
   213  	RepoID   int64
   214  	IsClosed optional.Option[bool]
   215  	OrderBy  db.SearchOrderBy
   216  	Type     Type
   217  	Title    string
   218  }
   219  
   220  func (opts SearchOptions) ToConds() builder.Cond {
   221  	cond := builder.NewCond()
   222  	if opts.RepoID > 0 {
   223  		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
   224  	}
   225  	if opts.IsClosed.Has() {
   226  		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
   227  	}
   228  
   229  	if opts.Type > 0 {
   230  		cond = cond.And(builder.Eq{"type": opts.Type})
   231  	}
   232  	if opts.OwnerID > 0 {
   233  		cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
   234  	}
   235  
   236  	if len(opts.Title) != 0 {
   237  		cond = cond.And(db.BuildCaseInsensitiveLike("title", opts.Title))
   238  	}
   239  	return cond
   240  }
   241  
   242  func (opts SearchOptions) ToOrders() string {
   243  	return opts.OrderBy.String()
   244  }
   245  
   246  func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
   247  	switch sortType {
   248  	case "oldest":
   249  		return db.SearchOrderByOldest
   250  	case "recentupdate":
   251  		return db.SearchOrderByRecentUpdated
   252  	case "leastupdate":
   253  		return db.SearchOrderByLeastUpdated
   254  	default:
   255  		return db.SearchOrderByNewest
   256  	}
   257  }
   258  
   259  // NewProject creates a new Project
   260  func NewProject(ctx context.Context, p *Project) error {
   261  	if !IsBoardTypeValid(p.BoardType) {
   262  		p.BoardType = BoardTypeNone
   263  	}
   264  
   265  	if !IsCardTypeValid(p.CardType) {
   266  		p.CardType = CardTypeTextOnly
   267  	}
   268  
   269  	if !IsTypeValid(p.Type) {
   270  		return util.NewInvalidArgumentErrorf("project type is not valid")
   271  	}
   272  
   273  	ctx, committer, err := db.TxContext(ctx)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	defer committer.Close()
   278  
   279  	if err := db.Insert(ctx, p); err != nil {
   280  		return err
   281  	}
   282  
   283  	if p.RepoID > 0 {
   284  		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
   285  			return err
   286  		}
   287  	}
   288  
   289  	if err := createBoardsForProjectsType(ctx, p); err != nil {
   290  		return err
   291  	}
   292  
   293  	return committer.Commit()
   294  }
   295  
   296  // GetProjectByID returns the projects in a repository
   297  func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
   298  	p := new(Project)
   299  
   300  	has, err := db.GetEngine(ctx).ID(id).Get(p)
   301  	if err != nil {
   302  		return nil, err
   303  	} else if !has {
   304  		return nil, ErrProjectNotExist{ID: id}
   305  	}
   306  
   307  	return p, nil
   308  }
   309  
   310  // GetProjectForRepoByID returns the projects in a repository
   311  func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
   312  	p := new(Project)
   313  	has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(p)
   314  	if err != nil {
   315  		return nil, err
   316  	} else if !has {
   317  		return nil, ErrProjectNotExist{ID: id}
   318  	}
   319  	return p, nil
   320  }
   321  
   322  // GetAllProjectsIDsByOwnerID returns the all projects ids it owns
   323  func GetAllProjectsIDsByOwnerIDAndType(ctx context.Context, ownerID int64, projectType Type) ([]int64, error) {
   324  	projects := make([]int64, 0)
   325  	return projects, db.GetEngine(ctx).Table(&Project{}).Where("owner_id=? AND type=?", ownerID, projectType).Cols("id").Find(&projects)
   326  }
   327  
   328  // UpdateProject updates project properties
   329  func UpdateProject(ctx context.Context, p *Project) error {
   330  	if !IsCardTypeValid(p.CardType) {
   331  		p.CardType = CardTypeTextOnly
   332  	}
   333  
   334  	_, err := db.GetEngine(ctx).ID(p.ID).Cols(
   335  		"title",
   336  		"description",
   337  		"card_type",
   338  	).Update(p)
   339  	return err
   340  }
   341  
   342  func updateRepositoryProjectCount(ctx context.Context, repoID int64) error {
   343  	if _, err := db.GetEngine(ctx).Exec(builder.Update(
   344  		builder.Eq{
   345  			"`num_projects`": builder.Select("count(*)").From("`project`").
   346  				Where(builder.Eq{"`project`.`repo_id`": repoID}.
   347  					And(builder.Eq{"`project`.`type`": TypeRepository})),
   348  		}).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil {
   349  		return err
   350  	}
   351  
   352  	if _, err := db.GetEngine(ctx).Exec(builder.Update(
   353  		builder.Eq{
   354  			"`num_closed_projects`": builder.Select("count(*)").From("`project`").
   355  				Where(builder.Eq{"`project`.`repo_id`": repoID}.
   356  					And(builder.Eq{"`project`.`type`": TypeRepository}).
   357  					And(builder.Eq{"`project`.`is_closed`": true})),
   358  		}).From("`repository`").Where(builder.Eq{"id": repoID})); err != nil {
   359  		return err
   360  	}
   361  	return nil
   362  }
   363  
   364  // ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed
   365  func ChangeProjectStatusByRepoIDAndID(ctx context.Context, repoID, projectID int64, isClosed bool) error {
   366  	ctx, committer, err := db.TxContext(ctx)
   367  	if err != nil {
   368  		return err
   369  	}
   370  	defer committer.Close()
   371  
   372  	p := new(Project)
   373  
   374  	has, err := db.GetEngine(ctx).ID(projectID).Where("repo_id = ?", repoID).Get(p)
   375  	if err != nil {
   376  		return err
   377  	} else if !has {
   378  		return ErrProjectNotExist{ID: projectID, RepoID: repoID}
   379  	}
   380  
   381  	if err := changeProjectStatus(ctx, p, isClosed); err != nil {
   382  		return err
   383  	}
   384  
   385  	return committer.Commit()
   386  }
   387  
   388  // ChangeProjectStatus toggle a project between opened and closed
   389  func ChangeProjectStatus(ctx context.Context, p *Project, isClosed bool) error {
   390  	ctx, committer, err := db.TxContext(ctx)
   391  	if err != nil {
   392  		return err
   393  	}
   394  	defer committer.Close()
   395  
   396  	if err := changeProjectStatus(ctx, p, isClosed); err != nil {
   397  		return err
   398  	}
   399  
   400  	return committer.Commit()
   401  }
   402  
   403  func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error {
   404  	p.IsClosed = isClosed
   405  	p.ClosedDateUnix = timeutil.TimeStampNow()
   406  	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)
   407  	if err != nil {
   408  		return err
   409  	}
   410  	if count < 1 {
   411  		return nil
   412  	}
   413  
   414  	return updateRepositoryProjectCount(ctx, p.RepoID)
   415  }
   416  
   417  // DeleteProjectByID deletes a project from a repository. if it's not in a database
   418  // transaction, it will start a new database transaction
   419  func DeleteProjectByID(ctx context.Context, id int64) error {
   420  	return db.WithTx(ctx, func(ctx context.Context) error {
   421  		p, err := GetProjectByID(ctx, id)
   422  		if err != nil {
   423  			if IsErrProjectNotExist(err) {
   424  				return nil
   425  			}
   426  			return err
   427  		}
   428  
   429  		if err := deleteProjectIssuesByProjectID(ctx, id); err != nil {
   430  			return err
   431  		}
   432  
   433  		if err := deleteBoardByProjectID(ctx, id); err != nil {
   434  			return err
   435  		}
   436  
   437  		if _, err = db.GetEngine(ctx).ID(p.ID).Delete(new(Project)); err != nil {
   438  			return err
   439  		}
   440  
   441  		return updateRepositoryProjectCount(ctx, p.RepoID)
   442  	})
   443  }
   444  
   445  func DeleteProjectByRepoID(ctx context.Context, repoID int64) error {
   446  	switch {
   447  	case setting.Database.Type.IsSQLite3():
   448  		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 {
   449  			return err
   450  		}
   451  		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 {
   452  			return err
   453  		}
   454  		if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
   455  			return err
   456  		}
   457  	case setting.Database.Type.IsPostgreSQL():
   458  		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 {
   459  			return err
   460  		}
   461  		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 {
   462  			return err
   463  		}
   464  		if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
   465  			return err
   466  		}
   467  	default:
   468  		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 {
   469  			return err
   470  		}
   471  		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 {
   472  			return err
   473  		}
   474  		if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil {
   475  			return err
   476  		}
   477  	}
   478  
   479  	return updateRepositoryProjectCount(ctx, repoID)
   480  }