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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. // Copyright 2020 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "fmt"
  7. "net/http"
  8. "strings"
  9. "code.gitea.io/gitea/models"
  10. "code.gitea.io/gitea/modules/base"
  11. "code.gitea.io/gitea/modules/context"
  12. "code.gitea.io/gitea/modules/markup"
  13. "code.gitea.io/gitea/modules/markup/markdown"
  14. "code.gitea.io/gitea/modules/setting"
  15. "code.gitea.io/gitea/modules/util"
  16. "code.gitea.io/gitea/modules/web"
  17. "code.gitea.io/gitea/services/forms"
  18. )
  19. const (
  20. tplProjects base.TplName = "repo/projects/list"
  21. tplProjectsNew base.TplName = "repo/projects/new"
  22. tplProjectsView base.TplName = "repo/projects/view"
  23. tplGenericProjectsNew base.TplName = "user/project"
  24. )
  25. // MustEnableProjects check if projects are enabled in settings
  26. func MustEnableProjects(ctx *context.Context) {
  27. if models.UnitTypeProjects.UnitGlobalDisabled() {
  28. ctx.NotFound("EnableKanbanBoard", nil)
  29. return
  30. }
  31. if ctx.Repo.Repository != nil {
  32. if !ctx.Repo.CanRead(models.UnitTypeProjects) {
  33. ctx.NotFound("MustEnableProjects", nil)
  34. return
  35. }
  36. }
  37. }
  38. // Projects renders the home page of projects
  39. func Projects(ctx *context.Context) {
  40. ctx.Data["Title"] = ctx.Tr("repo.project_board")
  41. sortType := ctx.QueryTrim("sort")
  42. isShowClosed := strings.ToLower(ctx.QueryTrim("state")) == "closed"
  43. repo := ctx.Repo.Repository
  44. page := ctx.QueryInt("page")
  45. if page <= 1 {
  46. page = 1
  47. }
  48. ctx.Data["OpenCount"] = repo.NumOpenProjects
  49. ctx.Data["ClosedCount"] = repo.NumClosedProjects
  50. var total int
  51. if !isShowClosed {
  52. total = repo.NumOpenProjects
  53. } else {
  54. total = repo.NumClosedProjects
  55. }
  56. projects, count, err := models.GetProjects(models.ProjectSearchOptions{
  57. RepoID: repo.ID,
  58. Page: page,
  59. IsClosed: util.OptionalBoolOf(isShowClosed),
  60. SortType: sortType,
  61. Type: models.ProjectTypeRepository,
  62. })
  63. if err != nil {
  64. ctx.ServerError("GetProjects", err)
  65. return
  66. }
  67. for i := range projects {
  68. projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  69. URLPrefix: ctx.Repo.RepoLink,
  70. Metas: ctx.Repo.Repository.ComposeMetas(),
  71. }, projects[i].Description)
  72. if err != nil {
  73. ctx.ServerError("RenderString", err)
  74. return
  75. }
  76. }
  77. ctx.Data["Projects"] = projects
  78. if isShowClosed {
  79. ctx.Data["State"] = "closed"
  80. } else {
  81. ctx.Data["State"] = "open"
  82. }
  83. numPages := 0
  84. if count > 0 {
  85. numPages = int((int(count) - 1) / setting.UI.IssuePagingNum)
  86. }
  87. pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
  88. pager.AddParam(ctx, "state", "State")
  89. ctx.Data["Page"] = pager
  90. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  91. ctx.Data["IsShowClosed"] = isShowClosed
  92. ctx.Data["IsProjectsPage"] = true
  93. ctx.Data["SortType"] = sortType
  94. ctx.HTML(http.StatusOK, tplProjects)
  95. }
  96. // NewProject render creating a project page
  97. func NewProject(ctx *context.Context) {
  98. ctx.Data["Title"] = ctx.Tr("repo.projects.new")
  99. ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
  100. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  101. ctx.HTML(http.StatusOK, tplProjectsNew)
  102. }
  103. // NewProjectPost creates a new project
  104. func NewProjectPost(ctx *context.Context) {
  105. form := web.GetForm(ctx).(*forms.CreateProjectForm)
  106. ctx.Data["Title"] = ctx.Tr("repo.projects.new")
  107. if ctx.HasError() {
  108. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  109. ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
  110. ctx.HTML(http.StatusOK, tplProjectsNew)
  111. return
  112. }
  113. if err := models.NewProject(&models.Project{
  114. RepoID: ctx.Repo.Repository.ID,
  115. Title: form.Title,
  116. Description: form.Content,
  117. CreatorID: ctx.User.ID,
  118. BoardType: form.BoardType,
  119. Type: models.ProjectTypeRepository,
  120. }); err != nil {
  121. ctx.ServerError("NewProject", err)
  122. return
  123. }
  124. ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
  125. ctx.Redirect(ctx.Repo.RepoLink + "/projects")
  126. }
  127. // ChangeProjectStatus updates the status of a project between "open" and "close"
  128. func ChangeProjectStatus(ctx *context.Context) {
  129. toClose := false
  130. switch ctx.Params(":action") {
  131. case "open":
  132. toClose = false
  133. case "close":
  134. toClose = true
  135. default:
  136. ctx.Redirect(ctx.Repo.RepoLink + "/projects")
  137. }
  138. id := ctx.ParamsInt64(":id")
  139. if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
  140. if models.IsErrProjectNotExist(err) {
  141. ctx.NotFound("", err)
  142. } else {
  143. ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
  144. }
  145. return
  146. }
  147. ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action"))
  148. }
  149. // DeleteProject delete a project
  150. func DeleteProject(ctx *context.Context) {
  151. p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  152. if err != nil {
  153. if models.IsErrProjectNotExist(err) {
  154. ctx.NotFound("", nil)
  155. } else {
  156. ctx.ServerError("GetProjectByID", err)
  157. }
  158. return
  159. }
  160. if p.RepoID != ctx.Repo.Repository.ID {
  161. ctx.NotFound("", nil)
  162. return
  163. }
  164. if err := models.DeleteProjectByID(p.ID); err != nil {
  165. ctx.Flash.Error("DeleteProjectByID: " + err.Error())
  166. } else {
  167. ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
  168. }
  169. ctx.JSON(http.StatusOK, map[string]interface{}{
  170. "redirect": ctx.Repo.RepoLink + "/projects",
  171. })
  172. }
  173. // EditProject allows a project to be edited
  174. func EditProject(ctx *context.Context) {
  175. ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
  176. ctx.Data["PageIsProjects"] = true
  177. ctx.Data["PageIsEditProjects"] = true
  178. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  179. p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  180. if err != nil {
  181. if models.IsErrProjectNotExist(err) {
  182. ctx.NotFound("", nil)
  183. } else {
  184. ctx.ServerError("GetProjectByID", err)
  185. }
  186. return
  187. }
  188. if p.RepoID != ctx.Repo.Repository.ID {
  189. ctx.NotFound("", nil)
  190. return
  191. }
  192. ctx.Data["title"] = p.Title
  193. ctx.Data["content"] = p.Description
  194. ctx.HTML(http.StatusOK, tplProjectsNew)
  195. }
  196. // EditProjectPost response for editing a project
  197. func EditProjectPost(ctx *context.Context) {
  198. form := web.GetForm(ctx).(*forms.CreateProjectForm)
  199. ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
  200. ctx.Data["PageIsProjects"] = true
  201. ctx.Data["PageIsEditProjects"] = true
  202. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  203. if ctx.HasError() {
  204. ctx.HTML(http.StatusOK, tplProjectsNew)
  205. return
  206. }
  207. p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  208. if err != nil {
  209. if models.IsErrProjectNotExist(err) {
  210. ctx.NotFound("", nil)
  211. } else {
  212. ctx.ServerError("GetProjectByID", err)
  213. }
  214. return
  215. }
  216. if p.RepoID != ctx.Repo.Repository.ID {
  217. ctx.NotFound("", nil)
  218. return
  219. }
  220. p.Title = form.Title
  221. p.Description = form.Content
  222. if err = models.UpdateProject(p); err != nil {
  223. ctx.ServerError("UpdateProjects", err)
  224. return
  225. }
  226. ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
  227. ctx.Redirect(ctx.Repo.RepoLink + "/projects")
  228. }
  229. // ViewProject renders the project board for a project
  230. func ViewProject(ctx *context.Context) {
  231. project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  232. if err != nil {
  233. if models.IsErrProjectNotExist(err) {
  234. ctx.NotFound("", nil)
  235. } else {
  236. ctx.ServerError("GetProjectByID", err)
  237. }
  238. return
  239. }
  240. if project.RepoID != ctx.Repo.Repository.ID {
  241. ctx.NotFound("", nil)
  242. return
  243. }
  244. boards, err := models.GetProjectBoards(project.ID)
  245. if err != nil {
  246. ctx.ServerError("GetProjectBoards", err)
  247. return
  248. }
  249. if boards[0].ID == 0 {
  250. boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
  251. }
  252. issueList, err := boards.LoadIssues()
  253. if err != nil {
  254. ctx.ServerError("LoadIssuesOfBoards", err)
  255. return
  256. }
  257. ctx.Data["Issues"] = issueList
  258. linkedPrsMap := make(map[int64][]*models.Issue)
  259. for _, issue := range issueList {
  260. var referencedIds []int64
  261. for _, comment := range issue.Comments {
  262. if comment.RefIssueID != 0 && comment.RefIsPull {
  263. referencedIds = append(referencedIds, comment.RefIssueID)
  264. }
  265. }
  266. if len(referencedIds) > 0 {
  267. if linkedPrs, err := models.Issues(&models.IssuesOptions{
  268. IssueIDs: referencedIds,
  269. IsPull: util.OptionalBoolTrue,
  270. }); err == nil {
  271. linkedPrsMap[issue.ID] = linkedPrs
  272. }
  273. }
  274. }
  275. ctx.Data["LinkedPRs"] = linkedPrsMap
  276. project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  277. URLPrefix: ctx.Repo.RepoLink,
  278. Metas: ctx.Repo.Repository.ComposeMetas(),
  279. }, project.Description)
  280. if err != nil {
  281. ctx.ServerError("RenderString", err)
  282. return
  283. }
  284. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  285. ctx.Data["Project"] = project
  286. ctx.Data["Boards"] = boards
  287. ctx.Data["PageIsProjects"] = true
  288. ctx.Data["RequiresDraggable"] = true
  289. ctx.HTML(http.StatusOK, tplProjectsView)
  290. }
  291. // UpdateIssueProject change an issue's project
  292. func UpdateIssueProject(ctx *context.Context) {
  293. issues := getActionIssues(ctx)
  294. if ctx.Written() {
  295. return
  296. }
  297. projectID := ctx.QueryInt64("id")
  298. for _, issue := range issues {
  299. oldProjectID := issue.ProjectID()
  300. if oldProjectID == projectID {
  301. continue
  302. }
  303. if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
  304. ctx.ServerError("ChangeProjectAssign", err)
  305. return
  306. }
  307. }
  308. ctx.JSON(http.StatusOK, map[string]interface{}{
  309. "ok": true,
  310. })
  311. }
  312. // DeleteProjectBoard allows for the deletion of a project board
  313. func DeleteProjectBoard(ctx *context.Context) {
  314. if ctx.User == nil {
  315. ctx.JSON(http.StatusForbidden, map[string]string{
  316. "message": "Only signed in users are allowed to perform this action.",
  317. })
  318. return
  319. }
  320. if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
  321. ctx.JSON(http.StatusForbidden, map[string]string{
  322. "message": "Only authorized users are allowed to perform this action.",
  323. })
  324. return
  325. }
  326. project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  327. if err != nil {
  328. if models.IsErrProjectNotExist(err) {
  329. ctx.NotFound("", nil)
  330. } else {
  331. ctx.ServerError("GetProjectByID", err)
  332. }
  333. return
  334. }
  335. pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
  336. if err != nil {
  337. ctx.ServerError("GetProjectBoard", err)
  338. return
  339. }
  340. if pb.ProjectID != ctx.ParamsInt64(":id") {
  341. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  342. "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
  343. })
  344. return
  345. }
  346. if project.RepoID != ctx.Repo.Repository.ID {
  347. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  348. "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
  349. })
  350. return
  351. }
  352. if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
  353. ctx.ServerError("DeleteProjectBoardByID", err)
  354. return
  355. }
  356. ctx.JSON(http.StatusOK, map[string]interface{}{
  357. "ok": true,
  358. })
  359. }
  360. // AddBoardToProjectPost allows a new board to be added to a project.
  361. func AddBoardToProjectPost(ctx *context.Context) {
  362. form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
  363. if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
  364. ctx.JSON(http.StatusForbidden, map[string]string{
  365. "message": "Only authorized users are allowed to perform this action.",
  366. })
  367. return
  368. }
  369. project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  370. if err != nil {
  371. if models.IsErrProjectNotExist(err) {
  372. ctx.NotFound("", nil)
  373. } else {
  374. ctx.ServerError("GetProjectByID", err)
  375. }
  376. return
  377. }
  378. if err := models.NewProjectBoard(&models.ProjectBoard{
  379. ProjectID: project.ID,
  380. Title: form.Title,
  381. CreatorID: ctx.User.ID,
  382. }); err != nil {
  383. ctx.ServerError("NewProjectBoard", err)
  384. return
  385. }
  386. ctx.JSON(http.StatusOK, map[string]interface{}{
  387. "ok": true,
  388. })
  389. }
  390. func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
  391. if ctx.User == nil {
  392. ctx.JSON(http.StatusForbidden, map[string]string{
  393. "message": "Only signed in users are allowed to perform this action.",
  394. })
  395. return nil, nil
  396. }
  397. if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
  398. ctx.JSON(http.StatusForbidden, map[string]string{
  399. "message": "Only authorized users are allowed to perform this action.",
  400. })
  401. return nil, nil
  402. }
  403. project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  404. if err != nil {
  405. if models.IsErrProjectNotExist(err) {
  406. ctx.NotFound("", nil)
  407. } else {
  408. ctx.ServerError("GetProjectByID", err)
  409. }
  410. return nil, nil
  411. }
  412. board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
  413. if err != nil {
  414. ctx.ServerError("GetProjectBoard", err)
  415. return nil, nil
  416. }
  417. if board.ProjectID != ctx.ParamsInt64(":id") {
  418. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  419. "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
  420. })
  421. return nil, nil
  422. }
  423. if project.RepoID != ctx.Repo.Repository.ID {
  424. ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
  425. "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
  426. })
  427. return nil, nil
  428. }
  429. return project, board
  430. }
  431. // EditProjectBoard allows a project board's to be updated
  432. func EditProjectBoard(ctx *context.Context) {
  433. form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
  434. _, board := checkProjectBoardChangePermissions(ctx)
  435. if ctx.Written() {
  436. return
  437. }
  438. if form.Title != "" {
  439. board.Title = form.Title
  440. }
  441. if form.Sorting != 0 {
  442. board.Sorting = form.Sorting
  443. }
  444. if err := models.UpdateProjectBoard(board); err != nil {
  445. ctx.ServerError("UpdateProjectBoard", err)
  446. return
  447. }
  448. ctx.JSON(http.StatusOK, map[string]interface{}{
  449. "ok": true,
  450. })
  451. }
  452. // SetDefaultProjectBoard set default board for uncategorized issues/pulls
  453. func SetDefaultProjectBoard(ctx *context.Context) {
  454. project, board := checkProjectBoardChangePermissions(ctx)
  455. if ctx.Written() {
  456. return
  457. }
  458. if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
  459. ctx.ServerError("SetDefaultBoard", err)
  460. return
  461. }
  462. ctx.JSON(http.StatusOK, map[string]interface{}{
  463. "ok": true,
  464. })
  465. }
  466. // MoveIssueAcrossBoards move a card from one board to another in a project
  467. func MoveIssueAcrossBoards(ctx *context.Context) {
  468. if ctx.User == nil {
  469. ctx.JSON(http.StatusForbidden, map[string]string{
  470. "message": "Only signed in users are allowed to perform this action.",
  471. })
  472. return
  473. }
  474. if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
  475. ctx.JSON(http.StatusForbidden, map[string]string{
  476. "message": "Only authorized users are allowed to perform this action.",
  477. })
  478. return
  479. }
  480. p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
  481. if err != nil {
  482. if models.IsErrProjectNotExist(err) {
  483. ctx.NotFound("", nil)
  484. } else {
  485. ctx.ServerError("GetProjectByID", err)
  486. }
  487. return
  488. }
  489. if p.RepoID != ctx.Repo.Repository.ID {
  490. ctx.NotFound("", nil)
  491. return
  492. }
  493. var board *models.ProjectBoard
  494. if ctx.ParamsInt64(":boardID") == 0 {
  495. board = &models.ProjectBoard{
  496. ID: 0,
  497. ProjectID: 0,
  498. Title: ctx.Tr("repo.projects.type.uncategorized"),
  499. }
  500. } else {
  501. board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
  502. if err != nil {
  503. if models.IsErrProjectBoardNotExist(err) {
  504. ctx.NotFound("", nil)
  505. } else {
  506. ctx.ServerError("GetProjectBoard", err)
  507. }
  508. return
  509. }
  510. if board.ProjectID != p.ID {
  511. ctx.NotFound("", nil)
  512. return
  513. }
  514. }
  515. issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
  516. if err != nil {
  517. if models.IsErrIssueNotExist(err) {
  518. ctx.NotFound("", nil)
  519. } else {
  520. ctx.ServerError("GetIssueByID", err)
  521. }
  522. return
  523. }
  524. if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
  525. ctx.ServerError("MoveIssueAcrossProjectBoards", err)
  526. return
  527. }
  528. ctx.JSON(http.StatusOK, map[string]interface{}{
  529. "ok": true,
  530. })
  531. }
  532. // CreateProject renders the generic project creation page
  533. func CreateProject(ctx *context.Context) {
  534. ctx.Data["Title"] = ctx.Tr("repo.projects.new")
  535. ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
  536. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  537. ctx.HTML(http.StatusOK, tplGenericProjectsNew)
  538. }
  539. // CreateProjectPost creates an individual and/or organization project
  540. func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
  541. user := checkContextUser(ctx, form.UID)
  542. if ctx.Written() {
  543. return
  544. }
  545. ctx.Data["ContextUser"] = user
  546. if ctx.HasError() {
  547. ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
  548. ctx.HTML(http.StatusOK, tplGenericProjectsNew)
  549. return
  550. }
  551. var projectType = models.ProjectTypeIndividual
  552. if user.IsOrganization() {
  553. projectType = models.ProjectTypeOrganization
  554. }
  555. if err := models.NewProject(&models.Project{
  556. Title: form.Title,
  557. Description: form.Content,
  558. CreatorID: user.ID,
  559. BoardType: form.BoardType,
  560. Type: projectType,
  561. }); err != nil {
  562. ctx.ServerError("NewProject", err)
  563. return
  564. }
  565. ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
  566. ctx.Redirect(setting.AppSubURL + "/")
  567. }