Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

projects.go 18KB

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