mirror of https://github.com/go-gitea/gitea.git
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.
675 lines
19 KiB
675 lines
19 KiB
// Copyright 2022 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package org |
|
|
|
import ( |
|
"errors" |
|
"fmt" |
|
"net/http" |
|
"strings" |
|
|
|
"code.gitea.io/gitea/models/db" |
|
issues_model "code.gitea.io/gitea/models/issues" |
|
org_model "code.gitea.io/gitea/models/organization" |
|
project_model "code.gitea.io/gitea/models/project" |
|
attachment_model "code.gitea.io/gitea/models/repo" |
|
"code.gitea.io/gitea/models/unit" |
|
"code.gitea.io/gitea/modules/json" |
|
"code.gitea.io/gitea/modules/optional" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/templates" |
|
"code.gitea.io/gitea/modules/web" |
|
"code.gitea.io/gitea/routers/web/shared/issue" |
|
shared_user "code.gitea.io/gitea/routers/web/shared/user" |
|
"code.gitea.io/gitea/services/context" |
|
"code.gitea.io/gitea/services/forms" |
|
project_service "code.gitea.io/gitea/services/projects" |
|
) |
|
|
|
const ( |
|
tplProjects templates.TplName = "org/projects/list" |
|
tplProjectsNew templates.TplName = "org/projects/new" |
|
tplProjectsView templates.TplName = "org/projects/view" |
|
) |
|
|
|
// MustEnableProjects check if projects are enabled in settings |
|
func MustEnableProjects(ctx *context.Context) { |
|
if unit.TypeProjects.UnitGlobalDisabled() { |
|
ctx.NotFound(nil) |
|
return |
|
} |
|
} |
|
|
|
// Projects renders the home page of projects |
|
func Projects(ctx *context.Context) { |
|
shared_user.PrepareContextForProfileBigAvatar(ctx) |
|
ctx.Data["Title"] = ctx.Tr("repo.projects") |
|
|
|
sortType := ctx.FormTrim("sort") |
|
|
|
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" |
|
keyword := ctx.FormTrim("q") |
|
page := ctx.FormInt("page") |
|
if page <= 1 { |
|
page = 1 |
|
} |
|
|
|
var projectType project_model.Type |
|
if ctx.ContextUser.IsOrganization() { |
|
projectType = project_model.TypeOrganization |
|
} else { |
|
projectType = project_model.TypeIndividual |
|
} |
|
projects, total, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ |
|
ListOptions: db.ListOptions{ |
|
Page: page, |
|
PageSize: setting.UI.IssuePagingNum, |
|
}, |
|
OwnerID: ctx.ContextUser.ID, |
|
IsClosed: optional.Some(isShowClosed), |
|
OrderBy: project_model.GetSearchOrderByBySortType(sortType), |
|
Type: projectType, |
|
Title: keyword, |
|
}) |
|
if err != nil { |
|
ctx.ServerError("FindProjects", err) |
|
return |
|
} |
|
|
|
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil { |
|
ctx.ServerError("LoadIssueNumbersForProjects", err) |
|
return |
|
} |
|
|
|
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{ |
|
OwnerID: ctx.ContextUser.ID, |
|
IsClosed: optional.Some(!isShowClosed), |
|
Type: projectType, |
|
}) |
|
if err != nil { |
|
ctx.ServerError("CountProjects", err) |
|
return |
|
} |
|
|
|
if isShowClosed { |
|
ctx.Data["OpenCount"] = opTotal |
|
ctx.Data["ClosedCount"] = total |
|
} else { |
|
ctx.Data["OpenCount"] = total |
|
ctx.Data["ClosedCount"] = opTotal |
|
} |
|
|
|
ctx.Data["Projects"] = projects |
|
shared_user.RenderUserHeader(ctx) |
|
|
|
if isShowClosed { |
|
ctx.Data["State"] = "closed" |
|
} else { |
|
ctx.Data["State"] = "open" |
|
} |
|
renderUtils := templates.NewRenderUtils(ctx) |
|
for _, project := range projects { |
|
project.RenderedContent = renderUtils.MarkdownToHtml(project.Description) |
|
} |
|
|
|
err = shared_user.LoadHeaderCount(ctx) |
|
if err != nil { |
|
ctx.ServerError("LoadHeaderCount", err) |
|
return |
|
} |
|
|
|
numPages := 0 |
|
if total > 0 { |
|
numPages = (int(total) - 1/setting.UI.IssuePagingNum) |
|
} |
|
|
|
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) |
|
pager.AddParamFromRequest(ctx.Req) |
|
ctx.Data["Page"] = pager |
|
|
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) |
|
ctx.Data["IsShowClosed"] = isShowClosed |
|
ctx.Data["PageIsViewProjects"] = true |
|
ctx.Data["SortType"] = sortType |
|
|
|
ctx.HTML(http.StatusOK, tplProjects) |
|
} |
|
|
|
func canWriteProjects(ctx *context.Context) bool { |
|
if ctx.ContextUser.IsOrganization() { |
|
return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) |
|
} |
|
return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID |
|
} |
|
|
|
// RenderNewProject render creating a project page |
|
func RenderNewProject(ctx *context.Context) { |
|
ctx.Data["Title"] = ctx.Tr("repo.projects.new") |
|
ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs() |
|
ctx.Data["CardTypes"] = project_model.GetCardConfig() |
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) |
|
ctx.Data["PageIsViewProjects"] = true |
|
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() |
|
ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects" |
|
shared_user.RenderUserHeader(ctx) |
|
|
|
err := shared_user.LoadHeaderCount(ctx) |
|
if err != nil { |
|
ctx.ServerError("LoadHeaderCount", err) |
|
return |
|
} |
|
|
|
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") |
|
shared_user.RenderUserHeader(ctx) |
|
|
|
if ctx.HasError() { |
|
RenderNewProject(ctx) |
|
return |
|
} |
|
|
|
newProject := project_model.Project{ |
|
OwnerID: ctx.ContextUser.ID, |
|
Title: form.Title, |
|
Description: form.Content, |
|
CreatorID: ctx.Doer.ID, |
|
TemplateType: form.TemplateType, |
|
CardType: form.CardType, |
|
} |
|
|
|
if ctx.ContextUser.IsOrganization() { |
|
newProject.Type = project_model.TypeOrganization |
|
} else { |
|
newProject.Type = project_model.TypeIndividual |
|
} |
|
|
|
if err := project_model.NewProject(ctx, &newProject); err != nil { |
|
ctx.ServerError("NewProject", err) |
|
return |
|
} |
|
|
|
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) |
|
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects") |
|
} |
|
|
|
// ChangeProjectStatus updates the status of a project between "open" and "close" |
|
func ChangeProjectStatus(ctx *context.Context) { |
|
var toClose bool |
|
switch ctx.PathParam("action") { |
|
case "open": |
|
toClose = false |
|
case "close": |
|
toClose = true |
|
default: |
|
ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects") |
|
return |
|
} |
|
id := ctx.PathParamInt64("id") |
|
|
|
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { |
|
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id)) |
|
} |
|
|
|
// DeleteProject delete a project |
|
func DeleteProject(ctx *context.Context) { |
|
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
if p.OwnerID != ctx.ContextUser.ID { |
|
ctx.NotFound(nil) |
|
return |
|
} |
|
|
|
if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil { |
|
ctx.Flash.Error("DeleteProjectByID: " + err.Error()) |
|
} else { |
|
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success")) |
|
} |
|
|
|
ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects") |
|
} |
|
|
|
// RenderEditProject allows a project to be edited |
|
func RenderEditProject(ctx *context.Context) { |
|
ctx.Data["Title"] = ctx.Tr("repo.projects.edit") |
|
ctx.Data["PageIsEditProjects"] = true |
|
ctx.Data["PageIsViewProjects"] = true |
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) |
|
ctx.Data["CardTypes"] = project_model.GetCardConfig() |
|
|
|
shared_user.RenderUserHeader(ctx) |
|
|
|
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
if p.OwnerID != ctx.ContextUser.ID { |
|
ctx.NotFound(nil) |
|
return |
|
} |
|
|
|
ctx.Data["projectID"] = p.ID |
|
ctx.Data["title"] = p.Title |
|
ctx.Data["content"] = p.Description |
|
ctx.Data["redirect"] = ctx.FormString("redirect") |
|
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() |
|
ctx.Data["card_type"] = p.CardType |
|
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID) |
|
|
|
ctx.HTML(http.StatusOK, tplProjectsNew) |
|
} |
|
|
|
// EditProjectPost response for editing a project |
|
func EditProjectPost(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.CreateProjectForm) |
|
projectID := ctx.PathParamInt64("id") |
|
ctx.Data["Title"] = ctx.Tr("repo.projects.edit") |
|
ctx.Data["PageIsEditProjects"] = true |
|
ctx.Data["PageIsViewProjects"] = true |
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) |
|
ctx.Data["CardTypes"] = project_model.GetCardConfig() |
|
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID) |
|
|
|
shared_user.RenderUserHeader(ctx) |
|
|
|
err := shared_user.LoadHeaderCount(ctx) |
|
if err != nil { |
|
ctx.ServerError("LoadHeaderCount", err) |
|
return |
|
} |
|
|
|
if ctx.HasError() { |
|
ctx.HTML(http.StatusOK, tplProjectsNew) |
|
return |
|
} |
|
|
|
p, err := project_model.GetProjectByID(ctx, projectID) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
if p.OwnerID != ctx.ContextUser.ID { |
|
ctx.NotFound(nil) |
|
return |
|
} |
|
|
|
p.Title = form.Title |
|
p.Description = form.Content |
|
p.CardType = form.CardType |
|
if err = project_model.UpdateProject(ctx, p); err != nil { |
|
ctx.ServerError("UpdateProjects", err) |
|
return |
|
} |
|
|
|
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title)) |
|
if ctx.FormString("redirect") == "project" { |
|
ctx.Redirect(p.Link(ctx)) |
|
} else { |
|
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects") |
|
} |
|
} |
|
|
|
// ViewProject renders the project with board view for a project |
|
func ViewProject(ctx *context.Context) { |
|
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
if project.OwnerID != ctx.ContextUser.ID { |
|
ctx.NotFound(nil) |
|
return |
|
} |
|
if err := project.LoadOwner(ctx); err != nil { |
|
ctx.ServerError("LoadOwner", err) |
|
return |
|
} |
|
|
|
columns, err := project.GetColumns(ctx) |
|
if err != nil { |
|
ctx.ServerError("GetProjectColumns", err) |
|
return |
|
} |
|
|
|
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner) |
|
if ctx.Written() { |
|
return |
|
} |
|
assigneeID := ctx.FormString("assignee") |
|
|
|
opts := issues_model.IssuesOptions{ |
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs, |
|
AssigneeID: assigneeID, |
|
Owner: project.Owner, |
|
Doer: ctx.Doer, |
|
} |
|
|
|
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts) |
|
if err != nil { |
|
ctx.ServerError("LoadIssuesOfColumns", err) |
|
return |
|
} |
|
for _, column := range columns { |
|
column.NumIssues = int64(len(issuesMap[column.ID])) |
|
} |
|
|
|
if project.CardType != project_model.CardTypeTextOnly { |
|
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) |
|
for _, issuesList := range issuesMap { |
|
for _, issue := range issuesList { |
|
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { |
|
issuesAttachmentMap[issue.ID] = issueAttachment |
|
} |
|
} |
|
} |
|
ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap |
|
} |
|
|
|
linkedPrsMap := make(map[int64][]*issues_model.Issue) |
|
for _, issuesList := range issuesMap { |
|
for _, issue := range issuesList { |
|
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 := issues_model.Issues(ctx, &issues_model.IssuesOptions{ |
|
IssueIDs: referencedIDs, |
|
IsPull: optional.Some(true), |
|
}); err == nil { |
|
linkedPrsMap[issue.ID] = linkedPrs |
|
} |
|
} |
|
} |
|
} |
|
|
|
// TODO: Add option to filter also by repository specific labels |
|
labels, err := issues_model.GetLabelsByOrgID(ctx, project.OwnerID, "", db.ListOptions{}) |
|
if err != nil { |
|
ctx.ServerError("GetLabelsByOrgID", err) |
|
return |
|
} |
|
|
|
// Get the exclusive scope for every label ID |
|
labelExclusiveScopes := make([]string, 0, len(preparedLabelFilter.SelectedLabelIDs)) |
|
for _, labelID := range preparedLabelFilter.SelectedLabelIDs { |
|
foundExclusiveScope := false |
|
for _, label := range labels { |
|
if label.ID == labelID || label.ID == -labelID { |
|
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) |
|
foundExclusiveScope = true |
|
break |
|
} |
|
} |
|
if !foundExclusiveScope { |
|
labelExclusiveScopes = append(labelExclusiveScopes, "") |
|
} |
|
} |
|
|
|
for _, l := range labels { |
|
l.LoadSelectedLabelsAfterClick(preparedLabelFilter.SelectedLabelIDs, labelExclusiveScopes) |
|
} |
|
ctx.Data["Labels"] = labels |
|
ctx.Data["NumLabels"] = len(labels) |
|
|
|
// Get assignees. |
|
assigneeUsers, err := org_model.GetOrgAssignees(ctx, project.OwnerID) |
|
if err != nil { |
|
ctx.ServerError("GetRepoAssignees", err) |
|
return |
|
} |
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) |
|
ctx.Data["AssigneeID"] = assigneeID |
|
|
|
project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description) |
|
ctx.Data["LinkedPRs"] = linkedPrsMap |
|
ctx.Data["PageIsViewProjects"] = true |
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) |
|
ctx.Data["Project"] = project |
|
ctx.Data["IssuesMap"] = issuesMap |
|
ctx.Data["Columns"] = columns |
|
shared_user.RenderUserHeader(ctx) |
|
|
|
err = shared_user.LoadHeaderCount(ctx) |
|
if err != nil { |
|
ctx.ServerError("LoadHeaderCount", err) |
|
return |
|
} |
|
|
|
ctx.HTML(http.StatusOK, tplProjectsView) |
|
} |
|
|
|
// DeleteProjectColumn allows for the deletion of a project column |
|
func DeleteProjectColumn(ctx *context.Context) { |
|
if ctx.Doer == nil { |
|
ctx.JSON(http.StatusForbidden, map[string]string{ |
|
"message": "Only signed in users are allowed to perform this action.", |
|
}) |
|
return |
|
} |
|
|
|
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
|
|
pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) |
|
if err != nil { |
|
ctx.ServerError("GetProjectColumn", err) |
|
return |
|
} |
|
if pb.ProjectID != ctx.PathParamInt64("id") { |
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ |
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), |
|
}) |
|
return |
|
} |
|
|
|
if project.OwnerID != ctx.ContextUser.ID { |
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ |
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID), |
|
}) |
|
return |
|
} |
|
|
|
if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64("columnID")); err != nil { |
|
ctx.ServerError("DeleteProjectColumnByID", err) |
|
return |
|
} |
|
|
|
ctx.JSONOK() |
|
} |
|
|
|
// AddColumnToProjectPost allows a new column to be added to a project. |
|
func AddColumnToProjectPost(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.EditProjectColumnForm) |
|
|
|
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
|
|
if err := project_model.NewColumn(ctx, &project_model.Column{ |
|
ProjectID: project.ID, |
|
Title: form.Title, |
|
Color: form.Color, |
|
CreatorID: ctx.Doer.ID, |
|
}); err != nil { |
|
ctx.ServerError("NewProjectColumn", err) |
|
return |
|
} |
|
|
|
ctx.JSONOK() |
|
} |
|
|
|
// CheckProjectColumnChangePermissions check permission |
|
func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) { |
|
if ctx.Doer == nil { |
|
ctx.JSON(http.StatusForbidden, map[string]string{ |
|
"message": "Only signed in users are allowed to perform this action.", |
|
}) |
|
return nil, nil |
|
} |
|
|
|
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return nil, nil |
|
} |
|
|
|
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) |
|
if err != nil { |
|
ctx.ServerError("GetProjectColumn", err) |
|
return nil, nil |
|
} |
|
if column.ProjectID != ctx.PathParamInt64("id") { |
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ |
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), |
|
}) |
|
return nil, nil |
|
} |
|
|
|
if project.OwnerID != ctx.ContextUser.ID { |
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ |
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID), |
|
}) |
|
return nil, nil |
|
} |
|
return project, column |
|
} |
|
|
|
// EditProjectColumn allows a project column's to be updated |
|
func EditProjectColumn(ctx *context.Context) { |
|
form := web.GetForm(ctx).(*forms.EditProjectColumnForm) |
|
_, column := CheckProjectColumnChangePermissions(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
if form.Title != "" { |
|
column.Title = form.Title |
|
} |
|
column.Color = form.Color |
|
if form.Sorting != 0 { |
|
column.Sorting = form.Sorting |
|
} |
|
|
|
if err := project_model.UpdateColumn(ctx, column); err != nil { |
|
ctx.ServerError("UpdateProjectColumn", err) |
|
return |
|
} |
|
|
|
ctx.JSONOK() |
|
} |
|
|
|
// SetDefaultProjectColumn set default column for uncategorized issues/pulls |
|
func SetDefaultProjectColumn(ctx *context.Context) { |
|
project, column := CheckProjectColumnChangePermissions(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil { |
|
ctx.ServerError("SetDefaultColumn", err) |
|
return |
|
} |
|
|
|
ctx.JSONOK() |
|
} |
|
|
|
// MoveIssues moves or keeps issues in a column and sorts them inside that column |
|
func MoveIssues(ctx *context.Context) { |
|
if ctx.Doer == nil { |
|
ctx.JSON(http.StatusForbidden, map[string]string{ |
|
"message": "Only signed in users are allowed to perform this action.", |
|
}) |
|
return |
|
} |
|
|
|
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) |
|
return |
|
} |
|
if project.OwnerID != ctx.ContextUser.ID { |
|
ctx.NotFound(nil) |
|
return |
|
} |
|
|
|
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) |
|
return |
|
} |
|
|
|
if column.ProjectID != project.ID { |
|
ctx.NotFound(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) |
|
return |
|
} |
|
|
|
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 := issues_model.GetIssuesByIDs(ctx, issueIDs) |
|
if err != nil { |
|
ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err) |
|
return |
|
} |
|
|
|
if len(movedIssues) != len(form.Issues) { |
|
ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) |
|
return |
|
} |
|
|
|
if _, err = movedIssues.LoadRepositories(ctx); err != nil { |
|
ctx.ServerError("LoadRepositories", err) |
|
return |
|
} |
|
|
|
for _, issue := range movedIssues { |
|
if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID { |
|
ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID")) |
|
return |
|
} |
|
} |
|
|
|
if err = project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, sortedIssueIDs); err != nil { |
|
ctx.ServerError("MoveIssuesOnProjectColumn", err) |
|
return |
|
} |
|
|
|
ctx.JSONOK() |
|
}
|
|
|