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 }