gitea源码

column.go 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. // Copyright 2020 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package project
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "regexp"
  9. "code.gitea.io/gitea/models/db"
  10. "code.gitea.io/gitea/modules/setting"
  11. "code.gitea.io/gitea/modules/timeutil"
  12. "code.gitea.io/gitea/modules/util"
  13. "xorm.io/builder"
  14. )
  15. type (
  16. // CardType is used to represent a project column card type
  17. CardType uint8
  18. // ColumnList is a list of all project columns in a repository
  19. ColumnList []*Column
  20. )
  21. const (
  22. // CardTypeTextOnly is a project column card type that is text only
  23. CardTypeTextOnly CardType = iota
  24. // CardTypeImagesAndText is a project column card type that has images and text
  25. CardTypeImagesAndText
  26. )
  27. // ColumnColorPattern is a regexp witch can validate ColumnColor
  28. var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
  29. // Column is used to represent column on a project
  30. type Column struct {
  31. ID int64 `xorm:"pk autoincr"`
  32. Title string
  33. Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column
  34. Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
  35. Color string `xorm:"VARCHAR(7)"`
  36. ProjectID int64 `xorm:"INDEX NOT NULL"`
  37. CreatorID int64 `xorm:"NOT NULL"`
  38. NumIssues int64 `xorm:"-"`
  39. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  40. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  41. }
  42. // TableName return the real table name
  43. func (Column) TableName() string {
  44. return "project_board" // TODO: the legacy table name should be project_column
  45. }
  46. func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
  47. issues := make([]*ProjectIssue, 0, 5)
  48. if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
  49. And("project_board_id=?", c.ID).
  50. OrderBy("sorting, id").
  51. Find(&issues); err != nil {
  52. return nil, err
  53. }
  54. return issues, nil
  55. }
  56. func init() {
  57. db.RegisterModel(new(Column))
  58. }
  59. // IsCardTypeValid checks if the project column card type is valid
  60. func IsCardTypeValid(p CardType) bool {
  61. switch p {
  62. case CardTypeTextOnly, CardTypeImagesAndText:
  63. return true
  64. default:
  65. return false
  66. }
  67. }
  68. func createDefaultColumnsForProject(ctx context.Context, project *Project) error {
  69. var items []string
  70. switch project.TemplateType {
  71. case TemplateTypeBugTriage:
  72. items = setting.Project.ProjectBoardBugTriageType
  73. case TemplateTypeBasicKanban:
  74. items = setting.Project.ProjectBoardBasicKanbanType
  75. case TemplateTypeNone:
  76. fallthrough
  77. default:
  78. return nil
  79. }
  80. return db.WithTx(ctx, func(ctx context.Context) error {
  81. column := Column{
  82. CreatedUnix: timeutil.TimeStampNow(),
  83. CreatorID: project.CreatorID,
  84. Title: "Backlog",
  85. ProjectID: project.ID,
  86. Default: true,
  87. }
  88. if err := db.Insert(ctx, column); err != nil {
  89. return err
  90. }
  91. if len(items) == 0 {
  92. return nil
  93. }
  94. columns := make([]Column, 0, len(items))
  95. for _, v := range items {
  96. columns = append(columns, Column{
  97. CreatedUnix: timeutil.TimeStampNow(),
  98. CreatorID: project.CreatorID,
  99. Title: v,
  100. ProjectID: project.ID,
  101. })
  102. }
  103. return db.Insert(ctx, columns)
  104. })
  105. }
  106. // maxProjectColumns max columns allowed in a project, this should not bigger than 127
  107. // because sorting is int8 in database
  108. const maxProjectColumns = 20
  109. // NewColumn adds a new project column to a given project
  110. func NewColumn(ctx context.Context, column *Column) error {
  111. if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
  112. return fmt.Errorf("bad color code: %s", column.Color)
  113. }
  114. res := struct {
  115. MaxSorting int64
  116. ColumnCount int64
  117. }{}
  118. if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
  119. Where("project_id=?", column.ProjectID).Get(&res); err != nil {
  120. return err
  121. }
  122. if res.ColumnCount >= maxProjectColumns {
  123. return errors.New("NewBoard: maximum number of columns reached")
  124. }
  125. column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
  126. _, err := db.GetEngine(ctx).Insert(column)
  127. return err
  128. }
  129. // DeleteColumnByID removes all issues references to the project column.
  130. func DeleteColumnByID(ctx context.Context, columnID int64) error {
  131. return db.WithTx(ctx, func(ctx context.Context) error {
  132. return deleteColumnByID(ctx, columnID)
  133. })
  134. }
  135. func deleteColumnByID(ctx context.Context, columnID int64) error {
  136. column, err := GetColumn(ctx, columnID)
  137. if err != nil {
  138. if IsErrProjectColumnNotExist(err) {
  139. return nil
  140. }
  141. return err
  142. }
  143. if column.Default {
  144. return errors.New("deleteColumnByID: cannot delete default column")
  145. }
  146. // move all issues to the default column
  147. project, err := GetProjectByID(ctx, column.ProjectID)
  148. if err != nil {
  149. return err
  150. }
  151. defaultColumn, err := project.MustDefaultColumn(ctx)
  152. if err != nil {
  153. return err
  154. }
  155. if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
  156. return err
  157. }
  158. if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil {
  159. return err
  160. }
  161. return nil
  162. }
  163. func deleteColumnByProjectID(ctx context.Context, projectID int64) error {
  164. _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Column{})
  165. return err
  166. }
  167. // GetColumn fetches the current column of a project
  168. func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
  169. column := new(Column)
  170. has, err := db.GetEngine(ctx).ID(columnID).Get(column)
  171. if err != nil {
  172. return nil, err
  173. } else if !has {
  174. return nil, ErrProjectColumnNotExist{ColumnID: columnID}
  175. }
  176. return column, nil
  177. }
  178. // UpdateColumn updates a project column
  179. func UpdateColumn(ctx context.Context, column *Column) error {
  180. var fieldToUpdate []string
  181. if column.Sorting != 0 {
  182. fieldToUpdate = append(fieldToUpdate, "sorting")
  183. }
  184. if column.Title != "" {
  185. fieldToUpdate = append(fieldToUpdate, "title")
  186. }
  187. if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
  188. return fmt.Errorf("bad color code: %s", column.Color)
  189. }
  190. fieldToUpdate = append(fieldToUpdate, "color")
  191. _, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column)
  192. return err
  193. }
  194. // GetColumns fetches all columns related to a project
  195. func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
  196. columns := make([]*Column, 0, 5)
  197. if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&columns); err != nil {
  198. return nil, err
  199. }
  200. return columns, nil
  201. }
  202. // getDefaultColumn return default column and ensure only one exists
  203. func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
  204. var column Column
  205. has, err := db.GetEngine(ctx).
  206. Where("project_id=? AND `default` = ?", p.ID, true).
  207. Desc("id").Get(&column)
  208. if err != nil {
  209. return nil, err
  210. }
  211. if has {
  212. return &column, nil
  213. }
  214. return nil, ErrProjectColumnNotExist{ColumnID: 0}
  215. }
  216. // MustDefaultColumn returns the default column for a project.
  217. // If one exists, it is returned
  218. // If none exists, the first column will be elevated to the default column of this project
  219. func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
  220. c, err := p.getDefaultColumn(ctx)
  221. if err != nil && !IsErrProjectColumnNotExist(err) {
  222. return nil, err
  223. }
  224. if c != nil {
  225. return c, nil
  226. }
  227. var column Column
  228. has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
  229. if err != nil {
  230. return nil, err
  231. }
  232. if has {
  233. column.Default = true
  234. if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil {
  235. return nil, err
  236. }
  237. return &column, nil
  238. }
  239. // create a default column if none is found
  240. column = Column{
  241. ProjectID: p.ID,
  242. Default: true,
  243. Title: "Uncategorized",
  244. CreatorID: p.CreatorID,
  245. }
  246. if _, err := db.GetEngine(ctx).Insert(&column); err != nil {
  247. return nil, err
  248. }
  249. return &column, nil
  250. }
  251. // SetDefaultColumn represents a column for issues not assigned to one
  252. func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
  253. return db.WithTx(ctx, func(ctx context.Context) error {
  254. if _, err := GetColumn(ctx, columnID); err != nil {
  255. return err
  256. }
  257. if _, err := db.GetEngine(ctx).Where(builder.Eq{
  258. "project_id": projectID,
  259. "`default`": true,
  260. }).Cols("`default`").Update(&Column{Default: false}); err != nil {
  261. return err
  262. }
  263. _, err := db.GetEngine(ctx).ID(columnID).
  264. Where(builder.Eq{"project_id": projectID}).
  265. Cols("`default`").Update(&Column{Default: true})
  266. return err
  267. })
  268. }
  269. // UpdateColumnSorting update project column sorting
  270. func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
  271. return db.WithTx(ctx, func(ctx context.Context) error {
  272. for i := range cl {
  273. if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
  274. "sorting",
  275. ).Update(cl[i]); err != nil {
  276. return err
  277. }
  278. }
  279. return nil
  280. })
  281. }
  282. func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
  283. columns := make([]*Column, 0, 5)
  284. if len(columnsIDs) == 0 {
  285. return columns, nil
  286. }
  287. if err := db.GetEngine(ctx).
  288. Where("project_id =?", projectID).
  289. In("id", columnsIDs).
  290. OrderBy("sorting").Find(&columns); err != nil {
  291. return nil, err
  292. }
  293. return columns, nil
  294. }
  295. // MoveColumnsOnProject sorts columns in a project
  296. func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
  297. return db.WithTx(ctx, func(ctx context.Context) error {
  298. sess := db.GetEngine(ctx)
  299. columnIDs := util.ValuesOfMap(sortedColumnIDs)
  300. movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
  301. if err != nil {
  302. return err
  303. }
  304. if len(movedColumns) != len(sortedColumnIDs) {
  305. return errors.New("some columns do not exist")
  306. }
  307. for _, column := range movedColumns {
  308. if column.ProjectID != project.ID {
  309. return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
  310. }
  311. }
  312. for sorting, columnID := range sortedColumnIDs {
  313. if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
  314. return err
  315. }
  316. }
  317. return nil
  318. })
  319. }