code.gitea.io/gitea@v1.21.7/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 "fmt" 9 "regexp" 10 11 "code.gitea.io/gitea/models/db" 12 "code.gitea.io/gitea/modules/setting" 13 "code.gitea.io/gitea/modules/timeutil" 14 15 "xorm.io/builder" 16 ) 17 18 type ( 19 // BoardType is used to represent a project board type 20 BoardType uint8 21 22 // CardType is used to represent a project board card type 23 CardType uint8 24 25 // BoardList is a list of all project boards in a repository 26 BoardList []*Board 27 ) 28 29 const ( 30 // BoardTypeNone is a project board type that has no predefined columns 31 BoardTypeNone BoardType = iota 32 33 // BoardTypeBasicKanban is a project board type that has basic predefined columns 34 BoardTypeBasicKanban 35 36 // BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs 37 BoardTypeBugTriage 38 ) 39 40 const ( 41 // CardTypeTextOnly is a project board card type that is text only 42 CardTypeTextOnly CardType = iota 43 44 // CardTypeImagesAndText is a project board card type that has images and text 45 CardTypeImagesAndText 46 ) 47 48 // BoardColorPattern is a regexp witch can validate BoardColor 49 var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") 50 51 // Board is used to represent boards on a project 52 type Board struct { 53 ID int64 `xorm:"pk autoincr"` 54 Title string 55 Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board 56 Sorting int8 `xorm:"NOT NULL DEFAULT 0"` 57 Color string `xorm:"VARCHAR(7)"` 58 59 ProjectID int64 `xorm:"INDEX NOT NULL"` 60 CreatorID int64 `xorm:"NOT NULL"` 61 62 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 63 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 64 } 65 66 // TableName return the real table name 67 func (Board) TableName() string { 68 return "project_board" 69 } 70 71 // NumIssues return counter of all issues assigned to the board 72 func (b *Board) NumIssues(ctx context.Context) int { 73 c, err := db.GetEngine(ctx).Table("project_issue"). 74 Where("project_id=?", b.ProjectID). 75 And("project_board_id=?", b.ID). 76 GroupBy("issue_id"). 77 Cols("issue_id"). 78 Count() 79 if err != nil { 80 return 0 81 } 82 return int(c) 83 } 84 85 func init() { 86 db.RegisterModel(new(Board)) 87 } 88 89 // IsBoardTypeValid checks if the project board type is valid 90 func IsBoardTypeValid(p BoardType) bool { 91 switch p { 92 case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage: 93 return true 94 default: 95 return false 96 } 97 } 98 99 // IsCardTypeValid checks if the project board card type is valid 100 func IsCardTypeValid(p CardType) bool { 101 switch p { 102 case CardTypeTextOnly, CardTypeImagesAndText: 103 return true 104 default: 105 return false 106 } 107 } 108 109 func createBoardsForProjectsType(ctx context.Context, project *Project) error { 110 var items []string 111 112 switch project.BoardType { 113 114 case BoardTypeBugTriage: 115 items = setting.Project.ProjectBoardBugTriageType 116 117 case BoardTypeBasicKanban: 118 items = setting.Project.ProjectBoardBasicKanbanType 119 120 case BoardTypeNone: 121 fallthrough 122 default: 123 return nil 124 } 125 126 if len(items) == 0 { 127 return nil 128 } 129 130 boards := make([]Board, 0, len(items)) 131 132 for _, v := range items { 133 boards = append(boards, Board{ 134 CreatedUnix: timeutil.TimeStampNow(), 135 CreatorID: project.CreatorID, 136 Title: v, 137 ProjectID: project.ID, 138 }) 139 } 140 141 return db.Insert(ctx, boards) 142 } 143 144 // NewBoard adds a new project board to a given project 145 func NewBoard(ctx context.Context, board *Board) error { 146 if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { 147 return fmt.Errorf("bad color code: %s", board.Color) 148 } 149 150 _, err := db.GetEngine(ctx).Insert(board) 151 return err 152 } 153 154 // DeleteBoardByID removes all issues references to the project board. 155 func DeleteBoardByID(ctx context.Context, boardID int64) error { 156 ctx, committer, err := db.TxContext(ctx) 157 if err != nil { 158 return err 159 } 160 defer committer.Close() 161 162 if err := deleteBoardByID(ctx, boardID); err != nil { 163 return err 164 } 165 166 return committer.Commit() 167 } 168 169 func deleteBoardByID(ctx context.Context, boardID int64) error { 170 board, err := GetBoard(ctx, boardID) 171 if err != nil { 172 if IsErrProjectBoardNotExist(err) { 173 return nil 174 } 175 176 return err 177 } 178 179 if err = board.removeIssues(ctx); err != nil { 180 return err 181 } 182 183 if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil { 184 return err 185 } 186 return nil 187 } 188 189 func deleteBoardByProjectID(ctx context.Context, projectID int64) error { 190 _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{}) 191 return err 192 } 193 194 // GetBoard fetches the current board of a project 195 func GetBoard(ctx context.Context, boardID int64) (*Board, error) { 196 board := new(Board) 197 198 has, err := db.GetEngine(ctx).ID(boardID).Get(board) 199 if err != nil { 200 return nil, err 201 } else if !has { 202 return nil, ErrProjectBoardNotExist{BoardID: boardID} 203 } 204 205 return board, nil 206 } 207 208 // UpdateBoard updates a project board 209 func UpdateBoard(ctx context.Context, board *Board) error { 210 var fieldToUpdate []string 211 212 if board.Sorting != 0 { 213 fieldToUpdate = append(fieldToUpdate, "sorting") 214 } 215 216 if board.Title != "" { 217 fieldToUpdate = append(fieldToUpdate, "title") 218 } 219 220 if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { 221 return fmt.Errorf("bad color code: %s", board.Color) 222 } 223 fieldToUpdate = append(fieldToUpdate, "color") 224 225 _, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board) 226 227 return err 228 } 229 230 // GetBoards fetches all boards related to a project 231 // if no default board set, first board is a temporary "Uncategorized" board 232 func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { 233 boards := make([]*Board, 0, 5) 234 235 if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("Sorting").Find(&boards); err != nil { 236 return nil, err 237 } 238 239 defaultB, err := p.getDefaultBoard(ctx) 240 if err != nil { 241 return nil, err 242 } 243 244 return append([]*Board{defaultB}, boards...), nil 245 } 246 247 // getDefaultBoard return default board and create a dummy if none exist 248 func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { 249 var board Board 250 exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board) 251 if err != nil { 252 return nil, err 253 } 254 if exist { 255 return &board, nil 256 } 257 258 // represents a board for issues not assigned to one 259 return &Board{ 260 ProjectID: p.ID, 261 Title: "Uncategorized", 262 Default: true, 263 }, nil 264 } 265 266 // SetDefaultBoard represents a board for issues not assigned to one 267 // if boardID is 0 unset default 268 func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error { 269 _, err := db.GetEngine(ctx).Where(builder.Eq{ 270 "project_id": projectID, 271 "`default`": true, 272 }).Cols("`default`").Update(&Board{Default: false}) 273 if err != nil { 274 return err 275 } 276 277 if boardID > 0 { 278 _, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). 279 Cols("`default`").Update(&Board{Default: true}) 280 } 281 282 return err 283 } 284 285 // UpdateBoardSorting update project board sorting 286 func UpdateBoardSorting(ctx context.Context, bs BoardList) error { 287 for i := range bs { 288 _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( 289 "sorting", 290 ).Update(bs[i]) 291 if err != nil { 292 return err 293 } 294 } 295 return nil 296 }