Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
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.
655 lines
17 KiB
655 lines
17 KiB
// 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" |
|
"net/http" |
|
"net/url" |
|
"strings" |
|
|
|
"code.gitea.io/gitea/models" |
|
"code.gitea.io/gitea/models/perm" |
|
"code.gitea.io/gitea/models/unit" |
|
"code.gitea.io/gitea/modules/base" |
|
"code.gitea.io/gitea/modules/context" |
|
"code.gitea.io/gitea/modules/json" |
|
"code.gitea.io/gitea/modules/markup" |
|
"code.gitea.io/gitea/modules/markup/markdown" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/util" |
|
"code.gitea.io/gitea/modules/web" |
|
"code.gitea.io/gitea/services/forms" |
|
) |
|
|
|
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 unit.TypeProjects.UnitGlobalDisabled() { |
|
ctx.NotFound("EnableKanbanBoard", nil) |
|
return |
|
} |
|
|
|
if ctx.Repo.Repository != nil { |
|
if !ctx.Repo.CanRead(unit.TypeProjects) { |
|
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.FormTrim("sort") |
|
|
|
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" |
|
repo := ctx.Repo.Repository |
|
page := ctx.FormInt("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, err = markdown.RenderString(&markup.RenderContext{ |
|
URLPrefix: ctx.Repo.RepoLink, |
|
Metas: ctx.Repo.Repository.ComposeMetas(), |
|
GitRepo: ctx.Repo.GitRepo, |
|
Ctx: ctx, |
|
}, projects[i].Description) |
|
if err != nil { |
|
ctx.ServerError("RenderString", err) |
|
return |
|
} |
|
} |
|
|
|
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(unit.TypeProjects) |
|
ctx.Data["IsShowClosed"] = isShowClosed |
|
ctx.Data["IsProjectsPage"] = true |
|
ctx.Data["SortType"] = sortType |
|
|
|
ctx.HTML(http.StatusOK, 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(unit.TypeProjects) |
|
ctx.HTML(http.StatusOK, tplProjectsNew) |
|
} |
|
|
|
// NewProjectPost creates a new project |
|
func NewProjectPost(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.CreateProjectForm) |
|
ctx.Data["Title"] = ctx.Tr("repo.projects.new") |
|
|
|
if ctx.HasError() { |
|
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) |
|
ctx.Data["ProjectTypes"] = models.GetProjectsConfig() |
|
ctx.HTML(http.StatusOK, 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=" + url.QueryEscape(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(http.StatusOK, 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["PageIsEditProjects"] = true |
|
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) |
|
|
|
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(http.StatusOK, tplProjectsNew) |
|
} |
|
|
|
// EditProjectPost response for editing a project |
|
func EditProjectPost(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.CreateProjectForm) |
|
ctx.Data["Title"] = ctx.Tr("repo.projects.edit") |
|
ctx.Data["PageIsEditProjects"] = true |
|
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) |
|
|
|
if ctx.HasError() { |
|
ctx.HTML(http.StatusOK, 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 |
|
} |
|
|
|
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, err = markdown.RenderString(&markup.RenderContext{ |
|
URLPrefix: ctx.Repo.RepoLink, |
|
Metas: ctx.Repo.Repository.ComposeMetas(), |
|
GitRepo: ctx.Repo.GitRepo, |
|
Ctx: ctx, |
|
}, project.Description) |
|
if err != nil { |
|
ctx.ServerError("RenderString", err) |
|
return |
|
} |
|
|
|
ctx.Data["IsProjectsPage"] = true |
|
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) |
|
ctx.Data["Project"] = project |
|
ctx.Data["Boards"] = boards |
|
|
|
ctx.HTML(http.StatusOK, tplProjectsView) |
|
} |
|
|
|
// UpdateIssueProject change an issue's project |
|
func UpdateIssueProject(ctx *context.Context) { |
|
issues := getActionIssues(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
projectID := ctx.FormInt64("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(http.StatusOK, 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(http.StatusForbidden, 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(perm.AccessModeWrite, unit.TypeProjects) { |
|
ctx.JSON(http.StatusForbidden, 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(http.StatusUnprocessableEntity, 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(http.StatusUnprocessableEntity, 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(http.StatusOK, map[string]interface{}{ |
|
"ok": true, |
|
}) |
|
} |
|
|
|
// AddBoardToProjectPost allows a new board to be added to a project. |
|
func AddBoardToProjectPost(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.EditProjectBoardForm) |
|
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { |
|
ctx.JSON(http.StatusForbidden, 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, |
|
Color: form.Color, |
|
CreatorID: ctx.User.ID, |
|
}); err != nil { |
|
ctx.ServerError("NewProjectBoard", err) |
|
return |
|
} |
|
|
|
ctx.JSON(http.StatusOK, map[string]interface{}{ |
|
"ok": true, |
|
}) |
|
} |
|
|
|
func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) { |
|
if ctx.User == nil { |
|
ctx.JSON(http.StatusForbidden, 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(perm.AccessModeWrite, unit.TypeProjects) { |
|
ctx.JSON(http.StatusForbidden, 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(http.StatusUnprocessableEntity, 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(http.StatusUnprocessableEntity, 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 |
|
} |
|
|
|
// EditProjectBoard allows a project board's to be updated |
|
func EditProjectBoard(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.EditProjectBoardForm) |
|
_, board := checkProjectBoardChangePermissions(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
if form.Title != "" { |
|
board.Title = form.Title |
|
} |
|
|
|
board.Color = form.Color |
|
|
|
if form.Sorting != 0 { |
|
board.Sorting = form.Sorting |
|
} |
|
|
|
if err := models.UpdateProjectBoard(board); err != nil { |
|
ctx.ServerError("UpdateProjectBoard", err) |
|
return |
|
} |
|
|
|
ctx.JSON(http.StatusOK, 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(http.StatusOK, map[string]interface{}{ |
|
"ok": true, |
|
}) |
|
} |
|
|
|
// MoveIssues moves or keeps issues in a column and sorts them inside that column |
|
func MoveIssues(ctx *context.Context) { |
|
if ctx.User == nil { |
|
ctx.JSON(http.StatusForbidden, 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(perm.AccessModeWrite, unit.TypeProjects) { |
|
ctx.JSON(http.StatusForbidden, 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("ProjectNotExist", nil) |
|
} else { |
|
ctx.ServerError("GetProjectByID", err) |
|
} |
|
return |
|
} |
|
if project.RepoID != ctx.Repo.Repository.ID { |
|
ctx.NotFound("InvalidRepoID", nil) |
|
return |
|
} |
|
|
|
var board *models.ProjectBoard |
|
|
|
if ctx.ParamsInt64(":boardID") == 0 { |
|
board = &models.ProjectBoard{ |
|
ID: 0, |
|
ProjectID: project.ID, |
|
Title: ctx.Tr("repo.projects.type.uncategorized"), |
|
} |
|
} else { |
|
// column |
|
board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID")) |
|
if err != nil { |
|
if models.IsErrProjectBoardNotExist(err) { |
|
ctx.NotFound("ProjectBoardNotExist", nil) |
|
} else { |
|
ctx.ServerError("GetProjectBoard", err) |
|
} |
|
return |
|
} |
|
if board.ProjectID != project.ID { |
|
ctx.NotFound("BoardNotInProject", nil) |
|
return |
|
} |
|
} |
|
|
|
type movedIssuesForm struct { |
|
Issues []struct { |
|
IssueID int64 `json:"issueID"` |
|
Sorting int64 `json:"sorting"` |
|
} `json:"issues"` |
|
} |
|
|
|
form := &movedIssuesForm{} |
|
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { |
|
ctx.ServerError("DecodeMovedIssuesForm", err) |
|
} |
|
|
|
issueIDs := make([]int64, 0, len(form.Issues)) |
|
sortedIssueIDs := make(map[int64]int64) |
|
for _, issue := range form.Issues { |
|
issueIDs = append(issueIDs, issue.IssueID) |
|
sortedIssueIDs[issue.Sorting] = issue.IssueID |
|
} |
|
movedIssues, err := models.GetIssuesByIDs(issueIDs) |
|
if err != nil { |
|
if models.IsErrIssueNotExist(err) { |
|
ctx.NotFound("IssueNotExisting", nil) |
|
} else { |
|
ctx.ServerError("GetIssueByID", err) |
|
} |
|
return |
|
} |
|
|
|
if len(movedIssues) != len(form.Issues) { |
|
ctx.ServerError("IssuesNotFound", err) |
|
return |
|
} |
|
|
|
if err = models.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil { |
|
ctx.ServerError("MoveIssuesOnProjectBoard", err) |
|
return |
|
} |
|
|
|
ctx.JSON(http.StatusOK, 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(unit.TypeProjects) |
|
|
|
ctx.HTML(http.StatusOK, tplGenericProjectsNew) |
|
}
|
|
|