Files
gitea/routers/api/v1/shared/project.go
T
7fae3d5db3 feat(api): add project APIs (#38691)
Adds REST APIs for project boards for repo, org and user scopes, using
as much shared code as possible for all 3 scopes.

Fixes: https://github.com/go-gitea/gitea/issues/14299
Fixes: https://github.com/go-gitea/gitea/issues/31769
Fixes: https://github.com/go-gitea/gitea/issues/35921
Replaces: https://github.com/go-gitea/gitea/pull/37518
Replaces: https://github.com/go-gitea/gitea/pull/36008
Replaces: https://github.com/go-gitea/gitea/pull/28111
Replaces: https://github.com/go-gitea/gitea/pull/31768
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Supen.Huang <supen.huang@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ember <ember@mubergacres.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: beardev-in <abhinav.edulakanti@gmail.com>
2026-08-08 13:53:28 +00:00

2122 lines
55 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package shared
import (
"math"
"net/http"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
project_model "gitea.dev/models/project"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
"gitea.dev/services/convert"
project_service "gitea.dev/services/projects"
)
// projectScope identifies whose projects a route operates on. Exactly one of Repo /
// Owner is set. Access rights are checked at the route level.
type projectScope struct {
Type project_model.Type
Repo *repo_model.Repository
Owner *user_model.User
}
// projectScopeFromContext derives the scope from the route's assignment. Organization
// routes assign the org as the context user, so only the repository case is distinct.
func projectScopeFromContext(ctx *context.APIContext) projectScope {
if ctx.Repo != nil && ctx.Repo.Repository != nil {
return projectScope{Type: project_model.TypeRepository, Repo: ctx.Repo.Repository}
}
owner := util.IfZero(ctx.ContextUser, ctx.Doer) // "/user/projects" acts on the authenticated user
return projectScope{Type: util.Iif(owner.IsOrganization(), project_model.TypeOrganization, project_model.TypeIndividual), Owner: owner}
}
func (s projectScope) repoID() int64 {
if s.Repo != nil {
return s.Repo.ID
}
return 0
}
func (s projectScope) ownerID() int64 {
if s.Owner != nil {
return s.Owner.ID
}
return 0
}
// attach preloads the relation the converter needs for HTMLURL, sparing a lookup per project.
func (s projectScope) attach(projects ...*project_model.Project) {
for _, project := range projects {
project.Repo, project.Owner = s.Repo, s.Owner
}
}
// findProject loads the "id" path param, scoped to this owner: another owner's ID reads
// as not found.
func (s projectScope) findProject(ctx *context.APIContext) *project_model.Project {
var project *project_model.Project
var err error
if s.Repo != nil {
project, err = project_model.GetProjectForRepoByID(ctx, s.Repo.ID, ctx.PathParamInt64("id"))
} else {
project, err = project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), s.Owner.ID)
}
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
s.attach(project)
return project
}
// columnIn resolves the "column_id" path param inside an already-resolved project.
func columnIn(ctx *context.APIContext, project *project_model.Project) *project_model.Column {
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("column_id"), project.ID)
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
return column
}
func (s projectScope) findColumn(ctx *context.APIContext) (*project_model.Project, *project_model.Column) {
project := s.findProject(ctx)
if ctx.Written() {
return nil, nil
}
return project, columnIn(ctx, project)
}
// findColumnIssue additionally resolves the "issue_id" path param, rejecting closed projects.
func (s projectScope) findColumnIssue(ctx *context.APIContext) (*project_model.Column, *issues_model.Issue) {
_, column := s.findOpenColumn(ctx)
if ctx.Written() {
return nil, nil
}
return column, s.findIssue(ctx)
}
// findIssue resolves an issue addressable within this scope. Owner-level boards span
// repositories, so the issue is looked up globally and gated on the doer's read access.
func (s projectScope) findIssue(ctx *context.APIContext) *issues_model.Issue {
issueID := ctx.PathParamInt64("issue_id")
var issue *issues_model.Issue
var err error
if s.Repo != nil {
issue, err = issues_model.GetIssueByRepoID(ctx, s.Repo.ID, issueID)
} else {
issue, err = issues_model.GetIssueByID(ctx, issueID)
}
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
if s.Repo == nil {
if err := issue.LoadRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return nil
}
perm, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil
}
// hide the issue's existence rather than reporting it as forbidden
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return nil
}
// the repo route group has mustNotBeArchived, owner boards have to check per issue
if issue.Repo.IsArchived {
ctx.APIError(http.StatusLocked, "repo is archived")
return nil
}
}
return issue
}
// findOpenProject is findProject for mutating endpoints: a closed project is read-only.
func (s projectScope) findOpenProject(ctx *context.APIContext) *project_model.Project {
project := s.findProject(ctx)
if ctx.Written() {
return nil
}
if project.IsClosed {
ctx.APIError(http.StatusForbidden, "project is closed")
return nil
}
return project
}
// findOpenColumn is findColumn with the same closed-project rejection.
func (s projectScope) findOpenColumn(ctx *context.APIContext) (*project_model.Project, *project_model.Column) {
project := s.findOpenProject(ctx)
if ctx.Written() {
return nil, nil
}
return project, columnIn(ctx, project)
}
func ListProjects(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects repository repoListProjects
// ---
// summary: List a repository's projects
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: state
// in: query
// description: State of the project (open, closed, all)
// type: string
// enum: [open, closed, all]
// default: open
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /orgs/{org}/projects organization orgListProjects
// ---
// summary: List an organization's projects
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: state
// in: query
// description: State of the project (open, closed, all)
// type: string
// enum: [open, closed, all]
// default: open
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /user/projects user userCurrentListProjects
// ---
// summary: List your projects
// produces:
// - application/json
// parameters:
// - name: state
// in: query
// description: State of the project (open, closed, all)
// type: string
// enum: [open, closed, all]
// default: open
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// swagger:operation GET /users/{username}/projects user userListProjects
// ---
// summary: List a user's projects
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user
// type: string
// required: true
// - name: state
// in: query
// description: State of the project (open, closed, all)
// type: string
// enum: [open, closed, all]
// default: open
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "404":
// "$ref": "#/responses/notFound"
scope := projectScopeFromContext(ctx)
listOptions := utils.GetListOptions(ctx)
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: listOptions,
RepoID: scope.repoID(),
OwnerID: scope.ownerID(),
IsClosed: common.ParseIssueFilterStateIsClosed(ctx.FormTrim("state")),
Type: scope.Type,
OrderBy: db.SearchOrderByIDReverse, // unique, so pagination cannot repeat or skip rows
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
// attach first, else LoadOwner re-queries the owner this scope already holds, per project
scope.attach(projects...)
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToProjectList(ctx, projects, ctx.Doer))
}
func GetProject(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id} repository repoGetProject
// ---
// summary: Get a project
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Project"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /orgs/{org}/projects/{id} organization orgGetProject
// ---
// summary: Get a project
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Project"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /user/projects/{id} user userCurrentGetProject
// ---
// summary: Get a project
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Project"
// "404":
// "$ref": "#/responses/notFound"
project := projectScopeFromContext(ctx).findProject(ctx)
if ctx.Written() {
return
}
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project, ctx.Doer))
}
func CreateProject(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects repository repoCreateProject
// ---
// summary: Create a project owned by a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectOption"
// responses:
// "201":
// "$ref": "#/responses/Project"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /orgs/{org}/projects organization orgCreateProject
// ---
// summary: Create a project owned by an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectOption"
// responses:
// "201":
// "$ref": "#/responses/Project"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation POST /user/projects user userCurrentCreateProject
// ---
// summary: Create a project owned by the authenticated user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectOption"
// responses:
// "201":
// "$ref": "#/responses/Project"
// "422":
// "$ref": "#/responses/validationError"
scope := projectScopeFromContext(ctx)
form := web.GetForm(ctx).(*api.CreateProjectOption)
templateType, err := convert.ProjectTemplateTypeFromString(form.TemplateType)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
cardType, err := convert.ProjectCardTypeFromString(form.CardType)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
project := &project_model.Project{
RepoID: scope.repoID(),
OwnerID: scope.ownerID(),
Title: form.Title,
Description: form.Description,
CreatorID: ctx.Doer.ID,
TemplateType: templateType,
CardType: cardType,
Type: scope.Type,
}
if err := project_model.NewProject(ctx, project); err != nil {
ctx.APIErrorInternal(err)
return
}
scope.attach(project)
ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project, ctx.Doer))
}
func EditProject(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id} repository repoEditProject
// ---
// summary: Edit a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectOption"
// responses:
// "200":
// "$ref": "#/responses/Project"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation PATCH /orgs/{org}/projects/{id} organization orgEditProject
// ---
// summary: Edit a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectOption"
// responses:
// "200":
// "$ref": "#/responses/Project"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation PATCH /user/projects/{id} user userCurrentEditProject
// ---
// summary: Edit a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectOption"
// responses:
// "200":
// "$ref": "#/responses/Project"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
project := projectScopeFromContext(ctx).findProject(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.EditProjectOption)
if form.Title != nil && util.IsEmptyString(*form.Title) {
ctx.APIError(http.StatusUnprocessableEntity, "title must not be empty")
return
}
opts := project_service.UpdateProjectOptions{
Title: optional.FromPtr(form.Title),
Description: optional.FromPtr(form.Description),
}
if form.CardType != nil {
cardType, err := convert.ProjectCardTypeFromString(*form.CardType)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
opts.CardType = optional.Some(cardType)
}
if form.State != nil {
switch *form.State {
case api.StateOpen:
opts.IsClosed = optional.Some(false)
case api.StateClosed:
opts.IsClosed = optional.Some(true)
default:
ctx.APIError(http.StatusUnprocessableEntity, "state must be 'open' or 'closed'")
return
}
}
if err := project_service.UpdateProject(ctx, project, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project, ctx.Doer))
}
func DeleteProject(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id} repository repoDeleteProject
// ---
// summary: Delete a project
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation DELETE /orgs/{org}/projects/{id} organization orgDeleteProject
// ---
// summary: Delete a project
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation DELETE /user/projects/{id} user userCurrentDeleteProject
// ---
// summary: Delete a project
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
project := projectScopeFromContext(ctx).findProject(ctx)
if ctx.Written() {
return
}
if err := project_model.DeleteProjectByID(ctx, project.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func ListProjectColumns(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns repository repoListProjectColumns
// ---
// summary: List a project's columns
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectColumnList"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /orgs/{org}/projects/{id}/columns organization orgListProjectColumns
// ---
// summary: List a project's columns
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectColumnList"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /user/projects/{id}/columns user userCurrentListProjectColumns
// ---
// summary: List a project's columns
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectColumnList"
// "404":
// "$ref": "#/responses/notFound"
project := projectScopeFromContext(ctx).findProject(ctx)
if ctx.Written() {
return
}
total, err := project_model.CountColumns(ctx, project.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
listOptions := utils.GetListOptions(ctx)
columns, err := project_model.GetColumns(ctx, project.ID, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToProjectColumnList(ctx, columns, ctx.Doer))
}
func CreateProjectColumn(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns repository repoCreateProjectColumn
// ---
// summary: Create a column in a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectColumnOption"
// responses:
// "201":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /orgs/{org}/projects/{id}/columns organization orgCreateProjectColumn
// ---
// summary: Create a column in a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectColumnOption"
// responses:
// "201":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation POST /user/projects/{id}/columns user userCurrentCreateProjectColumn
// ---
// summary: Create a column in a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectColumnOption"
// responses:
// "201":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
project := projectScopeFromContext(ctx).findOpenProject(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.CreateProjectColumnOption)
column := &project_model.Column{
Title: form.Title,
Color: form.Color,
ProjectID: project.ID,
CreatorID: ctx.Doer.ID,
}
if err := project_model.NewColumn(ctx, column); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToProjectColumn(ctx, column, ctx.Doer))
}
func GetProjectColumn(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns/{column_id} repository repoGetProjectColumn
// ---
// summary: Get a project column
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /orgs/{org}/projects/{id}/columns/{column_id} organization orgGetProjectColumn
// ---
// summary: Get a project column
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /user/projects/{id}/columns/{column_id} user userCurrentGetProjectColumn
// ---
// summary: Get a project column
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "404":
// "$ref": "#/responses/notFound"
_, column := projectScopeFromContext(ctx).findColumn(ctx)
if ctx.Written() {
return
}
ctx.JSON(http.StatusOK, convert.ToProjectColumn(ctx, column, ctx.Doer))
}
func EditProjectColumn(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id}/columns/{column_id} repository repoEditProjectColumn
// ---
// summary: Edit a project column
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectColumnOption"
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation PATCH /orgs/{org}/projects/{id}/columns/{column_id} organization orgEditProjectColumn
// ---
// summary: Edit a project column
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectColumnOption"
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation PATCH /user/projects/{id}/columns/{column_id} user userCurrentEditProjectColumn
// ---
// summary: Edit a project column
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectColumnOption"
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
_, column := projectScopeFromContext(ctx).findOpenColumn(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.EditProjectColumnOption)
if form.Title != nil {
if util.IsEmptyString(*form.Title) {
ctx.APIError(http.StatusUnprocessableEntity, "title must not be empty")
return
}
column.Title = *form.Title
}
if form.Color != nil {
column.Color = *form.Color
}
if form.Sorting != nil {
if *form.Sorting < math.MinInt8 || *form.Sorting > math.MaxInt8 {
ctx.APIError(http.StatusUnprocessableEntity, "sorting must be between -128 and 127")
return
}
column.Sorting = int8(*form.Sorting)
}
if err := project_model.UpdateColumn(ctx, column); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProjectColumn(ctx, column, ctx.Doer))
}
func DeleteProjectColumn(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id}/columns/{column_id} repository repoDeleteProjectColumn
// ---
// summary: Delete a project column
// description: The default column cannot be deleted while it is still the column new issues land in.
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation DELETE /orgs/{org}/projects/{id}/columns/{column_id} organization orgDeleteProjectColumn
// ---
// summary: Delete a project column
// description: The default column cannot be deleted while it is still the column new issues land in.
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation DELETE /user/projects/{id}/columns/{column_id} user userCurrentDeleteProjectColumn
// ---
// summary: Delete a project column
// description: The default column cannot be deleted while it is still the column new issues land in.
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
_, column := projectScopeFromContext(ctx).findOpenColumn(ctx)
if ctx.Written() {
return
}
if err := project_model.DeleteColumnByID(ctx, column.ID); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.Status(http.StatusNoContent)
}
func SetDefaultProjectColumn(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/default repository repoSetDefaultProjectColumn
// ---
// summary: Set a project's default column
// description: The default column is where newly assigned issues land.
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /orgs/{org}/projects/{id}/columns/{column_id}/default organization orgSetDefaultProjectColumn
// ---
// summary: Set a project's default column
// description: The default column is where newly assigned issues land.
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation POST /user/projects/{id}/columns/{column_id}/default user userCurrentSetDefaultProjectColumn
// ---
// summary: Set a project's default column
// description: The default column is where newly assigned issues land.
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
project, column := projectScopeFromContext(ctx).findOpenColumn(ctx)
if ctx.Written() {
return
}
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func MoveProjectColumns(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns/move repository repoMoveProjectColumns
// ---
// summary: Reorder a project's columns
// description: Reorders every column of the project at once. The body lists all column IDs in their new order.
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectColumnsOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /orgs/{org}/projects/{id}/columns/move organization orgMoveProjectColumns
// ---
// summary: Reorder a project's columns
// description: Reorders every column of the project at once. The body lists all column IDs in their new order.
// consumes:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectColumnsOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation POST /user/projects/{id}/columns/move user userCurrentMoveProjectColumns
// ---
// summary: Reorder a project's columns
// description: Reorders every column of the project at once. The body lists all column IDs in their new order.
// consumes:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectColumnsOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
project := projectScopeFromContext(ctx).findOpenProject(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.MoveProjectColumnsOption)
columns, err := project_model.GetColumns(ctx, project.ID, db.ListOptionsAll)
if err != nil {
ctx.APIErrorInternal(err)
return
}
existingIDs := container.FilterSlice(columns, func(column *project_model.Column) (int64, bool) { return column.ID, true })
if !util.SliceSortedEqual(form.ColumnIDs, existingIDs) {
ctx.APIError(http.StatusUnprocessableEntity, "column_ids must list every column of the project exactly once")
return
}
sortedColumnIDs := make(map[int64]int64, len(form.ColumnIDs))
for position, columnID := range form.ColumnIDs {
sortedColumnIDs[int64(position)] = columnID
}
if err := project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.Status(http.StatusNoContent)
}
func ListProjectColumnIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues repository repoListProjectColumnIssues
// ---
// summary: List the issues in a project column
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /orgs/{org}/projects/{id}/columns/{column_id}/issues organization orgListProjectColumnIssues
// ---
// summary: List the issues in a project column
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "404":
// "$ref": "#/responses/notFound"
// swagger:operation GET /user/projects/{id}/columns/{column_id}/issues user userCurrentListProjectColumnIssues
// ---
// summary: List the issues in a project column
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "404":
// "$ref": "#/responses/notFound"
scope := projectScopeFromContext(ctx)
project, column := scope.findColumn(ctx)
if ctx.Written() {
return
}
issueIDs, err := project_model.GetColumnIssueIDs(ctx, column)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(issueIDs) == 0 {
ctx.SetTotalCountHeader(0)
ctx.JSON(http.StatusOK, []*api.Issue{})
return
}
listOptions := utils.GetListOptions(ctx)
issuesOpts := &issues_model.IssuesOptions{
Paginator: &listOptions,
IssueIDs: issueIDs,
ProjectIDs: []int64{project.ID}, // joins project_issue so the column sorting applies
SortType: "project-column-sorting",
}
if scope.Repo != nil {
// the route already established repo read access, and every issue here is that repo's
issuesOpts.RepoIDs = []int64{scope.Repo.ID}
} else {
// an owner-level board spans repositories, so filter to what this doer may see
issuesOpts.Owner = scope.Owner
issuesOpts.Doer = ctx.Doer
issuesOpts.AllPublic = ctx.Doer == nil
if ctx.PublicOnly {
issuesOpts.AllPublic = true
issuesOpts.Doer = nil // a public-only token must not reach the doer's private repos
}
}
count, err := issues_model.CountIssues(ctx, issuesOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
issues, err := issues_model.Issues(ctx, issuesOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
func AddIssueToProjectColumn(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues/{issue_id} repository repoAddIssueToProjectColumn
// ---
// summary: Add an issue to a project column
// description: Assigns the issue to the project if it is not a member yet, then places it in the column.
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /orgs/{org}/projects/{id}/columns/{column_id}/issues/{issue_id} organization orgAddIssueToProjectColumn
// ---
// summary: Add an issue to a project column
// description: Assigns the issue to the project if it is not a member yet, then places it in the column.
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /user/projects/{id}/columns/{column_id}/issues/{issue_id} user userCurrentAddIssueToProjectColumn
// ---
// summary: Add an issue to a project column
// description: Assigns the issue to the project if it is not a member yet, then places it in the column.
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
column, issue := projectScopeFromContext(ctx).findColumnIssue(ctx)
if ctx.Written() {
return
}
if err := project_service.AddIssueToColumn(ctx, ctx.Doer, issue, column); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.Status(http.StatusCreated)
}
func RemoveIssueFromProjectColumn(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues/{issue_id} repository repoRemoveIssueFromProjectColumn
// ---
// summary: Remove an issue from a project column
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation DELETE /orgs/{org}/projects/{id}/columns/{column_id}/issues/{issue_id} organization orgRemoveIssueFromProjectColumn
// ---
// summary: Remove an issue from a project column
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation DELETE /user/projects/{id}/columns/{column_id}/issues/{issue_id} user userCurrentRemoveIssueFromProjectColumn
// ---
// summary: Remove an issue from a project column
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
column, issue := projectScopeFromContext(ctx).findColumnIssue(ctx)
if ctx.Written() {
return
}
if err := project_service.RemoveIssueFromColumn(ctx, ctx.Doer, issue, column); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.Status(http.StatusNoContent)
}
func MoveProjectIssue(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/issues/{issue_id}/move repository repoMoveProjectIssue
// ---
// summary: Move an issue between a project's columns
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectIssueOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /orgs/{org}/projects/{id}/issues/{issue_id}/move organization orgMoveProjectIssue
// ---
// summary: Move an issue between a project's columns
// consumes:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectIssueOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// swagger:operation POST /user/projects/{id}/issues/{issue_id}/move user userCurrentMoveProjectIssue
// ---
// summary: Move an issue between a project's columns
// consumes:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: global id of the issue, not the repository-local index
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectIssueOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
scope := projectScopeFromContext(ctx)
project := scope.findOpenProject(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.MoveProjectIssueOption)
column, err := project_model.GetColumnByIDAndProjectID(ctx, form.ColumnID, project.ID)
if err != nil {
if project_model.IsErrProjectColumnNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "target column does not belong to this project")
return
}
ctx.APIErrorInternal(err)
return
}
issue := scope.findIssue(ctx)
if ctx.Written() {
return
}
if err := project_service.MoveIssueToColumn(ctx, ctx.Doer, issue, column, optional.FromPtr(form.Sorting)); err != nil {
ctx.APIErrorAuto(err)
return
}
ctx.Status(http.StatusNoContent)
}