// Copyright 2020 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package repo import ( "fmt" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) const ( tplProjects base.TplName = "repo/projects/list" tplProjectsNew base.TplName = "repo/projects/new" tplProjectsView base.TplName = "repo/projects/view" tplGenericProjectsNew base.TplName = "user/project" ) // MustEnableProjects check if projects are enabled in settings func MustEnableProjects(ctx *context.Context) { if models.UnitTypeProjects.UnitGlobalDisabled() { ctx.NotFound("EnableKanbanBoard", nil) return } if ctx.Repo.Repository != nil { if !ctx.Repo.CanRead(models.UnitTypeProjects) { ctx.NotFound("MustEnableProjects", nil) return } } } // Projects renders the home page of projects func Projects(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.project_board") sortType := ctx.QueryTrim("sort") isShowClosed := strings.ToLower(ctx.QueryTrim("state")) == "closed" repo := ctx.Repo.Repository page := ctx.QueryInt("page") if page <= 1 { page = 1 } ctx.Data["OpenCount"] = repo.NumOpenProjects ctx.Data["ClosedCount"] = repo.NumClosedProjects var total int if !isShowClosed { total = repo.NumOpenProjects } else { total = repo.NumClosedProjects } projects, count, err := models.GetProjects(models.ProjectSearchOptions{ RepoID: repo.ID, Page: page, IsClosed: util.OptionalBoolOf(isShowClosed), SortType: sortType, Type: models.ProjectTypeRepository, }) if err != nil { ctx.ServerError("GetProjects", err) return } for i := range projects { projects[i].RenderedContent = string(markdown.Render([]byte(projects[i].Description), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) } ctx.Data["Projects"] = projects if isShowClosed { ctx.Data["State"] = "closed" } else { ctx.Data["State"] = "open" } numPages := 0 if count > 0 { numPages = int((int(count) - 1) / setting.UI.IssuePagingNum) } pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) pager.AddParam(ctx, "state", "State") ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["IsProjectsPage"] = true ctx.Data["SortType"] = sortType ctx.HTML(200, tplProjects) } // NewProject render creating a project page func NewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["ProjectTypes"] = models.GetProjectsConfig() ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) ctx.HTML(200, tplProjectsNew) } // NewProjectPost creates a new project func NewProjectPost(ctx *context.Context, form auth.CreateProjectForm) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") if ctx.HasError() { ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) ctx.Data["ProjectTypes"] = models.GetProjectsConfig() ctx.HTML(200, tplProjectsNew) return } if err := models.NewProject(&models.Project{ RepoID: ctx.Repo.Repository.ID, Title: form.Title, Description: form.Content, CreatorID: ctx.User.ID, BoardType: form.BoardType, Type: models.ProjectTypeRepository, }); err != nil { ctx.ServerError("NewProject", err) return } ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) ctx.Redirect(ctx.Repo.RepoLink + "/projects") } // ChangeProjectStatus updates the status of a project between "open" and "close" func ChangeProjectStatus(ctx *context.Context) { toClose := false switch ctx.Params(":action") { case "open": toClose = false case "close": toClose = true default: ctx.Redirect(ctx.Repo.RepoLink + "/projects") } id := ctx.ParamsInt64(":id") if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", err) } else { ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err) } return } ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action")) } // DeleteProject delete a project func DeleteProject(ctx *context.Context) { p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } if p.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) return } if err := models.DeleteProjectByID(p.ID); err != nil { ctx.Flash.Error("DeleteProjectByID: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success")) } ctx.JSON(200, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/projects", }) } // EditProject allows a project to be edited func EditProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsProjects"] = true ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } if p.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) return } ctx.Data["title"] = p.Title ctx.Data["content"] = p.Description ctx.HTML(200, tplProjectsNew) } // EditProjectPost response for editing a project func EditProjectPost(ctx *context.Context, form auth.CreateProjectForm) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsProjects"] = true ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) if ctx.HasError() { ctx.HTML(200, tplProjectsNew) return } p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } if p.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) return } p.Title = form.Title p.Description = form.Content if err = models.UpdateProject(p); err != nil { ctx.ServerError("UpdateProjects", err) return } ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title)) ctx.Redirect(ctx.Repo.RepoLink + "/projects") } // ViewProject renders the project board for a project func ViewProject(ctx *context.Context) { project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } if project.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) return } boards, err := models.GetProjectBoards(project.ID) if err != nil { ctx.ServerError("GetProjectBoards", err) return } if boards[0].ID == 0 { boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") } issueList, err := boards.LoadIssues() if err != nil { ctx.ServerError("LoadIssuesOfBoards", err) return } ctx.Data["Issues"] = issueList linkedPrsMap := make(map[int64][]*models.Issue) for _, issue := range issueList { var referencedIds []int64 for _, comment := range issue.Comments { if comment.RefIssueID != 0 && comment.RefIsPull { referencedIds = append(referencedIds, comment.RefIssueID) } } if len(referencedIds) > 0 { if linkedPrs, err := models.Issues(&models.IssuesOptions{ IssueIDs: referencedIds, IsPull: util.OptionalBoolTrue, }); err == nil { linkedPrsMap[issue.ID] = linkedPrs } } } ctx.Data["LinkedPRs"] = linkedPrsMap project.RenderedContent = string(markdown.Render([]byte(project.Description), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) ctx.Data["Project"] = project ctx.Data["Boards"] = boards ctx.Data["PageIsProjects"] = true ctx.Data["RequiresDraggable"] = true ctx.HTML(200, tplProjectsView) } // UpdateIssueProject change an issue's project func UpdateIssueProject(ctx *context.Context) { issues := getActionIssues(ctx) if ctx.Written() { return } projectID := ctx.QueryInt64("id") for _, issue := range issues { oldProjectID := issue.ProjectID() if oldProjectID == projectID { continue } if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil { ctx.ServerError("ChangeProjectAssign", err) return } } ctx.JSON(200, map[string]interface{}{ "ok": true, }) } // DeleteProjectBoard allows for the deletion of a project board func DeleteProjectBoard(ctx *context.Context) { if ctx.User == nil { ctx.JSON(403, map[string]string{ "message": "Only signed in users are allowed to perform this action.", }) return } if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) { ctx.JSON(403, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) return } project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID")) if err != nil { ctx.ServerError("GetProjectBoard", err) return } if pb.ProjectID != ctx.ParamsInt64(":id") { ctx.JSON(422, map[string]string{ "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) return } if project.RepoID != ctx.Repo.Repository.ID { ctx.JSON(422, map[string]string{ "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID), }) return } if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil { ctx.ServerError("DeleteProjectBoardByID", err) return } ctx.JSON(200, map[string]interface{}{ "ok": true, }) } // AddBoardToProjectPost allows a new board to be added to a project. func AddBoardToProjectPost(ctx *context.Context, form auth.EditProjectBoardTitleForm) { if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) { ctx.JSON(403, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) return } project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } if err := models.NewProjectBoard(&models.ProjectBoard{ ProjectID: project.ID, Title: form.Title, CreatorID: ctx.User.ID, }); err != nil { ctx.ServerError("NewProjectBoard", err) return } ctx.JSON(200, map[string]interface{}{ "ok": true, }) } func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) { if ctx.User == nil { ctx.JSON(403, map[string]string{ "message": "Only signed in users are allowed to perform this action.", }) return nil, nil } if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) { ctx.JSON(403, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) return nil, nil } project, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return nil, nil } board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID")) if err != nil { ctx.ServerError("GetProjectBoard", err) return nil, nil } if board.ProjectID != ctx.ParamsInt64(":id") { ctx.JSON(422, map[string]string{ "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), }) return nil, nil } if project.RepoID != ctx.Repo.Repository.ID { ctx.JSON(422, map[string]string{ "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID), }) return nil, nil } return project, board } // EditProjectBoardTitle allows a project board's title to be updated func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitleForm) { _, board := checkProjectBoardChangePermissions(ctx) if ctx.Written() { return } if form.Title != "" { board.Title = form.Title } if err := models.UpdateProjectBoard(board); err != nil { ctx.ServerError("UpdateProjectBoard", err) return } ctx.JSON(200, map[string]interface{}{ "ok": true, }) } // SetDefaultProjectBoard set default board for uncategorized issues/pulls func SetDefaultProjectBoard(ctx *context.Context) { project, board := checkProjectBoardChangePermissions(ctx) if ctx.Written() { return } if err := models.SetDefaultBoard(project.ID, board.ID); err != nil { ctx.ServerError("SetDefaultBoard", err) return } ctx.JSON(200, map[string]interface{}{ "ok": true, }) } // MoveIssueAcrossBoards move a card from one board to another in a project func MoveIssueAcrossBoards(ctx *context.Context) { if ctx.User == nil { ctx.JSON(403, map[string]string{ "message": "Only signed in users are allowed to perform this action.", }) return } if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) { ctx.JSON(403, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) return } p, err := models.GetProjectByID(ctx.ParamsInt64(":id")) if err != nil { if models.IsErrProjectNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectByID", err) } return } if p.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) return } var board *models.ProjectBoard if ctx.ParamsInt64(":boardID") == 0 { board = &models.ProjectBoard{ ID: 0, ProjectID: 0, Title: ctx.Tr("repo.projects.type.uncategorized"), } } else { board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID")) if err != nil { if models.IsErrProjectBoardNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetProjectBoard", err) } return } if board.ProjectID != p.ID { ctx.NotFound("", nil) return } } issue, err := models.GetIssueByID(ctx.ParamsInt64(":index")) if err != nil { if models.IsErrIssueNotExist(err) { ctx.NotFound("", nil) } else { ctx.ServerError("GetIssueByID", err) } return } if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil { ctx.ServerError("MoveIssueAcrossProjectBoards", err) return } ctx.JSON(200, map[string]interface{}{ "ok": true, }) } // CreateProject renders the generic project creation page func CreateProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["ProjectTypes"] = models.GetProjectsConfig() ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) ctx.HTML(200, tplGenericProjectsNew) } // CreateProjectPost creates an individual and/or organization project func CreateProjectPost(ctx *context.Context, form auth.UserCreateProjectForm) { user := checkContextUser(ctx, form.UID) if ctx.Written() { return } ctx.Data["ContextUser"] = user if ctx.HasError() { ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) ctx.HTML(200, tplGenericProjectsNew) return } var projectType = models.ProjectTypeIndividual if user.IsOrganization() { projectType = models.ProjectTypeOrganization } if err := models.NewProject(&models.Project{ Title: form.Title, Description: form.Content, CreatorID: user.ID, BoardType: form.BoardType, Type: projectType, }); err != nil { ctx.ServerError("NewProject", err) return } ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) ctx.Redirect(setting.AppSubURL + "/") }