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

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