mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-30 19:08:37 +00:00 
			
		
		
		
	api(refactor): create a common handler for org and repo projects
edit project api response format to include more fields
This commit is contained in:
		| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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"}) | ||||
| } | ||||
| @@ -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"}) | ||||
| } | ||||
| @@ -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"}) | ||||
| } | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user