gitea源码

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