Kanban board (#8346)
Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: jaqra <48099350+jaqra@users.noreply.github.com> Co-authored-by: Kerry <flatline-studios@users.noreply.github.com> Co-authored-by: Jaqra <jaqra@hotmail.com> Co-authored-by: Kyle Evans <kevans91@users.noreply.github.com> Co-authored-by: Tsakiridis Ilias <TsakiDev@users.noreply.github.com> Co-authored-by: Ilias Tsakiridis <ilias.tsakiridis@outlook.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
d285b5d35a
commit
4027c5dd7c
75 changed files with 3569 additions and 58 deletions
|
@ -719,6 +719,17 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
|||
}
|
||||
}
|
||||
|
||||
if opts.HasProjects != nil && !models.UnitTypeProjects.UnitGlobalDisabled() {
|
||||
if *opts.HasProjects {
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: models.UnitTypeProjects,
|
||||
})
|
||||
} else {
|
||||
deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
|
||||
}
|
||||
}
|
||||
|
||||
if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
|
||||
return err
|
||||
|
|
|
@ -104,7 +104,7 @@ func MustAllowPulls(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalBool) {
|
||||
func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
|
||||
var err error
|
||||
viewType := ctx.Query("type")
|
||||
sortType := ctx.Query("sort")
|
||||
|
@ -215,6 +215,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
|
|||
PosterID: posterID,
|
||||
MentionedID: mentionedID,
|
||||
MilestoneIDs: mileIDs,
|
||||
ProjectID: projectID,
|
||||
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||
IsPull: isPullOption,
|
||||
LabelIDs: labelIDs,
|
||||
|
@ -357,7 +358,7 @@ func Issues(ctx *context.Context) {
|
|||
ctx.Data["PageIsIssueList"] = true
|
||||
}
|
||||
|
||||
issues(ctx, ctx.QueryInt64("milestone"), util.OptionalBoolOf(isPullList))
|
||||
issues(ctx, ctx.QueryInt64("milestone"), ctx.QueryInt64("project"), util.OptionalBoolOf(isPullList))
|
||||
|
||||
var err error
|
||||
// Get milestones
|
||||
|
@ -402,6 +403,33 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repos
|
|||
}
|
||||
}
|
||||
|
||||
func retrieveProjects(ctx *context.Context, repo *models.Repository) {
|
||||
|
||||
var err error
|
||||
|
||||
ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
|
||||
RepoID: repo.ID,
|
||||
Page: -1,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
Type: models.ProjectTypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
|
||||
RepoID: repo.ID,
|
||||
Page: -1,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
Type: models.ProjectTypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RetrieveRepoReviewers find all reviewers of a repository
|
||||
func RetrieveRepoReviewers(ctx *context.Context, repo *models.Repository, issuePosterID int64) {
|
||||
var err error
|
||||
|
@ -439,6 +467,11 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull boo
|
|||
return nil
|
||||
}
|
||||
|
||||
retrieveProjects(ctx, repo)
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
brs, err := ctx.Repo.GitRepo.GetBranches()
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBranches", err)
|
||||
|
@ -502,6 +535,7 @@ func NewIssue(ctx *context.Context) {
|
|||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||
body := ctx.Query("body")
|
||||
ctx.Data["BodyQuery"] = body
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
|
||||
|
||||
milestoneID := ctx.QueryInt64("milestone")
|
||||
if milestoneID > 0 {
|
||||
|
@ -514,6 +548,20 @@ func NewIssue(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
projectID := ctx.QueryInt64("project")
|
||||
if projectID > 0 {
|
||||
project, err := models.GetProjectByID(projectID)
|
||||
if err != nil {
|
||||
log.Error("GetProjectByID: %d: %v", projectID, err)
|
||||
} else if project.RepoID != ctx.Repo.Repository.ID {
|
||||
log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
|
||||
} else {
|
||||
ctx.Data["project_id"] = projectID
|
||||
ctx.Data["Project"] = project
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
|
||||
renderAttachmentSettings(ctx)
|
||||
|
||||
|
@ -528,7 +576,7 @@ func NewIssue(ctx *context.Context) {
|
|||
}
|
||||
|
||||
// ValidateRepoMetas check and returns repository's meta informations
|
||||
func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull bool) ([]int64, []int64, int64) {
|
||||
func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
|
||||
var (
|
||||
repo = ctx.Repo.Repository
|
||||
err error
|
||||
|
@ -536,7 +584,7 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull b
|
|||
|
||||
labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
|
||||
if ctx.Written() {
|
||||
return nil, nil, 0
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
|
||||
var labelIDs []int64
|
||||
|
@ -545,7 +593,7 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull b
|
|||
if len(form.LabelIDs) > 0 {
|
||||
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
|
||||
if err != nil {
|
||||
return nil, nil, 0
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
labelIDMark := base.Int64sToMap(labelIDs)
|
||||
|
||||
|
@ -567,17 +615,32 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull b
|
|||
ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMilestoneByID", err)
|
||||
return nil, nil, 0
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
ctx.Data["milestone_id"] = milestoneID
|
||||
}
|
||||
|
||||
if form.ProjectID > 0 {
|
||||
p, err := models.GetProjectByID(form.ProjectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
if p.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("", nil)
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
|
||||
ctx.Data["Project"] = p
|
||||
ctx.Data["project_id"] = form.ProjectID
|
||||
}
|
||||
|
||||
// Check assignees
|
||||
var assigneeIDs []int64
|
||||
if len(form.AssigneeIDs) > 0 {
|
||||
assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
|
||||
if err != nil {
|
||||
return nil, nil, 0
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
|
||||
// Check if the passed assignees actually exists and is assignable
|
||||
|
@ -585,17 +648,18 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull b
|
|||
assignee, err := models.GetUserByID(aID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return nil, nil, 0
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
|
||||
valid, err := models.CanBeAssigned(assignee, repo, isPull)
|
||||
if err != nil {
|
||||
ctx.ServerError("canBeAssigned", err)
|
||||
return nil, nil, 0
|
||||
ctx.ServerError("CanBeAssigned", err)
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
|
||||
if !valid {
|
||||
ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
|
||||
return nil, nil, 0
|
||||
return nil, nil, 0, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -605,7 +669,7 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull b
|
|||
assigneeIDs = append(assigneeIDs, form.AssigneeID)
|
||||
}
|
||||
|
||||
return labelIDs, assigneeIDs, milestoneID
|
||||
return labelIDs, assigneeIDs, milestoneID, form.ProjectID
|
||||
}
|
||||
|
||||
// NewIssuePost response for creating new issue
|
||||
|
@ -623,7 +687,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
|
|||
attachments []string
|
||||
)
|
||||
|
||||
labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form, false)
|
||||
labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, form, false)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
@ -661,6 +725,13 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
|
|||
return
|
||||
}
|
||||
|
||||
if projectID > 0 {
|
||||
if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
|
||||
ctx.ServerError("ChangeProjectAssign", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
|
||||
}
|
||||
|
@ -758,6 +829,8 @@ func ViewIssue(ctx *context.Context) {
|
|||
ctx.Data["RequireHighlightJS"] = true
|
||||
ctx.Data["RequireTribute"] = true
|
||||
ctx.Data["RequireSimpleMDE"] = true
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
|
||||
|
||||
renderAttachmentSettings(ctx)
|
||||
|
||||
if err = issue.LoadAttributes(); err != nil {
|
||||
|
@ -839,6 +912,8 @@ func ViewIssue(ctx *context.Context) {
|
|||
// Check milestone and assignee.
|
||||
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
RetrieveRepoMilestonesAndAssignees(ctx, repo)
|
||||
retrieveProjects(ctx, repo)
|
||||
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
@ -977,6 +1052,26 @@ func ViewIssue(ctx *context.Context) {
|
|||
if comment.MilestoneID > 0 && comment.Milestone == nil {
|
||||
comment.Milestone = ghostMilestone
|
||||
}
|
||||
} else if comment.Type == models.CommentTypeProject {
|
||||
|
||||
if err = comment.LoadProject(); err != nil {
|
||||
ctx.ServerError("LoadProject", err)
|
||||
return
|
||||
}
|
||||
|
||||
ghostProject := &models.Project{
|
||||
ID: -1,
|
||||
Title: ctx.Tr("repo.issues.deleted_project"),
|
||||
}
|
||||
|
||||
if comment.OldProjectID > 0 && comment.OldProject == nil {
|
||||
comment.OldProject = ghostProject
|
||||
}
|
||||
|
||||
if comment.ProjectID > 0 && comment.Project == nil {
|
||||
comment.Project = ghostProject
|
||||
}
|
||||
|
||||
} else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
|
||||
if err = comment.LoadAssigneeUser(); err != nil {
|
||||
ctx.ServerError("LoadAssigneeUser", err)
|
||||
|
@ -1149,6 +1244,7 @@ func ViewIssue(ctx *context.Context) {
|
|||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
|
||||
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
|
||||
ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeProjects)
|
||||
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
|
||||
ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
|
||||
ctx.Data["RefEndName"] = git.RefEndName(issue.Ref)
|
||||
|
|
|
@ -207,39 +207,28 @@ func EditMilestonePost(ctx *context.Context, form auth.CreateMilestoneForm) {
|
|||
ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
|
||||
}
|
||||
|
||||
// ChangeMilestonStatus response for change a milestone's status
|
||||
func ChangeMilestonStatus(ctx *context.Context) {
|
||||
m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if models.IsErrMilestoneNotExist(err) {
|
||||
ctx.NotFound("", err)
|
||||
} else {
|
||||
ctx.ServerError("GetMilestoneByRepoID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeMilestoneStatus response for change a milestone's status
|
||||
func ChangeMilestoneStatus(ctx *context.Context) {
|
||||
toClose := false
|
||||
switch ctx.Params(":action") {
|
||||
case "open":
|
||||
if m.IsClosed {
|
||||
if err = models.ChangeMilestoneStatus(m, false); err != nil {
|
||||
ctx.ServerError("ChangeMilestoneStatus", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
|
||||
toClose = false
|
||||
case "close":
|
||||
if !m.IsClosed {
|
||||
m.ClosedDateUnix = timeutil.TimeStampNow()
|
||||
if err = models.ChangeMilestoneStatus(m, true); err != nil {
|
||||
ctx.ServerError("ChangeMilestoneStatus", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
|
||||
toClose = true
|
||||
default:
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
|
||||
}
|
||||
id := ctx.ParamsInt64(":id")
|
||||
|
||||
if err := models.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
|
||||
if models.IsErrMilestoneNotExist(err) {
|
||||
ctx.NotFound("", err)
|
||||
} else {
|
||||
ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + ctx.Params(":action"))
|
||||
}
|
||||
|
||||
// DeleteMilestone delete a milestone
|
||||
|
@ -274,7 +263,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
|
|||
ctx.Data["Title"] = milestone.Name
|
||||
ctx.Data["Milestone"] = milestone
|
||||
|
||||
issues(ctx, milestoneID, util.OptionalBoolNone)
|
||||
issues(ctx, milestoneID, 0, util.OptionalBoolNone)
|
||||
|
||||
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
|
||||
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
|
||||
|
|
591
routers/repo/projects.go
Normal file
591
routers/repo/projects.go
Normal file
|
@ -0,0 +1,591 @@
|
|||
// 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["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.HTML(200, tplProjectsNew)
|
||||
}
|
||||
|
||||
// NewRepoProjectPost creates a new project
|
||||
func NewRepoProjectPost(ctx *context.Context, form auth.CreateProjectForm) {
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||
|
||||
if ctx.HasError() {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(200, tplMilestoneNew)
|
||||
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
|
||||
}
|
||||
|
||||
uncategorizedBoard, err := models.GetUncategorizedBoard(project.ID)
|
||||
uncategorizedBoard.Title = ctx.Tr("repo.projects.type.uncategorized")
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUncategorizedBoard", err)
|
||||
return
|
||||
}
|
||||
|
||||
boards, err := models.GetProjectBoards(project.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoards", err)
|
||||
return
|
||||
}
|
||||
|
||||
allBoards := models.ProjectBoardList{uncategorizedBoard}
|
||||
allBoards = append(allBoards, boards...)
|
||||
|
||||
if ctx.Data["Issues"], err = allBoards.LoadIssues(); err != nil {
|
||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Project"] = project
|
||||
ctx.Data["Boards"] = allBoards
|
||||
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.InternalServerError(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,
|
||||
})
|
||||
}
|
||||
|
||||
// EditProjectBoardTitle allows a project board's title to be updated
|
||||
func EditProjectBoardTitle(ctx *context.Context, form auth.EditProjectBoardTitleForm) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// 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.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.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 + "/")
|
||||
}
|
|
@ -906,7 +906,7 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
|
|||
}
|
||||
defer headGitRepo.Close()
|
||||
|
||||
labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form, true)
|
||||
labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, form, true)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -284,6 +284,15 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
|
|||
}
|
||||
}
|
||||
|
||||
if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() {
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: models.UnitTypeProjects,
|
||||
})
|
||||
} else if !models.UnitTypeProjects.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
|
||||
}
|
||||
|
||||
if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
|
||||
units = append(units, models.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
|
|
|
@ -275,6 +275,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled()
|
||||
ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled()
|
||||
ctx.Data["UnitPullsGlobalDisabled"] = models.UnitTypePullRequests.UnitGlobalDisabled()
|
||||
ctx.Data["UnitProjectsGlobalDisabled"] = models.UnitTypeProjects.UnitGlobalDisabled()
|
||||
})
|
||||
|
||||
// FIXME: not all routes need go through same middlewares.
|
||||
|
@ -533,6 +534,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
reqRepoPullsReader := context.RequireRepoReader(models.UnitTypePullRequests)
|
||||
reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
|
||||
reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
|
||||
reqRepoProjectsReader := context.RequireRepoReader(models.UnitTypeProjects)
|
||||
|
||||
// ***** START: Organization *****
|
||||
m.Group("/org", func() {
|
||||
|
@ -750,6 +752,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
|
||||
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
|
||||
m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
|
||||
m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
|
||||
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
|
||||
m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
|
||||
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
|
||||
|
@ -772,7 +775,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
Post(bindIgnErr(auth.CreateMilestoneForm{}), repo.NewMilestonePost)
|
||||
m.Get("/:id/edit", repo.EditMilestone)
|
||||
m.Post("/:id/edit", bindIgnErr(auth.CreateMilestoneForm{}), repo.EditMilestonePost)
|
||||
m.Post("/:id/:action", repo.ChangeMilestonStatus)
|
||||
m.Post("/:id/:action", repo.ChangeMilestoneStatus)
|
||||
m.Post("/delete", repo.DeleteMilestone)
|
||||
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
|
||||
m.Group("/pull", func() {
|
||||
|
@ -853,6 +856,28 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
|
||||
}, context.RepoRef())
|
||||
|
||||
m.Group("/projects", func() {
|
||||
m.Get("", repo.Projects)
|
||||
m.Get("/new", repo.NewProject)
|
||||
m.Post("/new", bindIgnErr(auth.CreateProjectForm{}), repo.NewRepoProjectPost)
|
||||
m.Group("/:id", func() {
|
||||
m.Get("", repo.ViewProject)
|
||||
m.Post("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.AddBoardToProjectPost)
|
||||
m.Post("/delete", repo.DeleteProject)
|
||||
|
||||
m.Get("/edit", repo.EditProject)
|
||||
m.Post("/edit", bindIgnErr(auth.CreateProjectForm{}), repo.EditProjectPost)
|
||||
m.Post("/^:action(open|close)$", repo.ChangeProjectStatus)
|
||||
|
||||
m.Group("/:boardID", func() {
|
||||
m.Put("", bindIgnErr(auth.EditProjectBoardTitleForm{}), repo.EditProjectBoardTitle)
|
||||
m.Delete("", repo.DeleteProjectBoard)
|
||||
|
||||
m.Post("/:index", repo.MoveIssueAcrossBoards)
|
||||
})
|
||||
})
|
||||
}, reqRepoProjectsReader, repo.MustEnableProjects)
|
||||
|
||||
m.Group("/wiki", func() {
|
||||
m.Get("/?:page", repo.Wiki)
|
||||
m.Get("/_pages", repo.WikiPages)
|
||||
|
|
|
@ -101,7 +101,7 @@ func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
|
|||
ctx.Data["Feeds"] = actions
|
||||
}
|
||||
|
||||
// Dashboard render the dashborad page
|
||||
// Dashboard render the dashboard page
|
||||
func Dashboard(ctx *context.Context) {
|
||||
ctxUser := getDashboardContextUser(ctx)
|
||||
if ctx.Written() {
|
||||
|
|
|
@ -216,6 +216,16 @@ func Profile(ctx *context.Context) {
|
|||
}
|
||||
|
||||
total = int(count)
|
||||
case "projects":
|
||||
ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
|
||||
Page: -1,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
Type: models.ProjectTypeIndividual,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjects", err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
|
||||
ListOptions: models.ListOptions{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue