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