From b2992372a562893891c592f700318bc0e237d9e5 Mon Sep 17 00:00:00 2001 From: eyad-hussein Date: Fri, 12 Jul 2024 20:45:48 +0300 Subject: [PATCH] api: implement logic for all (user and org)/reponame/projects endpoints and crud operations create endpoints for each operation in the web router and corresponding handler and tested them manually --- routers/api/v1/api.go | 116 +++++- routers/api/v1/org/project.go | 106 ++---- routers/api/v1/repo/project.go | 460 +++++++++++++++++++++++ routers/api/v1/shared/project/project.go | 47 +++ 4 files changed, 640 insertions(+), 89 deletions(-) create mode 100644 routers/api/v1/repo/project.go create mode 100644 routers/api/v1/shared/project/project.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 417e75a8c0..a3b65db97d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -91,6 +91,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/packages" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" + "code.gitea.io/gitea/routers/api/v1/shared/project" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/actions" @@ -137,6 +138,7 @@ func sudo() func(ctx *context.APIContext) { func repoAssignment() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { userName := ctx.PathParam("username") + orgName := ctx.PathParam("org") repoName := ctx.PathParam("reponame") var ( @@ -145,25 +147,49 @@ func repoAssignment() func(ctx *context.APIContext) { ) // Check if the user is the same as the repository owner. - if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { - owner = ctx.Doer - } else { - owner, err = user_model.GetUserByName(ctx, userName) + if userName != "" { + if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { + owner = ctx.Doer + } else { + owner, err = user_model.GetUserByName(ctx, userName) + if err != nil { + if user_model.IsErrUserNotExist(err) { + if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { + context.RedirectToUser(ctx.Base, userName, redirectUserID) + } else if user_model.IsErrUserRedirectNotExist(err) { + ctx.NotFound("GetUserByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) + } + } else { + ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + } + return + } + } + } + + if orgName != "" { + org, err := organization.GetOrgByName(ctx, orgName) if err != nil { - if user_model.IsErrUserNotExist(err) { - if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { - context.RedirectToUser(ctx.Base, userName, redirectUserID) + if organization.IsErrOrgNotExist(err) { + redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName) + if err == nil { + context.RedirectToUser(ctx.Base, orgName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetUserByName", err) + ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) } } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) } return } + ctx.Org.Organization = org + owner = org.AsUser() } + ctx.Repo.Owner = owner ctx.ContextUser = owner @@ -978,7 +1004,7 @@ func Routes() *web.Router { m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) m.Delete("", org.DeleteProject) m.Put("", bind(api.CreateProjectOption{}), org.EditProject) - m.Post("/move", org.MoveColumns) + m.Post("/move", project.MoveColumns) m.Post("/{action:open|close}", org.ChangeProjectStatus) m.Group("/{columnID}", func() { @@ -998,6 +1024,40 @@ func Routes() *web.Router { }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), context.UserAssignmentAPI()) + // Users (requires user scope) + m.Group("users/{username}/{reponame}", func() { + m.Group("/projects", func() { + m.Group("", func() { + m.Get("", repo.GetProjects) + m.Get("/{id}", repo.GetProject) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) + + m.Group("", func() { + m.Post("", bind(api.CreateProjectOption{}), repo.CreateProject) + m.Group("/{id}", func() { + m.Post("", bind(api.EditProjectColumnOption{}), repo.AddColumnToProject) + m.Delete("", repo.DeleteProject) + m.Put("", bind(api.CreateProjectOption{}), repo.EditProject) + m.Post("/move", project.MoveColumns) + m.Post("/{action:open|close}", repo.ChangeProjectStatus) + + m.Group("/{columnID}", func() { + m.Put("", bind(api.EditProjectColumnOption{}), repo.EditProjectColumn) + m.Delete("", repo.DeleteProjectColumn) + m.Post("/default", repo.SetDefaultProjectColumn) + m.Post("/move", repo.MoveIssues) + }) + }) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), func(ctx *context.APIContext) { + if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID { + ctx.NotFound("NewProject", nil) + return + } + }) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), individualPermsChecker) + + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryRepository), reqToken(), repoAssignment()) + // Organizations (requires orgs scope) m.Group("orgs/{org}/-", func() { m.Group("/projects", func() { @@ -1012,7 +1072,7 @@ func Routes() *web.Router { m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) m.Delete("", org.DeleteProject) m.Put("", bind(api.CreateProjectOption{}), org.EditProject) - m.Post("/move", org.MoveColumns) + m.Post("/move", project.MoveColumns) m.Post("/{action:open|close}", org.ChangeProjectStatus) m.Group("/{columnID}", func() { @@ -1032,6 +1092,40 @@ func Routes() *web.Router { }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), orgAssignment(true)) + // Organizations (requires orgs scope) + m.Group("orgs/{org}/{reponame}", func() { + m.Group("/projects", func() { + m.Group("", func() { + m.Get("", repo.GetProjects) + m.Get("/{id}", repo.GetProject) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) + + m.Group("", func() { + m.Post("", bind(api.CreateProjectOption{}), repo.CreateProject) + m.Group("/{id}", func() { + m.Post("", bind(api.EditProjectColumnOption{}), repo.AddColumnToProject) + m.Delete("", repo.DeleteProject) + m.Put("", bind(api.CreateProjectOption{}), repo.EditProject) + m.Post("/move", project.MoveColumns) + m.Post("/{action:open|close}", repo.ChangeProjectStatus) + + m.Group("/{columnID}", func() { + m.Put("", bind(api.EditProjectColumnOption{}), repo.EditProjectColumn) + m.Delete("", repo.DeleteProjectColumn) + m.Post("/default", repo.SetDefaultProjectColumn) + m.Post("/move", repo.MoveIssues) + }) + }) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), func(ctx *context.APIContext) { + if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID { + ctx.NotFound("NewProject", nil) + return + } + }) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), individualPermsChecker) + + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryRepository), reqToken(), repoAssignment()) + // Users (requires user scope) m.Group("/users", func() { m.Group("/{username}", func() { diff --git a/routers/api/v1/org/project.go b/routers/api/v1/org/project.go index 7429c5c943..a687f51d9b 100644 --- a/routers/api/v1/org/project.go +++ b/routers/api/v1/org/project.go @@ -12,7 +12,6 @@ import ( project_model "code.gitea.io/gitea/models/project" attachment_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -68,16 +67,10 @@ func ChangeProjectStatus(ctx *context.APIContext) { // Projects renders the home page of projects func GetProjects(ctx *context.APIContext) { - 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() { @@ -86,10 +79,6 @@ func GetProjects(ctx *context.APIContext) { projectType = project_model.TypeIndividual } projects, err := db.Find[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), @@ -180,31 +169,31 @@ func GetProject(ctx *context.APIContext) { ctx.JSON(http.StatusOK, data) } -// AddColumnToProject adds a new column to a project -func AddColumnToProject(ctx *context.APIContext) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) +// EditProject updates a project +func EditProject(ctx *context.APIContext) { + form := web.GetForm(ctx).(*api.CreateProjectOption) + projectID := ctx.PathParamInt64(":id") + + p, err := project_model.GetProjectByID(ctx, projectID) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return } - if project.OwnerID != ctx.ContextUser.ID { + if p.OwnerID != ctx.ContextUser.ID { ctx.NotFound("", nil) return } - form := web.GetForm(ctx).(*api.EditProjectColumnOption) - column := &project_model.Column{ - ProjectID: project.ID, - Title: form.Title, - Sorting: form.Sorting, - Color: form.Color, - } - if err := project_model.NewColumn(ctx, column); err != nil { - ctx.ServerError("NewProjectColumn", err) + p.Title = form.Title + p.Description = form.Content + p.CardType = project_model.CardType(form.CardType) + + if err = project_model.UpdateProject(ctx, p); err != nil { + ctx.ServerError("UpdateProjects", err) return } - ctx.JSON(http.StatusCreated, column) + ctx.JSON(http.StatusOK, p) } // DeleteProject delete a project @@ -229,71 +218,32 @@ func DeleteProject(ctx *context.APIContext) { ctx.JSON(http.StatusOK, map[string]any{"message": "project deleted successfully"}) } -// EditProject updates a project -func EditProject(ctx *context.APIContext) { - form := web.GetForm(ctx).(*api.CreateProjectOption) - projectID := ctx.PathParamInt64(":id") - - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID) - - 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 = project_model.CardType(form.CardType) - - if err = project_model.UpdateProject(ctx, p); err != nil { - ctx.ServerError("UpdateProjects", err) - return - } - - ctx.JSON(http.StatusOK, p) -} - -// MoveColumns moves or keeps columns in a project and sorts them inside that project -func MoveColumns(ctx *context.APIContext) { +// AddColumnToProject adds a new column to a project +func AddColumnToProject(ctx *context.APIContext) { project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return } - if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) { - ctx.NotFound("CanBeAccessedByOwnerRepo", nil) + if project.OwnerID != ctx.ContextUser.ID { + ctx.NotFound("", nil) return } - type movedColumnsForm struct { - Columns []struct { - ColumnID int64 `json:"columnID"` - Sorting int64 `json:"sorting"` - } `json:"columns"` + form := web.GetForm(ctx).(*api.EditProjectColumnOption) + column := &project_model.Column{ + ProjectID: project.ID, + Title: form.Title, + Sorting: form.Sorting, + Color: form.Color, + CreatorID: ctx.Doer.ID, } - - form := &movedColumnsForm{} - if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("DecodeMovedColumnsForm", err) + if err := project_model.NewColumn(ctx, column); err != nil { + ctx.ServerError("NewProjectColumn", err) return } - sortedColumnIDs := make(map[int64]int64) - for _, column := range form.Columns { - sortedColumnIDs[column.Sorting] = column.ColumnID - } - - if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil { - ctx.ServerError("MoveColumnsOnProject", err) - return - } - - ctx.JSON(http.StatusOK, map[string]string{"message": "columns moved successfully"}) + ctx.JSON(http.StatusCreated, column) } // CheckProjectColumnChangePermissions check permission diff --git a/routers/api/v1/repo/project.go b/routers/api/v1/repo/project.go new file mode 100644 index 0000000000..42e4fe1e56 --- /dev/null +++ b/routers/api/v1/repo/project.go @@ -0,0 +1,460 @@ +package repo + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + 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/optional" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" +) + +// CreateProject creates a new project +func CreateProject(ctx *context.APIContext) { + form := web.GetForm(ctx).(*api.CreateProjectOption) + + project := &project_model.Project{ + RepoID: ctx.Repo.Repository.ID, + Title: form.Title, + Description: form.Content, + CreatorID: ctx.Doer.ID, + TemplateType: project_model.TemplateType(form.TemplateType), + CardType: project_model.CardType(form.CardType), + Type: project_model.TypeRepository, + } + + if err := project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + ctx.JSON(http.StatusCreated, project) +} + +// Projects renders the home page of projects +func GetProjects(ctx *context.APIContext) { + sortType := ctx.FormTrim("sort") + + isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" + keyword := ctx.FormTrim("q") + repo := ctx.Repo.Repository + + projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ + RepoID: repo.ID, + IsClosed: optional.Some(isShowClosed), + OrderBy: project_model.GetSearchOrderByBySortType(sortType), + Type: project_model.TypeRepository, + Title: keyword, + }) + if err != nil { + ctx.ServerError("FindProjects", err) + return + } + + ctx.JSON(http.StatusOK, projects) +} + +// TODO: Send issues as well +// GetProject returns a project by ID +func GetProject(ctx *context.APIContext) { + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + if project.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + columns, err := project.GetColumns(ctx) + if err != nil { + ctx.ServerError("GetProjectColumns", err) + return + } + + issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns) + if err != nil { + ctx.ServerError("LoadIssuesOfColumns", err) + return + } + + 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 + } + } + } + } + + issues := make(map[int64][]*issues_model.Issue) + + for _, column := range columns { + if empty := issuesMap[column.ID]; len(empty) == 0 { + continue + } + issues[column.ID] = issuesMap[column.ID] + + } + + data := map[string]any{ + "project": project, + "columns": columns, + } + + ctx.JSON(http.StatusOK, data) +} + +// EditProject updates a project +func EditProject(ctx *context.APIContext) { + form := web.GetForm(ctx).(*api.CreateProjectOption) + projectID := ctx.PathParamInt64(":id") + + p, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + p.Title = form.Title + p.Description = form.Content + p.CardType = project_model.CardType(form.CardType) + + if err = project_model.UpdateProject(ctx, p); err != nil { + ctx.ServerError("UpdateProjects", err) + return + } + + ctx.JSON(http.StatusOK, p) +} + +// DeleteProject delete a project +func DeleteProject(ctx *context.APIContext) { + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + + err = project_model.DeleteProjectByID(ctx, p.ID) + + if err != nil { + ctx.ServerError("DeleteProjectByID", err) + return + } + + ctx.JSON(http.StatusOK, map[string]any{"message": "project deleted successfully"}) +} + +// ChangeProjectStatus updates the status of a project between "open" and "close" +func ChangeProjectStatus(ctx *context.APIContext) { + var toClose bool + switch ctx.PathParam(":action") { + case "open": + toClose = false + case "close": + toClose = true + default: + ctx.NotFound("ChangeProjectStatus", nil) + return + } + id := ctx.PathParamInt64(":id") + + if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil { + ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) + return + } + ctx.JSON(http.StatusOK, map[string]any{"message": "project status updated successfully"}) +} + +// AddColumnToProject adds a new column to a project +func AddColumnToProject(ctx *context.APIContext) { + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + + form := web.GetForm(ctx).(*api.EditProjectColumnOption) + column := &project_model.Column{ + ProjectID: project.ID, + Title: form.Title, + Sorting: form.Sorting, + Color: form.Color, + CreatorID: ctx.Doer.ID, + } + if err := project_model.NewColumn(ctx, column); err != nil { + ctx.ServerError("NewProjectColumn", err) + return + } + + ctx.JSON(http.StatusCreated, column) +} + +// CheckProjectColumnChangePermissions check permission +func checkProjectColumnChangePermissions(ctx *context.APIContext) (*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 + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return nil, nil + } + + project, err := 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.RepoID != ctx.Repo.Repository.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.APIContext) { + form := web.GetForm(ctx).(*api.EditProjectColumnOption) + _, 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.JSON(http.StatusOK, column) +} + +// DeleteProjectColumn allows for the deletion of a project column +func DeleteProjectColumn(ctx *context.APIContext) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + project, err := 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.RepoID != ctx.Repo.Repository.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.JSON(http.StatusOK, map[string]string{"message": "column deleted successfully"}) +} + +// SetDefaultProjectColumn set default column for uncategorized issues/pulls +func SetDefaultProjectColumn(ctx *context.APIContext) { + 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.JSON(http.StatusOK, map[string]string{"message": "default column set successfully"}) +} + +// MoveIssues moves or keeps issues in a column and sorts them inside that column +func MoveIssues(ctx *context.APIContext) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + if project.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("InvalidRepoID", 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("ColumnNotInProject", 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_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) + return + } + + ctx.JSON(http.StatusOK, map[string]string{"message": "issues moved successfully"}) +} diff --git a/routers/api/v1/shared/project/project.go b/routers/api/v1/shared/project/project.go new file mode 100644 index 0000000000..d3a52a1f6f --- /dev/null +++ b/routers/api/v1/shared/project/project.go @@ -0,0 +1,47 @@ +package project + +import ( + "encoding/json" + "net/http" + + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/services/context" +) + +// MoveColumns moves or keeps columns in a project and sorts them inside that project +func MoveColumns(ctx *context.APIContext) { + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + if err != nil { + ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + return + } + if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) { + ctx.NotFound("CanBeAccessedByOwnerRepo", nil) + return + } + + type movedColumnsForm struct { + Columns []struct { + ColumnID int64 `json:"columnID"` + Sorting int64 `json:"sorting"` + } `json:"columns"` + } + + form := &movedColumnsForm{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedColumnsForm", err) + return + } + + sortedColumnIDs := make(map[int64]int64) + for _, column := range form.Columns { + sortedColumnIDs[column.Sorting] = column.ColumnID + } + + if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil { + ctx.ServerError("MoveColumnsOnProject", err) + return + } + + ctx.JSON(http.StatusOK, map[string]string{"message": "columns moved successfully"}) +}