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

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