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

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