mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 19:38:23 +00:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -91,6 +91,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/routers/api/v1/packages" | 	"code.gitea.io/gitea/routers/api/v1/packages" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/repo" | 	"code.gitea.io/gitea/routers/api/v1/repo" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/settings" | 	"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/api/v1/user" | ||||||
| 	"code.gitea.io/gitea/routers/common" | 	"code.gitea.io/gitea/routers/common" | ||||||
| 	"code.gitea.io/gitea/services/actions" | 	"code.gitea.io/gitea/services/actions" | ||||||
| @@ -137,6 +138,7 @@ func sudo() func(ctx *context.APIContext) { | |||||||
| func repoAssignment() func(ctx *context.APIContext) { | func repoAssignment() func(ctx *context.APIContext) { | ||||||
| 	return func(ctx *context.APIContext) { | 	return func(ctx *context.APIContext) { | ||||||
| 		userName := ctx.PathParam("username") | 		userName := ctx.PathParam("username") | ||||||
|  | 		orgName := ctx.PathParam("org") | ||||||
| 		repoName := ctx.PathParam("reponame") | 		repoName := ctx.PathParam("reponame") | ||||||
|  |  | ||||||
| 		var ( | 		var ( | ||||||
| @@ -145,25 +147,49 @@ func repoAssignment() func(ctx *context.APIContext) { | |||||||
| 		) | 		) | ||||||
|  |  | ||||||
| 		// Check if the user is the same as the repository owner. | 		// Check if the user is the same as the repository owner. | ||||||
| 		if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { | 		if userName != "" { | ||||||
| 			owner = ctx.Doer | 			if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { | ||||||
| 		} else { | 				owner = ctx.Doer | ||||||
| 			owner, err = user_model.GetUserByName(ctx, userName) | 			} 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 err != nil { | ||||||
| 				if user_model.IsErrUserNotExist(err) { | 				if organization.IsErrOrgNotExist(err) { | ||||||
| 					if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { | 					redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName) | ||||||
| 						context.RedirectToUser(ctx.Base, userName, redirectUserID) | 					if err == nil { | ||||||
|  | 						context.RedirectToUser(ctx.Base, orgName, redirectUserID) | ||||||
| 					} else if user_model.IsErrUserRedirectNotExist(err) { | 					} else if user_model.IsErrUserRedirectNotExist(err) { | ||||||
| 						ctx.NotFound("GetUserByName", err) | 						ctx.NotFound() | ||||||
| 					} else { | 					} else { | ||||||
| 						ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) | 						ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) | ||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					ctx.Error(http.StatusInternalServerError, "GetUserByName", err) | 					ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) | ||||||
| 				} | 				} | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  | 			ctx.Org.Organization = org | ||||||
|  | 			owner = org.AsUser() | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ctx.Repo.Owner = owner | 		ctx.Repo.Owner = owner | ||||||
| 		ctx.ContextUser = owner | 		ctx.ContextUser = owner | ||||||
|  |  | ||||||
| @@ -978,7 +1004,7 @@ func Routes() *web.Router { | |||||||
| 						m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) | 						m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) | ||||||
| 						m.Delete("", org.DeleteProject) | 						m.Delete("", org.DeleteProject) | ||||||
| 						m.Put("", bind(api.CreateProjectOption{}), org.EditProject) | 						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.Post("/{action:open|close}", org.ChangeProjectStatus) | ||||||
|  |  | ||||||
| 						m.Group("/{columnID}", func() { | 						m.Group("/{columnID}", func() { | ||||||
| @@ -998,6 +1024,40 @@ func Routes() *web.Router { | |||||||
|  |  | ||||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), context.UserAssignmentAPI()) | 		}, 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) | 		// Organizations (requires orgs scope) | ||||||
| 		m.Group("orgs/{org}/-", func() { | 		m.Group("orgs/{org}/-", func() { | ||||||
| 			m.Group("/projects", func() { | 			m.Group("/projects", func() { | ||||||
| @@ -1012,7 +1072,7 @@ func Routes() *web.Router { | |||||||
| 						m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) | 						m.Post("", bind(api.EditProjectColumnOption{}), org.AddColumnToProject) | ||||||
| 						m.Delete("", org.DeleteProject) | 						m.Delete("", org.DeleteProject) | ||||||
| 						m.Put("", bind(api.CreateProjectOption{}), org.EditProject) | 						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.Post("/{action:open|close}", org.ChangeProjectStatus) | ||||||
|  |  | ||||||
| 						m.Group("/{columnID}", func() { | 						m.Group("/{columnID}", func() { | ||||||
| @@ -1032,6 +1092,40 @@ func Routes() *web.Router { | |||||||
|  |  | ||||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), orgAssignment(true)) | 		}, 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) | 		// Users (requires user scope) | ||||||
| 		m.Group("/users", func() { | 		m.Group("/users", func() { | ||||||
| 			m.Group("/{username}", func() { | 			m.Group("/{username}", func() { | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ import ( | |||||||
| 	project_model "code.gitea.io/gitea/models/project" | 	project_model "code.gitea.io/gitea/models/project" | ||||||
| 	attachment_model "code.gitea.io/gitea/models/repo" | 	attachment_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/modules/optional" | 	"code.gitea.io/gitea/modules/optional" | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/services/context" | 	"code.gitea.io/gitea/services/context" | ||||||
| @@ -68,16 +67,10 @@ func ChangeProjectStatus(ctx *context.APIContext) { | |||||||
|  |  | ||||||
| // Projects renders the home page of projects | // Projects renders the home page of projects | ||||||
| func GetProjects(ctx *context.APIContext) { | func GetProjects(ctx *context.APIContext) { | ||||||
| 	ctx.Data["Title"] = ctx.Tr("repo.projects") |  | ||||||
|  |  | ||||||
| 	sortType := ctx.FormTrim("sort") | 	sortType := ctx.FormTrim("sort") | ||||||
|  |  | ||||||
| 	isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" | 	isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" | ||||||
| 	keyword := ctx.FormTrim("q") | 	keyword := ctx.FormTrim("q") | ||||||
| 	page := ctx.FormInt("page") |  | ||||||
| 	if page <= 1 { |  | ||||||
| 		page = 1 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var projectType project_model.Type | 	var projectType project_model.Type | ||||||
| 	if ctx.ContextUser.IsOrganization() { | 	if ctx.ContextUser.IsOrganization() { | ||||||
| @@ -86,10 +79,6 @@ func GetProjects(ctx *context.APIContext) { | |||||||
| 		projectType = project_model.TypeIndividual | 		projectType = project_model.TypeIndividual | ||||||
| 	} | 	} | ||||||
| 	projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ | 	projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ | ||||||
| 		ListOptions: db.ListOptions{ |  | ||||||
| 			Page:     page, |  | ||||||
| 			PageSize: setting.UI.IssuePagingNum, |  | ||||||
| 		}, |  | ||||||
| 		OwnerID:  ctx.ContextUser.ID, | 		OwnerID:  ctx.ContextUser.ID, | ||||||
| 		IsClosed: optional.Some(isShowClosed), | 		IsClosed: optional.Some(isShowClosed), | ||||||
| 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType), | 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType), | ||||||
| @@ -180,31 +169,31 @@ func GetProject(ctx *context.APIContext) { | |||||||
| 	ctx.JSON(http.StatusOK, data) | 	ctx.JSON(http.StatusOK, data) | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddColumnToProject adds a new column to a project | // EditProject updates a project | ||||||
| func AddColumnToProject(ctx *context.APIContext) { | func EditProject(ctx *context.APIContext) { | ||||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) | 	form := web.GetForm(ctx).(*api.CreateProjectOption) | ||||||
|  | 	projectID := ctx.PathParamInt64(":id") | ||||||
|  |  | ||||||
|  | 	p, err := project_model.GetProjectByID(ctx, projectID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if project.OwnerID != ctx.ContextUser.ID { | 	if p.OwnerID != ctx.ContextUser.ID { | ||||||
| 		ctx.NotFound("", nil) | 		ctx.NotFound("", nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	form := web.GetForm(ctx).(*api.EditProjectColumnOption) | 	p.Title = form.Title | ||||||
| 	column := &project_model.Column{ | 	p.Description = form.Content | ||||||
| 		ProjectID: project.ID, | 	p.CardType = project_model.CardType(form.CardType) | ||||||
| 		Title:     form.Title, |  | ||||||
| 		Sorting:   form.Sorting, | 	if err = project_model.UpdateProject(ctx, p); err != nil { | ||||||
| 		Color:     form.Color, | 		ctx.ServerError("UpdateProjects", err) | ||||||
| 	} |  | ||||||
| 	if err := project_model.NewColumn(ctx, column); err != nil { |  | ||||||
| 		ctx.ServerError("NewProjectColumn", err) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.JSON(http.StatusCreated, column) | 	ctx.JSON(http.StatusOK, p) | ||||||
| } | } | ||||||
|  |  | ||||||
| // DeleteProject delete a project | // DeleteProject delete a project | ||||||
| @@ -229,71 +218,32 @@ func DeleteProject(ctx *context.APIContext) { | |||||||
| 	ctx.JSON(http.StatusOK, map[string]any{"message": "project deleted successfully"}) | 	ctx.JSON(http.StatusOK, map[string]any{"message": "project deleted successfully"}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // EditProject updates a project | // AddColumnToProject adds a new column to a project | ||||||
| func EditProject(ctx *context.APIContext) { | func AddColumnToProject(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) { |  | ||||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) | 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) { | 	if project.OwnerID != ctx.ContextUser.ID { | ||||||
| 		ctx.NotFound("CanBeAccessedByOwnerRepo", nil) | 		ctx.NotFound("", nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	type movedColumnsForm struct { | 	form := web.GetForm(ctx).(*api.EditProjectColumnOption) | ||||||
| 		Columns []struct { | 	column := &project_model.Column{ | ||||||
| 			ColumnID int64 `json:"columnID"` | 		ProjectID: project.ID, | ||||||
| 			Sorting  int64 `json:"sorting"` | 		Title:     form.Title, | ||||||
| 		} `json:"columns"` | 		Sorting:   form.Sorting, | ||||||
|  | 		Color:     form.Color, | ||||||
|  | 		CreatorID: ctx.Doer.ID, | ||||||
| 	} | 	} | ||||||
|  | 	if err := project_model.NewColumn(ctx, column); err != nil { | ||||||
| 	form := &movedColumnsForm{} | 		ctx.ServerError("NewProjectColumn", err) | ||||||
| 	if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { |  | ||||||
| 		ctx.ServerError("DecodeMovedColumnsForm", err) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sortedColumnIDs := make(map[int64]int64) | 	ctx.JSON(http.StatusCreated, column) | ||||||
| 	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"}) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // CheckProjectColumnChangePermissions check permission | // CheckProjectColumnChangePermissions check permission | ||||||
|   | |||||||
							
								
								
									
										460
									
								
								routers/api/v1/repo/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								routers/api/v1/repo/project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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"}) | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								routers/api/v1/shared/project/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								routers/api/v1/shared/project/project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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"}) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user