You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

projects.go 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package org
  4. import (
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. issues_model "code.gitea.io/gitea/models/issues"
  12. project_model "code.gitea.io/gitea/models/project"
  13. attachment_model "code.gitea.io/gitea/models/repo"
  14. "code.gitea.io/gitea/models/unit"
  15. "code.gitea.io/gitea/modules/base"
  16. "code.gitea.io/gitea/modules/context"
  17. "code.gitea.io/gitea/modules/json"
  18. "code.gitea.io/gitea/modules/setting"
  19. "code.gitea.io/gitea/modules/util"
  20. "code.gitea.io/gitea/modules/web"
  21. shared_user "code.gitea.io/gitea/routers/web/shared/user"
  22. "code.gitea.io/gitea/services/forms"
  23. )
  24. const (
  25. tplProjects base.TplName = "org/projects/list"
  26. tplProjectsNew base.TplName = "org/projects/new"
  27. tplProjectsView base.TplName = "org/projects/view"
  28. )
  29. // MustEnableProjects check if projects are enabled in settings
  30. func MustEnableProjects(ctx *context.Context) {
  31. if unit.TypeProjects.UnitGlobalDisabled() {
  32. ctx.NotFound("EnableKanbanBoard", nil)
  33. return
  34. }
  35. }
  36. // Projects renders the home page of projects
  37. func Projects(ctx *context.Context) {
  38. ctx.Data["Title"] = ctx.Tr("repo.project_board")
  39. sortType := ctx.FormTrim("sort")
  40. isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
  41. page := ctx.FormInt("page")
  42. if page <= 1 {
  43. page = 1
  44. }
  45. var projectType project_model.Type
  46. if ctx.ContextUser.IsOrganization() {
  47. projectType = project_model.TypeOrganization
  48. } else {
  49. projectType = project_model.TypeIndividual
  50. }
  51. projects, total, err := project_model.FindProjects(ctx, project_model.SearchOptions{
  52. OwnerID: ctx.ContextUser.ID,
  53. Page: page,
  54. IsClosed: util.OptionalBoolOf(isShowClosed),
  55. OrderBy: project_model.GetSearchOrderByBySortType(sortType),
  56. Type: projectType,
  57. })
  58. if err != nil {
  59. ctx.ServerError("FindProjects", err)
  60. return
  61. }
  62. opTotal, err := project_model.CountProjects(ctx, project_model.SearchOptions{
  63. OwnerID: ctx.ContextUser.ID,
  64. IsClosed: util.OptionalBoolOf(!isShowClosed),
  65. Type: projectType,
  66. })
  67. if err != nil {
  68. ctx.ServerError("CountProjects", err)
  69. return
  70. }
  71. if isShowClosed {
  72. ctx.Data["OpenCount"] = opTotal
  73. ctx.Data["ClosedCount"] = total
  74. } else {
  75. ctx.Data["OpenCount"] = total
  76. ctx.Data["ClosedCount"] = opTotal
  77. }
  78. ctx.Data["Projects"] = projects
  79. shared_user.RenderUserHeader(ctx)
  80. if isShowClosed {
  81. ctx.Data["State"] = "closed"
  82. } else {
  83. ctx.Data["State"] = "open"
  84. }
  85. for _, project := range projects {
  86. project.RenderedContent = project.Description
  87. }
  88. numPages := 0
  89. if total > 0 {
  90. numPages = (int(total) - 1/setting.UI.IssuePagingNum)
  91. }
  92. pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
  93. pager.AddParam(ctx, "state", "State")
  94. ctx.Data["Page"] = pager
  95. ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
  96. ctx.Data["IsShowClosed"] = isShowClosed
  97. ctx.Data["PageIsViewProjects"] = true
  98. ctx.Data["SortType"] = sortType
  99. ctx.HTML(http.StatusOK, tplProjects)
  100. }
  101. func canWriteProjects(ctx *context.Context) bool {
  102. if ctx.ContextUser.IsOrganization() {
  103. return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects)
  104. }
  105. return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID
  106. }
  107. // RenderNewProject render creating a project page
  108. func RenderNewProject(ctx *context.Context) {
  109. ctx.Data["Title"] = ctx.Tr("repo.projects.new")
  110. ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
  111. ctx.Data["CardTypes"] = project_model.GetCardConfig()
  112. ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
  113. ctx.Data["PageIsViewProjects"] = true
  114. ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
  115. ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects"
  116. shared_user.RenderUserHeader(ctx)
  117. ctx.HTML(http.StatusOK, tplProjectsNew)
  118. }
  119. // NewProjectPost creates a new project
  120. func NewProjectPost(ctx *context.Context) {
  121. form := web.GetForm(ctx).(*forms.CreateProjectForm)
  122. ctx.Data["Title"] = ctx.Tr("repo.projects.new")
  123. shared_user.RenderUserHeader(ctx)
  124. if ctx.HasError() {
  125. RenderNewProject(ctx)
  126. return
  127. }
  128. newProject := project_model.Project{
  129. OwnerID: ctx.ContextUser.ID,
  130. Title: form.Title,
  131. Description: form.Content,
  132. CreatorID: ctx.Doer.ID,
  133. BoardType: form.BoardType,
  134. CardType: form.CardType,
  135. }
  136. if ctx.ContextUser.IsOrganization() {
  137. newProject.Type = project_model.TypeOrganization
  138. } else {
  139. newProject.Type = project_model.TypeIndividual
  140. }
  141. if err := project_model.NewProject(&newProject); err != nil {
  142. ctx.ServerError("NewProject", err)
  143. return
  144. }
  145. ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
  146. ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
  147. }
  148. // ChangeProjectStatus updates the status of a project between "open" and "close"
  149. func ChangeProjectStatus(ctx *context.Context) {
  150. toClose := false
  151. switch ctx.Params(":action") {
  152. case "open":
  153. toClose = false
  154. case "close":
  155. toClose = true
  156. default:
  157. ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
  158. }
  159. id := ctx.ParamsInt64(":id")
  160. if err := project_model.ChangeProjectStatusByRepoIDAndID(0, id, toClose); err != nil {
  161. if project_model.IsErrProjectNotExist(err) {
  162. ctx.NotFound("", err)
  163. } else {
  164. ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
  165. }
  166. return
  167. }
  168. ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action")))
  169. }
  170. // DeleteProject delete a project
  171. func DeleteProject(ctx *context.Context) {
  172. p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  173. if err != nil {
  174. if project_model.IsErrProjectNotExist(err) {
  175. ctx.NotFound("", nil)
  176. } else {
  177. ctx.ServerError("GetProjectByID", err)
  178. }
  179. return
  180. }
  181. if p.OwnerID != ctx.ContextUser.ID {
  182. ctx.NotFound("", nil)
  183. return
  184. }
  185. if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
  186. ctx.Flash.Error("DeleteProjectByID: " + err.Error())
  187. } else {
  188. ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
  189. }
  190. ctx.JSON(http.StatusOK, map[string]any{
  191. "redirect": ctx.ContextUser.HomeLink() + "/-/projects",
  192. })
  193. }
  194. // RenderEditProject allows a project to be edited
  195. func RenderEditProject(ctx *context.Context) {
  196. ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
  197. ctx.Data["PageIsEditProjects"] = true
  198. ctx.Data["PageIsViewProjects"] = true
  199. ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
  200. ctx.Data["CardTypes"] = project_model.GetCardConfig()
  201. shared_user.RenderUserHeader(ctx)
  202. p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  203. if err != nil {
  204. if project_model.IsErrProjectNotExist(err) {
  205. ctx.NotFound("", nil)
  206. } else {
  207. ctx.ServerError("GetProjectByID", err)
  208. }
  209. return
  210. }
  211. if p.OwnerID != ctx.ContextUser.ID {
  212. ctx.NotFound("", nil)
  213. return
  214. }
  215. ctx.Data["projectID"] = p.ID
  216. ctx.Data["title"] = p.Title
  217. ctx.Data["content"] = p.Description
  218. ctx.Data["redirect"] = ctx.FormString("redirect")
  219. ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
  220. ctx.Data["card_type"] = p.CardType
  221. ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID)
  222. ctx.HTML(http.StatusOK, tplProjectsNew)
  223. }
  224. // EditProjectPost response for editing a project
  225. func EditProjectPost(ctx *context.Context) {
  226. form := web.GetForm(ctx).(*forms.CreateProjectForm)
  227. projectID := ctx.ParamsInt64(":id")
  228. ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
  229. ctx.Data["PageIsEditProjects"] = true
  230. ctx.Data["PageIsViewProjects"] = true
  231. ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
  232. ctx.Data["CardTypes"] = project_model.GetCardConfig()
  233. ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID)
  234. shared_user.RenderUserHeader(ctx)
  235. if ctx.HasError() {
  236. ctx.HTML(http.StatusOK, tplProjectsNew)
  237. return
  238. }
  239. p, err := project_model.GetProjectByID(ctx, projectID)
  240. if err != nil {
  241. if project_model.IsErrProjectNotExist(err) {
  242. ctx.NotFound("", nil)
  243. } else {
  244. ctx.ServerError("GetProjectByID", err)
  245. }
  246. return
  247. }
  248. if p.OwnerID != ctx.ContextUser.ID {
  249. ctx.NotFound("", nil)
  250. return
  251. }
  252. p.Title = form.Title
  253. p.Description = form.Content
  254. p.CardType = form.CardType
  255. if err = project_model.UpdateProject(ctx, p); err != nil {
  256. ctx.ServerError("UpdateProjects", err)
  257. return
  258. }
  259. ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
  260. if ctx.FormString("redirect") == "project" {
  261. ctx.Redirect(p.Link())
  262. } else {
  263. ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
  264. }
  265. }
  266. // ViewProject renders the project board for a project
  267. func ViewProject(ctx *context.Context) {
  268. project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  269. if err != nil {
  270. if project_model.IsErrProjectNotExist(err) {
  271. ctx.NotFound("", nil)
  272. } else {
  273. ctx.ServerError("GetProjectByID", err)
  274. }
  275. return
  276. }
  277. if project.OwnerID != ctx.ContextUser.ID {
  278. ctx.NotFound("", nil)
  279. return
  280. }
  281. boards, err := project.GetBoards(ctx)
  282. if err != nil {
  283. ctx.ServerError("GetProjectBoards", err)
  284. return
  285. }
  286. if boards[0].ID == 0 {
  287. boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
  288. }
  289. issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
  290. if err != nil {
  291. ctx.ServerError("LoadIssuesOfBoards", err)
  292. return
  293. }
  294. if project.CardType != project_model.CardTypeTextOnly {
  295. issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
  296. for _, issuesList := range issuesMap {
  297. for _, issue := range issuesList {
  298. if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
  299. issuesAttachmentMap[issue.ID] = issueAttachment
  300. }
  301. }
  302. }
  303. ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
  304. }
  305. linkedPrsMap := make(map[int64][]*issues_model.Issue)
  306. for _, issuesList := range issuesMap {
  307. for _, issue := range issuesList {
  308. var referencedIds []int64
  309. for _, comment := range issue.Comments {
  310. if comment.RefIssueID != 0 && comment.RefIsPull {
  311. referencedIds = append(referencedIds, comment.RefIssueID)
  312. }
  313. }
  314. if len(referencedIds) > 0 {
  315. if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
  316. IssueIDs: referencedIds,
  317. IsPull: util.OptionalBoolTrue,
  318. }); err == nil {
  319. linkedPrsMap[issue.ID] = linkedPrs
  320. }
  321. }
  322. }
  323. }
  324. project.RenderedContent = project.Description
  325. ctx.Data["LinkedPRs"] = linkedPrsMap
  326. ctx.Data["PageIsViewProjects"] = true
  327. ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
  328. ctx.Data["Project"] = project
  329. ctx.Data["IssuesMap"] = issuesMap
  330. ctx.Data["Boards"] = boards
  331. shared_user.RenderUserHeader(ctx)
  332. ctx.HTML(http.StatusOK, tplProjectsView)
  333. }
  334. func getActionIssues(ctx *context.Context) issues_model.IssueList {
  335. commaSeparatedIssueIDs := ctx.FormString("issue_ids")
  336. if len(commaSeparatedIssueIDs) == 0 {
  337. return nil
  338. }
  339. issueIDs := make([]int64, 0, 10)
  340. for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
  341. issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
  342. if err != nil {
  343. ctx.ServerError("ParseInt", err)
  344. return nil
  345. }
  346. issueIDs = append(issueIDs, issueID)
  347. }
  348. issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
  349. if err != nil {
  350. ctx.ServerError("GetIssuesByIDs", err)
  351. return nil
  352. }
  353. // Check access rights for all issues
  354. issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
  355. prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
  356. for _, issue := range issues {
  357. if issue.RepoID != ctx.Repo.Repository.ID {
  358. ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
  359. return nil
  360. }
  361. if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
  362. ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  363. return nil
  364. }
  365. if err = issue.LoadAttributes(ctx); err != nil {
  366. ctx.ServerError("LoadAttributes", err)
  367. return nil
  368. }
  369. }
  370. return issues
  371. }
  372. // UpdateIssueProject change an issue's project
  373. func UpdateIssueProject(ctx *context.Context) {
  374. issues := getActionIssues(ctx)
  375. if ctx.Written() {
  376. return
  377. }
  378. if err := issues.LoadProjects(ctx); err != nil {
  379. ctx.ServerError("LoadProjects", err)
  380. return
  381. }
  382. projectID := ctx.FormInt64("id")
  383. for _, issue := range issues {
  384. if issue.Project != nil {
  385. if issue.Project.ID == projectID {
  386. continue
  387. }
  388. }
  389. if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
  390. ctx.ServerError("ChangeProjectAssign", err)
  391. return
  392. }
  393. }
  394. ctx.JSON(http.StatusOK, map[string]any{
  395. "ok": true,
  396. })
  397. }
  398. // DeleteProjectBoard allows for the deletion of a project board
  399. func DeleteProjectBoard(ctx *context.Context) {
  400. if ctx.Doer == nil {
  401. ctx.JSON(http.StatusForbidden, map[string]string{
  402. "message": "Only signed in users are allowed to perform this action.",
  403. })
  404. return
  405. }
  406. project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  407. if err != nil {
  408. if project_model.IsErrProjectNotExist(err) {
  409. ctx.NotFound("", nil)
  410. } else {
  411. ctx.ServerError("GetProjectByID", err)
  412. }
  413. return
  414. }
  415. pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
  416. if err != nil {
  417. ctx.ServerError("GetProjectBoard", err)
  418. return
  419. }
  420. if pb.ProjectID != ctx.ParamsInt64(":id") {
  421. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  422. "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
  423. })
  424. return
  425. }
  426. if project.OwnerID != ctx.ContextUser.ID {
  427. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  428. "message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
  429. })
  430. return
  431. }
  432. if err := project_model.DeleteBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
  433. ctx.ServerError("DeleteProjectBoardByID", err)
  434. return
  435. }
  436. ctx.JSON(http.StatusOK, map[string]any{
  437. "ok": true,
  438. })
  439. }
  440. // AddBoardToProjectPost allows a new board to be added to a project.
  441. func AddBoardToProjectPost(ctx *context.Context) {
  442. form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
  443. project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  444. if err != nil {
  445. if project_model.IsErrProjectNotExist(err) {
  446. ctx.NotFound("", nil)
  447. } else {
  448. ctx.ServerError("GetProjectByID", err)
  449. }
  450. return
  451. }
  452. if err := project_model.NewBoard(&project_model.Board{
  453. ProjectID: project.ID,
  454. Title: form.Title,
  455. Color: form.Color,
  456. CreatorID: ctx.Doer.ID,
  457. }); err != nil {
  458. ctx.ServerError("NewProjectBoard", err)
  459. return
  460. }
  461. ctx.JSON(http.StatusOK, map[string]any{
  462. "ok": true,
  463. })
  464. }
  465. // CheckProjectBoardChangePermissions check permission
  466. func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
  467. if ctx.Doer == nil {
  468. ctx.JSON(http.StatusForbidden, map[string]string{
  469. "message": "Only signed in users are allowed to perform this action.",
  470. })
  471. return nil, nil
  472. }
  473. project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  474. if err != nil {
  475. if project_model.IsErrProjectNotExist(err) {
  476. ctx.NotFound("", nil)
  477. } else {
  478. ctx.ServerError("GetProjectByID", err)
  479. }
  480. return nil, nil
  481. }
  482. board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
  483. if err != nil {
  484. ctx.ServerError("GetProjectBoard", err)
  485. return nil, nil
  486. }
  487. if board.ProjectID != ctx.ParamsInt64(":id") {
  488. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  489. "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
  490. })
  491. return nil, nil
  492. }
  493. if project.OwnerID != ctx.ContextUser.ID {
  494. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  495. "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID),
  496. })
  497. return nil, nil
  498. }
  499. return project, board
  500. }
  501. // EditProjectBoard allows a project board's to be updated
  502. func EditProjectBoard(ctx *context.Context) {
  503. form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
  504. _, board := CheckProjectBoardChangePermissions(ctx)
  505. if ctx.Written() {
  506. return
  507. }
  508. if form.Title != "" {
  509. board.Title = form.Title
  510. }
  511. board.Color = form.Color
  512. if form.Sorting != 0 {
  513. board.Sorting = form.Sorting
  514. }
  515. if err := project_model.UpdateBoard(ctx, board); err != nil {
  516. ctx.ServerError("UpdateProjectBoard", err)
  517. return
  518. }
  519. ctx.JSON(http.StatusOK, map[string]any{
  520. "ok": true,
  521. })
  522. }
  523. // SetDefaultProjectBoard set default board for uncategorized issues/pulls
  524. func SetDefaultProjectBoard(ctx *context.Context) {
  525. project, board := CheckProjectBoardChangePermissions(ctx)
  526. if ctx.Written() {
  527. return
  528. }
  529. if err := project_model.SetDefaultBoard(project.ID, board.ID); err != nil {
  530. ctx.ServerError("SetDefaultBoard", err)
  531. return
  532. }
  533. ctx.JSON(http.StatusOK, map[string]any{
  534. "ok": true,
  535. })
  536. }
  537. // UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls
  538. func UnsetDefaultProjectBoard(ctx *context.Context) {
  539. project, _ := CheckProjectBoardChangePermissions(ctx)
  540. if ctx.Written() {
  541. return
  542. }
  543. if err := project_model.SetDefaultBoard(project.ID, 0); err != nil {
  544. ctx.ServerError("SetDefaultBoard", err)
  545. return
  546. }
  547. ctx.JSON(http.StatusOK, map[string]any{
  548. "ok": true,
  549. })
  550. }
  551. // MoveIssues moves or keeps issues in a column and sorts them inside that column
  552. func MoveIssues(ctx *context.Context) {
  553. if ctx.Doer == nil {
  554. ctx.JSON(http.StatusForbidden, map[string]string{
  555. "message": "Only signed in users are allowed to perform this action.",
  556. })
  557. return
  558. }
  559. project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
  560. if err != nil {
  561. if project_model.IsErrProjectNotExist(err) {
  562. ctx.NotFound("ProjectNotExist", nil)
  563. } else {
  564. ctx.ServerError("GetProjectByID", err)
  565. }
  566. return
  567. }
  568. if project.OwnerID != ctx.ContextUser.ID {
  569. ctx.NotFound("InvalidRepoID", nil)
  570. return
  571. }
  572. var board *project_model.Board
  573. if ctx.ParamsInt64(":boardID") == 0 {
  574. board = &project_model.Board{
  575. ID: 0,
  576. ProjectID: project.ID,
  577. Title: ctx.Tr("repo.projects.type.uncategorized"),
  578. }
  579. } else {
  580. board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
  581. if err != nil {
  582. if project_model.IsErrProjectBoardNotExist(err) {
  583. ctx.NotFound("ProjectBoardNotExist", nil)
  584. } else {
  585. ctx.ServerError("GetProjectBoard", err)
  586. }
  587. return
  588. }
  589. if board.ProjectID != project.ID {
  590. ctx.NotFound("BoardNotInProject", nil)
  591. return
  592. }
  593. }
  594. type movedIssuesForm struct {
  595. Issues []struct {
  596. IssueID int64 `json:"issueID"`
  597. Sorting int64 `json:"sorting"`
  598. } `json:"issues"`
  599. }
  600. form := &movedIssuesForm{}
  601. if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
  602. ctx.ServerError("DecodeMovedIssuesForm", err)
  603. }
  604. issueIDs := make([]int64, 0, len(form.Issues))
  605. sortedIssueIDs := make(map[int64]int64)
  606. for _, issue := range form.Issues {
  607. issueIDs = append(issueIDs, issue.IssueID)
  608. sortedIssueIDs[issue.Sorting] = issue.IssueID
  609. }
  610. movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
  611. if err != nil {
  612. if issues_model.IsErrIssueNotExist(err) {
  613. ctx.NotFound("IssueNotExisting", nil)
  614. } else {
  615. ctx.ServerError("GetIssueByID", err)
  616. }
  617. return
  618. }
  619. if len(movedIssues) != len(form.Issues) {
  620. ctx.ServerError("some issues do not exist", errors.New("some issues do not exist"))
  621. return
  622. }
  623. if _, err = movedIssues.LoadRepositories(ctx); err != nil {
  624. ctx.ServerError("LoadRepositories", err)
  625. return
  626. }
  627. for _, issue := range movedIssues {
  628. if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID {
  629. ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID"))
  630. return
  631. }
  632. }
  633. if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil {
  634. ctx.ServerError("MoveIssuesOnProjectBoard", err)
  635. return
  636. }
  637. ctx.JSON(http.StatusOK, map[string]any{
  638. "ok": true,
  639. })
  640. }