code.gitea.io/gitea@v1.22.3/models/project/board.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 "errors" 9 "fmt" 10 "regexp" 11 12 "code.gitea.io/gitea/models/db" 13 "code.gitea.io/gitea/modules/setting" 14 "code.gitea.io/gitea/modules/timeutil" 15 "code.gitea.io/gitea/modules/util" 16 17 "xorm.io/builder" 18 ) 19 20 type ( 21 // BoardType is used to represent a project board type 22 BoardType uint8 23 24 // CardType is used to represent a project board card type 25 CardType uint8 26 27 // BoardList is a list of all project boards in a repository 28 BoardList []*Board 29 ) 30 31 const ( 32 // BoardTypeNone is a project board type that has no predefined columns 33 BoardTypeNone BoardType = iota 34 35 // BoardTypeBasicKanban is a project board type that has basic predefined columns 36 BoardTypeBasicKanban 37 38 // BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs 39 BoardTypeBugTriage 40 ) 41 42 const ( 43 // CardTypeTextOnly is a project board card type that is text only 44 CardTypeTextOnly CardType = iota 45 46 // CardTypeImagesAndText is a project board card type that has images and text 47 CardTypeImagesAndText 48 ) 49 50 // BoardColorPattern is a regexp witch can validate BoardColor 51 var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") 52 53 // Board is used to represent boards on a project 54 type Board struct { 55 ID int64 `xorm:"pk autoincr"` 56 Title string 57 Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board 58 Sorting int8 `xorm:"NOT NULL DEFAULT 0"` 59 Color string `xorm:"VARCHAR(7)"` 60 61 ProjectID int64 `xorm:"INDEX NOT NULL"` 62 CreatorID int64 `xorm:"NOT NULL"` 63 64 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 65 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 66 } 67 68 // TableName return the real table name 69 func (Board) TableName() string { 70 return "project_board" 71 } 72 73 // NumIssues return counter of all issues assigned to the board 74 func (b *Board) NumIssues(ctx context.Context) int { 75 c, err := db.GetEngine(ctx).Table("project_issue"). 76 Where("project_id=?", b.ProjectID). 77 And("project_board_id=?", b.ID). 78 GroupBy("issue_id"). 79 Cols("issue_id"). 80 Count() 81 if err != nil { 82 return 0 83 } 84 return int(c) 85 } 86 87 func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { 88 issues := make([]*ProjectIssue, 0, 5) 89 if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID). 90 And("project_board_id=?", b.ID). 91 OrderBy("sorting, id"). 92 Find(&issues); err != nil { 93 return nil, err 94 } 95 return issues, nil 96 } 97 98 func init() { 99 db.RegisterModel(new(Board)) 100 } 101 102 // IsBoardTypeValid checks if the project board type is valid 103 func IsBoardTypeValid(p BoardType) bool { 104 switch p { 105 case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage: 106 return true 107 default: 108 return false 109 } 110 } 111 112 // IsCardTypeValid checks if the project board card type is valid 113 func IsCardTypeValid(p CardType) bool { 114 switch p { 115 case CardTypeTextOnly, CardTypeImagesAndText: 116 return true 117 default: 118 return false 119 } 120 } 121 122 func createBoardsForProjectsType(ctx context.Context, project *Project) error { 123 var items []string 124 125 switch project.BoardType { 126 case BoardTypeBugTriage: 127 items = setting.Project.ProjectBoardBugTriageType 128 129 case BoardTypeBasicKanban: 130 items = setting.Project.ProjectBoardBasicKanbanType 131 case BoardTypeNone: 132 fallthrough 133 default: 134 return nil 135 } 136 137 board := Board{ 138 CreatedUnix: timeutil.TimeStampNow(), 139 CreatorID: project.CreatorID, 140 Title: "Backlog", 141 ProjectID: project.ID, 142 Default: true, 143 } 144 if err := db.Insert(ctx, board); err != nil { 145 return err 146 } 147 148 if len(items) == 0 { 149 return nil 150 } 151 152 boards := make([]Board, 0, len(items)) 153 154 for _, v := range items { 155 boards = append(boards, Board{ 156 CreatedUnix: timeutil.TimeStampNow(), 157 CreatorID: project.CreatorID, 158 Title: v, 159 ProjectID: project.ID, 160 }) 161 } 162 163 return db.Insert(ctx, boards) 164 } 165 166 // maxProjectColumns max columns allowed in a project, this should not bigger than 127 167 // because sorting is int8 in database 168 const maxProjectColumns = 20 169 170 // NewBoard adds a new project board to a given project 171 func NewBoard(ctx context.Context, board *Board) error { 172 if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { 173 return fmt.Errorf("bad color code: %s", board.Color) 174 } 175 res := struct { 176 MaxSorting int64 177 ColumnCount int64 178 }{} 179 if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board"). 180 Where("project_id=?", board.ProjectID).Get(&res); err != nil { 181 return err 182 } 183 if res.ColumnCount >= maxProjectColumns { 184 return fmt.Errorf("NewBoard: maximum number of columns reached") 185 } 186 board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) 187 _, err := db.GetEngine(ctx).Insert(board) 188 return err 189 } 190 191 // DeleteBoardByID removes all issues references to the project board. 192 func DeleteBoardByID(ctx context.Context, boardID int64) error { 193 ctx, committer, err := db.TxContext(ctx) 194 if err != nil { 195 return err 196 } 197 defer committer.Close() 198 199 if err := deleteBoardByID(ctx, boardID); err != nil { 200 return err 201 } 202 203 return committer.Commit() 204 } 205 206 func deleteBoardByID(ctx context.Context, boardID int64) error { 207 board, err := GetBoard(ctx, boardID) 208 if err != nil { 209 if IsErrProjectBoardNotExist(err) { 210 return nil 211 } 212 213 return err 214 } 215 216 if board.Default { 217 return fmt.Errorf("deleteBoardByID: cannot delete default board") 218 } 219 220 // move all issues to the default column 221 project, err := GetProjectByID(ctx, board.ProjectID) 222 if err != nil { 223 return err 224 } 225 defaultColumn, err := project.GetDefaultBoard(ctx) 226 if err != nil { 227 return err 228 } 229 230 if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { 231 return err 232 } 233 234 if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil { 235 return err 236 } 237 return nil 238 } 239 240 func deleteBoardByProjectID(ctx context.Context, projectID int64) error { 241 _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{}) 242 return err 243 } 244 245 // GetBoard fetches the current board of a project 246 func GetBoard(ctx context.Context, boardID int64) (*Board, error) { 247 board := new(Board) 248 has, err := db.GetEngine(ctx).ID(boardID).Get(board) 249 if err != nil { 250 return nil, err 251 } else if !has { 252 return nil, ErrProjectBoardNotExist{BoardID: boardID} 253 } 254 255 return board, nil 256 } 257 258 // UpdateBoard updates a project board 259 func UpdateBoard(ctx context.Context, board *Board) error { 260 var fieldToUpdate []string 261 262 if board.Sorting != 0 { 263 fieldToUpdate = append(fieldToUpdate, "sorting") 264 } 265 266 if board.Title != "" { 267 fieldToUpdate = append(fieldToUpdate, "title") 268 } 269 270 if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { 271 return fmt.Errorf("bad color code: %s", board.Color) 272 } 273 fieldToUpdate = append(fieldToUpdate, "color") 274 275 _, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board) 276 277 return err 278 } 279 280 // GetBoards fetches all boards related to a project 281 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { 282 boards := make([]*Board, 0, 5) 283 if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil { 284 return nil, err 285 } 286 287 return boards, nil 288 } 289 290 // GetDefaultBoard return default board and ensure only one exists 291 func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) { 292 var board Board 293 has, err := db.GetEngine(ctx). 294 Where("project_id=? AND `default` = ?", p.ID, true). 295 Desc("id").Get(&board) 296 if err != nil { 297 return nil, err 298 } 299 300 if has { 301 return &board, nil 302 } 303 304 // create a default board if none is found 305 board = Board{ 306 ProjectID: p.ID, 307 Default: true, 308 Title: "Uncategorized", 309 CreatorID: p.CreatorID, 310 } 311 if _, err := db.GetEngine(ctx).Insert(&board); err != nil { 312 return nil, err 313 } 314 return &board, nil 315 } 316 317 // SetDefaultBoard represents a board for issues not assigned to one 318 func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error { 319 return db.WithTx(ctx, func(ctx context.Context) error { 320 if _, err := GetBoard(ctx, boardID); err != nil { 321 return err 322 } 323 324 if _, err := db.GetEngine(ctx).Where(builder.Eq{ 325 "project_id": projectID, 326 "`default`": true, 327 }).Cols("`default`").Update(&Board{Default: false}); err != nil { 328 return err 329 } 330 331 _, err := db.GetEngine(ctx).ID(boardID). 332 Where(builder.Eq{"project_id": projectID}). 333 Cols("`default`").Update(&Board{Default: true}) 334 return err 335 }) 336 } 337 338 // UpdateBoardSorting update project board sorting 339 func UpdateBoardSorting(ctx context.Context, bs BoardList) error { 340 return db.WithTx(ctx, func(ctx context.Context) error { 341 for i := range bs { 342 if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( 343 "sorting", 344 ).Update(bs[i]); err != nil { 345 return err 346 } 347 } 348 return nil 349 }) 350 } 351 352 func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) { 353 columns := make([]*Board, 0, 5) 354 if err := db.GetEngine(ctx). 355 Where("project_id =?", projectID). 356 In("id", columnsIDs). 357 OrderBy("sorting").Find(&columns); err != nil { 358 return nil, err 359 } 360 return columns, nil 361 } 362 363 // MoveColumnsOnProject sorts columns in a project 364 func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error { 365 return db.WithTx(ctx, func(ctx context.Context) error { 366 sess := db.GetEngine(ctx) 367 columnIDs := util.ValuesOfMap(sortedColumnIDs) 368 movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs) 369 if err != nil { 370 return err 371 } 372 if len(movedColumns) != len(sortedColumnIDs) { 373 return errors.New("some columns do not exist") 374 } 375 376 for _, column := range movedColumns { 377 if column.ProjectID != project.ID { 378 return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID) 379 } 380 } 381 382 for sorting, columnID := range sortedColumnIDs { 383 if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil { 384 return err 385 } 386 } 387 return nil 388 }) 389 }