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 }