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

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