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 18KB

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