diff --git a/modules/structs/project.go b/modules/structs/project.go index 15dfb0e28e..d546021910 100644 --- a/modules/structs/project.go +++ b/modules/structs/project.go @@ -10,6 +10,15 @@ type Project struct { Description string `json:"description"` TemplateType uint8 `json:"template_type"` CardType uint8 `json:"card_type"` + OwnerID int64 `json:"owner_id"` + RepoID int64 `json:"repo_id"` + CreatorID int64 `json:"creator_id"` + IsClosed bool `json:"is_closed"` + Type uint8 `json:"type"` + + CreatedUnix int64 `json:"created_unix"` + UpdatedUnix int64 `json:"updated_unix"` + ClosedDateUnix int64 `json:"closed_date_unix"` } type CreateProjectOption struct { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9842cdb023..f621456a5d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -91,7 +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" + project_shared "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/actions" @@ -1019,24 +1019,24 @@ func Routes() *web.Router { m.Group("/{username}/-", func() { m.Group("/projects", func() { m.Group("", func() { - m.Get("", org.GetProjects) - m.Get("/{id}", org.GetProject) + m.Get("", project_shared.ProjectHandler("org", project_shared.GetProjects)) + m.Get("/{id}", project_shared.ProjectHandler("org", project_shared.GetProject)) }) m.Group("", func() { - m.Post("", bind(api.CreateProjectOption{}), org.CreateProject) + m.Post("", bind(api.CreateProjectOption{}), project_shared.ProjectHandler("org", project_shared.CreateProject)) m.Group("/{id}", func() { - m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) - m.Delete("", org.DeleteProject) - m.Put("", bind(api.CreateProjectOption{}), org.EditProject) - m.Post("/move", project.MoveColumns) - m.Post("/{action:open|close}", org.ChangeProjectStatus) + m.Post("", bind(api.EditProjectColumnOption{}), project_shared.ProjectHandler("org", project_shared.AddColumnToProject)) + m.Delete("", project_shared.ProjectHandler("org", project_shared.DeleteProject)) + m.Put("", bind(api.CreateProjectOption{}), project_shared.ProjectHandler("org", project_shared.EditProject)) + m.Post("/move", project_shared.MoveColumns) + m.Post("/{action:open|close}", project_shared.ChangeProjectStatus) m.Group("/{columnID}", func() { - m.Put("", bind(api.EditProjectColumnOption{}), org.EditProjectColumn) - m.Delete("", org.DeleteProjectColumn) - m.Post("/default", org.SetDefaultProjectColumn) - m.Post("/move", org.MoveIssues) + m.Put("", bind(api.EditProjectColumnOption{}), project_shared.ProjectHandler("org", project_shared.EditProjectColumn)) + m.Delete("", project_shared.ProjectHandler("org", project_shared.DeleteProjectColumn)) + m.Post("/default", project_shared.ProjectHandler("org", project_shared.SetDefaultProjectColumn)) + m.Post("/move", project_shared.ProjectHandler("org", project_shared.MoveIssues)) }) }) }, reqSelfOrAdmin(), reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true)) @@ -1048,31 +1048,31 @@ func Routes() *web.Router { m.Group("/{username}/{reponame}", func() { m.Group("/projects", func() { m.Group("", func() { - m.Get("", repo.GetProjects) - m.Get("/{id}", repo.GetProject) + m.Get("", project_shared.ProjectHandler("repo", project_shared.GetProjects)) + m.Get("/{id}", project_shared.ProjectHandler("repo", project_shared.GetProject)) }) m.Group("", func() { - m.Post("", bind(api.CreateProjectOption{}), repo.CreateProject) + m.Post("", bind(api.CreateProjectOption{}), project_shared.ProjectHandler("repo", project_shared.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.Post("", bind(api.EditProjectColumnOption{}), project_shared.ProjectHandler("repo", project_shared.AddColumnToProject)) + m.Delete("", project_shared.ProjectHandler("repo", project_shared.DeleteProject)) + m.Put("", bind(api.CreateProjectOption{}), project_shared.ProjectHandler("repo", project_shared.EditProject)) + m.Post("/move", project_shared.MoveColumns) + m.Post("/{action:open|close}", project_shared.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) + m.Put("", bind(api.EditProjectColumnOption{}), project_shared.ProjectHandler("repo", project_shared.EditProjectColumn)) + m.Delete("", project_shared.ProjectHandler("repo", project_shared.DeleteProjectColumn)) + m.Post("/default", project_shared.ProjectHandler("repo", project_shared.SetDefaultProjectColumn)) + m.Post("/move", project_shared.ProjectHandler("repo", project_shared.MoveIssues)) }) }) }, reqRepoWriter(unit.TypeProjects), mustNotBeArchived) }, individualPermsChecker) m.Group("/{type:issues|pulls}", func() { - m.Post("/projects", reqRepoWriterOr(unit.TypeIssues, unit.TypePullRequests), reqRepoWriter(unit.TypeProjects), repo.UpdateIssueProject) + m.Post("/projects", reqRepoWriterOr(unit.TypeIssues, unit.TypePullRequests), reqRepoWriter(unit.TypeProjects), project_shared.UpdateIssueProject) }, individualPermsChecker) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), repoAssignment(), reqRepoReader(unit.TypeProjects), mustEnableRepoProjects) diff --git a/routers/api/v1/org/project.go b/routers/api/v1/org/project.go deleted file mode 100644 index c789272267..0000000000 --- a/routers/api/v1/org/project.go +++ /dev/null @@ -1,408 +0,0 @@ -package org - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - project_model "code.gitea.io/gitea/models/project" - "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" - "code.gitea.io/gitea/services/convert" -) - -// CreateProject creates a new project -func CreateProject(ctx *context.APIContext) { - form := web.GetForm(ctx).(*api.CreateProjectOption) - - project := &project_model.Project{ - OwnerID: ctx.ContextUser.ID, - Title: form.Title, - Description: form.Content, - CreatorID: ctx.Doer.ID, - TemplateType: project_model.TemplateType(form.TemplateType), - CardType: project_model.CardType(form.CardType), - } - - if ctx.ContextUser.IsOrganization() { - project.Type = project_model.TypeOrganization - } else { - project.Type = project_model.TypeIndividual - } - - if err := project_model.NewProject(ctx, project); err != nil { - ctx.Error(http.StatusInternalServerError, "NewProject", err) - return - } - - ctx.JSON(http.StatusCreated, convert.ToProject(ctx, 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") - - var projectType project_model.Type - if ctx.ContextUser.IsOrganization() { - projectType = project_model.TypeOrganization - } else { - projectType = project_model.TypeIndividual - } - projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ - 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 - } - - ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) -} - -// 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.OwnerID != ctx.ContextUser.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 - } - - issues := issues_model.IssueList{} - - for _, column := range columns { - if empty := issuesMap[column.ID]; len(empty) == 0 { - continue - } - issues = append(issues, issuesMap[column.ID]...) - } - - ctx.JSON(http.StatusOK, map[string]any{ - "project": convert.ToProject(ctx, project), - "columns": convert.ToColumns(ctx, columns), - "issues": convert.ToAPIIssueList(ctx, ctx.Doer, issues), - }) -} - -// 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.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, convert.ToProject(ctx, 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.OwnerID != ctx.ContextUser.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, 0, 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) { - 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 - } - - 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, convert.ToColumn(ctx, 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 - } - - 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.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, convert.ToColumn(ctx, 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 - } - - 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.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 - } - - 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("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/repo/project.go b/routers/api/v1/shared/project.go similarity index 58% rename from routers/api/v1/repo/project.go rename to routers/api/v1/shared/project.go index a7cf7f4589..63fb629fdf 100644 --- a/routers/api/v1/repo/project.go +++ b/routers/api/v1/shared/project.go @@ -1,10 +1,12 @@ -package repo +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package shared import ( "encoding/json" "errors" "fmt" - "log" "net/http" "strings" @@ -21,18 +23,51 @@ import ( "code.gitea.io/gitea/services/convert" ) +var errInvalidModelType = errors.New("invalid model type") + +func checkModelType(model string) error { + if model != "repo" && model != "org" { + return errInvalidModelType + } + return nil +} + +// ProjectHandler is a handler for project actions +func ProjectHandler(model string, fn func(ctx *context.APIContext, model string)) func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + fn(ctx, model) + } +} + // CreateProject creates a new project -func CreateProject(ctx *context.APIContext) { +func CreateProject(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "CreateProject", err) + return + } + 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 model == "repo" { + project.Type = project_model.TypeRepository + project.RepoID = ctx.Repo.Repository.ID + } else { + if ctx.ContextUser.IsOrganization() { + project.Type = project_model.TypeOrganization + } else { + project.Type = project_model.TypeIndividual + } + project.OwnerID = ctx.ContextUser.ID } if err := project_model.NewProject(ctx, project); err != nil { @@ -43,21 +78,40 @@ func CreateProject(ctx *context.APIContext) { ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) } -// Projects renders the home page of projects -func GetProjects(ctx *context.APIContext) { +// GetProjects returns a list of projects +func GetProjects(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjects", err) + return + } + 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, + searchOptions := project_model.SearchOptions{ IsClosed: optional.Some(isShowClosed), OrderBy: project_model.GetSearchOrderByBySortType(sortType), - Type: project_model.TypeRepository, - Title: keyword, - }) + } + + if model == "repo" { + repo := ctx.Repo.Repository + searchOptions.RepoID = repo.ID + searchOptions.Type = project_model.TypeRepository + } else { + searchOptions.OwnerID = ctx.ContextUser.ID + + if ctx.ContextUser.IsOrganization() { + searchOptions.Type = project_model.TypeOrganization + } else { + searchOptions.Type = project_model.TypeIndividual + } + } + + projects, err := db.Find[project_model.Project](ctx, &searchOptions) + if err != nil { ctx.ServerError("FindProjects", err) return @@ -66,17 +120,20 @@ func GetProjects(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) } -// GetProject returns a project by ID -func GetProject(ctx *context.APIContext) { +// GetProject returns a project +func GetProject(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProject", err) + 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("", nil) - return - } columns, err := project.GetColumns(ctx) if err != nil { @@ -106,46 +163,52 @@ func GetProject(ctx *context.APIContext) { }) } -// EditProject updates a project -func EditProject(ctx *context.APIContext) { +// EditProject edits a project +func EditProject(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "EditProject", err) + return + } + form := web.GetForm(ctx).(*api.CreateProjectOption) projectID := ctx.PathParamInt64(":id") - p, err := project_model.GetProjectByID(ctx, projectID) + project, 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) + project.Title = form.Title + project.Description = form.Content + project.CardType = project_model.CardType(form.CardType) - if err = project_model.UpdateProject(ctx, p); err != nil { + if err = project_model.UpdateProject(ctx, project); err != nil { ctx.ServerError("UpdateProjects", err) return } - ctx.JSON(http.StatusOK, convert.ToProject(ctx, p)) + ctx.JSON(http.StatusOK, convert.ToProject(ctx, project)) } -// DeleteProject delete a project -func DeleteProject(ctx *context.APIContext) { - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) +// DeleteProject deletes a project +func DeleteProject(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProject", err) + return + } + + project, 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) + err = project_model.DeleteProjectByID(ctx, project.ID) if err != nil { ctx.ServerError("DeleteProjectByID", err) @@ -169,25 +232,40 @@ func ChangeProjectStatus(ctx *context.APIContext) { } id := ctx.PathParamInt64(":id") - if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil { + if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, 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.", - }) +func AddColumnToProject(ctx *context.APIContext, model string) { + var err error + err = checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "AddColumnToProject", err) return } - project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + if model == "repo" { + 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 + } + } + + var project *project_model.Project + if model == "repo" { + project, err = project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + } else { + project, err = project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) + } + if err != nil { - ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) + ctx.NotFoundOrServerError("GetProjectForRepoByID", project_model.IsErrProjectNotExist, err) return } @@ -207,8 +285,8 @@ func AddColumnToProject(ctx *context.APIContext) { ctx.JSON(http.StatusCreated, convert.ToColumn(ctx, column)) } -// CheckProjectColumnChangePermissions check permission -func checkProjectColumnChangePermissions(ctx *context.APIContext) (*project_model.Project, *project_model.Column) { +// checkProjectColumnChangePermissions check permission +func checkProjectColumnChangePermissions(ctx *context.APIContext, model string) (*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.", @@ -216,11 +294,13 @@ func checkProjectColumnChangePermissions(ctx *context.APIContext) (*project_mode 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 + if model == "repo" { + 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")) @@ -241,19 +321,35 @@ func checkProjectColumnChangePermissions(ctx *context.APIContext) (*project_mode 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 + if model == "repo" { + 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 + } + } else { + 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.APIContext) { +func EditProjectColumn(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "EditProjectColumn", err) + return + } + form := web.GetForm(ctx).(*api.EditProjectColumnOption) - _, column := checkProjectColumnChangePermissions(ctx) + _, column := checkProjectColumnChangePermissions(ctx, model) if ctx.Written() { return } @@ -275,7 +371,14 @@ func EditProjectColumn(ctx *context.APIContext) { } // DeleteProjectColumn allows for the deletion of a project column -func DeleteProjectColumn(ctx *context.APIContext) { +func DeleteProjectColumn(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProjectColumn", err) + return + } + if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -283,11 +386,13 @@ func DeleteProjectColumn(ctx *context.APIContext) { 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 + if model == "repo" { + 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")) @@ -308,11 +413,20 @@ func DeleteProjectColumn(ctx *context.APIContext) { 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 model == "repo" { + 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 + } + } else { + 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 { @@ -324,8 +438,15 @@ func DeleteProjectColumn(ctx *context.APIContext) { } // SetDefaultProjectColumn set default column for uncategorized issues/pulls -func SetDefaultProjectColumn(ctx *context.APIContext) { - project, column := checkProjectColumnChangePermissions(ctx) +func SetDefaultProjectColumn(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "SetDefaultProjectColumn", err) + return + } + + project, column := checkProjectColumnChangePermissions(ctx, model) if ctx.Written() { return } @@ -339,7 +460,14 @@ func SetDefaultProjectColumn(ctx *context.APIContext) { } // MoveIssues moves or keeps issues in a column and sorts them inside that column -func MoveIssues(ctx *context.APIContext) { +func MoveIssues(ctx *context.APIContext, model string) { + err := checkModelType(model) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "MoveIssues", err) + return + } + if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -347,11 +475,13 @@ func MoveIssues(ctx *context.APIContext) { 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 + if model == "repo" { + 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")) @@ -359,10 +489,6 @@ func MoveIssues(ctx *context.APIContext) { 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 { @@ -436,7 +562,7 @@ func getActionIssues(ctx *context.APIContext, issuesIDs []int64) issues_model.Is ctx.ServerError("GetIssuesByIDs", err) return nil } - // Check access rights for all issues + issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) for _, issue := range issues { @@ -470,8 +596,6 @@ func UpdateIssueProject(ctx *context.APIContext) { return } - log.Println("form", form) - log.Println(ctx.Repo.Repository.ID) issues := getActionIssues(ctx, form.Issues) if ctx.Written() { return @@ -502,3 +626,41 @@ func UpdateIssueProject(ctx *context.APIContext) { ctx.JSON(http.StatusOK, map[string]string{"message": "issues moved successfully"}) } + +// 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"}) +} diff --git a/routers/api/v1/shared/project/project.go b/routers/api/v1/shared/project/project.go deleted file mode 100644 index d3a52a1f6f..0000000000 --- a/routers/api/v1/shared/project/project.go +++ /dev/null @@ -1,47 +0,0 @@ -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"}) -} diff --git a/services/convert/project.go b/services/convert/project.go index db3d7991b5..8f0daa1460 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -12,25 +12,25 @@ import ( // ToProject converts a models.Project to api.Project func ToProject(ctx context.Context, project *project_model.Project) *api.Project { - if project == nil { - return nil - } - return &api.Project{ - ID: project.ID, - Title: project.Title, - Description: project.Description, - TemplateType: uint8(project.TemplateType), - CardType: uint8(project.CardType), + ID: project.ID, + Title: project.Title, + Description: project.Description, + TemplateType: uint8(project.TemplateType), + CardType: uint8(project.CardType), + OwnerID: project.OwnerID, + RepoID: project.RepoID, + CreatorID: project.CreatorID, + IsClosed: project.IsClosed, + Type: uint8(project.Type), + CreatedUnix: int64(project.CreatedUnix), + UpdatedUnix: int64(project.UpdatedUnix), + ClosedDateUnix: int64(project.ClosedDateUnix), } } // ToProjects converts a slice of models.Project to a slice of api.Project func ToProjects(ctx context.Context, projects []*project_model.Project) []*api.Project { - if projects == nil { - return nil - } - result := make([]*api.Project, len(projects)) for i, project := range projects { result[i] = ToProject(ctx, project)