mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	refactor: refactor endpoints to comply with github api
introduce new logic to handle project and column assignment, refactor everything and removed unnecessary code, create swagger docs
This commit is contained in:
		| @@ -76,6 +76,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/models/organization" | ||||
| 	"code.gitea.io/gitea/models/perm" | ||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | ||||
| 	project_model "code.gitea.io/gitea/models/project" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| @@ -89,9 +90,9 @@ import ( | ||||
| 	"code.gitea.io/gitea/routers/api/v1/notify" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/org" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/packages" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/project" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/repo" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/settings" | ||||
| 	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" | ||||
| @@ -135,6 +136,114 @@ func sudo() func(ctx *context.APIContext) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func projectIDAssignmentAPI() func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		if ctx.PathParam(":project_id") == "" { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		projectAssignment(ctx, ctx.PathParamInt64(":project_id")) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func projectAssignment(ctx *context.APIContext, projectID int64) { | ||||
| 	var ( | ||||
| 		owner *user_model.User | ||||
| 		err   error | ||||
| 	) | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, projectID) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusNotFound, "GetProjectByID", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if project.Type == project_model.TypeIndividual || project.Type == project_model.TypeOrganization { | ||||
| 		if err := project.LoadOwner(ctx); err != nil { | ||||
| 			ctx.Error(http.StatusNotFound, "LoadOwner", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(project.Owner.Name) { | ||||
| 			owner = ctx.Doer | ||||
| 		} else { | ||||
| 			owner = project.Owner | ||||
| 		} | ||||
|  | ||||
| 		if project.Type == project_model.TypeOrganization { | ||||
| 			ctx.Org.Organization = (*organization.Organization)(owner) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if err := project.LoadRepo(ctx); err != nil { | ||||
| 			ctx.Error(http.StatusNotFound, "LoadRepo", err) | ||||
| 		} | ||||
|  | ||||
| 		repo := project.Repo | ||||
|  | ||||
| 		if err := repo.LoadOwner(ctx); err != nil { | ||||
| 			ctx.Error(http.StatusNotFound, "LoadOwner", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		ctx.Repo.Repository = repo | ||||
| 		owner = repo.Owner | ||||
|  | ||||
| 		if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { | ||||
| 			taskID := ctx.Data["ActionsTaskID"].(int64) | ||||
| 			task, err := actions_model.GetTaskByID(ctx, taskID) | ||||
| 			if err != nil { | ||||
| 				ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) | ||||
| 				return | ||||
| 			} | ||||
| 			if task.RepoID != repo.ID { | ||||
| 				ctx.NotFound() | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if task.IsForkPullRequest { | ||||
| 				ctx.Repo.Permission.AccessMode = perm.AccessModeRead | ||||
| 			} else { | ||||
| 				ctx.Repo.Permission.AccessMode = perm.AccessModeWrite | ||||
| 			} | ||||
|  | ||||
| 			if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { | ||||
| 				ctx.Error(http.StatusInternalServerError, "LoadUnits", err) | ||||
| 				return | ||||
| 			} | ||||
| 			ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode) | ||||
| 		} else { | ||||
| 			ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) | ||||
| 			if err != nil { | ||||
| 				ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if !ctx.Repo.Permission.HasAnyUnitAccess() { | ||||
| 			ctx.NotFound() | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	ctx.ContextUser = owner | ||||
| } | ||||
|  | ||||
| func columnAssignment() func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		if ctx.PathParam("column_id") == "" { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusNotFound, "GetColumn", err) | ||||
| 		} | ||||
|  | ||||
| 		projectAssignment(ctx, column.ProjectID) | ||||
|  | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func repoAssignment() func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		userName := ctx.PathParam("username") | ||||
| @@ -169,6 +278,10 @@ func repoAssignment() func(ctx *context.APIContext) { | ||||
| 		ctx.Repo.Owner = owner | ||||
| 		ctx.ContextUser = owner | ||||
|  | ||||
| 		if owner.IsOrganization() { | ||||
| 			ctx.Org.Organization = (*organization.Organization)(owner) | ||||
| 		} | ||||
|  | ||||
| 		// Get repository. | ||||
| 		repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName) | ||||
| 		if err != nil { | ||||
| @@ -388,6 +501,9 @@ func reqAdmin() func(ctx *context.APIContext) { | ||||
| // reqRepoWriter user should have a permission to write to a repo, or be a site admin | ||||
| func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		if ctx.Repo.Repository == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { | ||||
| 			ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") | ||||
| 			return | ||||
| @@ -395,18 +511,6 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // reqRepoWriterOr returns a middleware for requiring repository write to one of the unit permission | ||||
| func reqRepoWriterOr(unitTypes ...unit.Type) func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		for _, unitType := range unitTypes { | ||||
| 			if ctx.Repo.CanWrite(unitType) { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 		ctx.NotFound(ctx.Req.URL.RequestURI(), nil) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin | ||||
| func reqRepoBranchWriter(ctx *context.APIContext) { | ||||
| 	options, ok := web.GetForm(ctx).(api.FileOptionInterface) | ||||
| @@ -419,6 +523,10 @@ func reqRepoBranchWriter(ctx *context.APIContext) { | ||||
| // reqRepoReader user should have specific read permission or be a repo admin or a site admin | ||||
| func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		if ctx.Repo.Repository == nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { | ||||
| 			ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") | ||||
| 			return | ||||
| @@ -555,6 +663,15 @@ func reqWebhooksEnabled() func(ctx *context.APIContext) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func reqProjectOwner() func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		if ctx.Repo.Repository == nil && ctx.ContextUser.IsIndividual() && ctx.ContextUser != ctx.Doer { | ||||
| 			ctx.Error(http.StatusForbidden, "", "must be the project owner") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func orgAssignment(args ...bool) func(ctx *context.APIContext) { | ||||
| 	var ( | ||||
| 		assignOrg  bool | ||||
| @@ -569,18 +686,6 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { | ||||
| 	return func(ctx *context.APIContext) { | ||||
| 		ctx.Org = new(context.APIOrganization) | ||||
|  | ||||
| 		if ctx.ContextUser == nil { | ||||
| 			if ctx.Org.Organization == nil { | ||||
| 				getOrganizationByParams(ctx) | ||||
| 				ctx.ContextUser = ctx.Org.Organization.AsUser() | ||||
| 			} | ||||
| 		} else if ctx.ContextUser.IsOrganization() { | ||||
| 			if ctx.Org == nil { | ||||
| 				ctx.Org = &context.APIOrganization{} | ||||
| 			} | ||||
| 			ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		if assignOrg { | ||||
| 			getOrganizationByParams(ctx) | ||||
| @@ -639,6 +744,12 @@ func mustEnableRepoProjects(ctx *context.APIContext) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getAuthenticatedUser(ctx *context.APIContext) { | ||||
| 	if ctx.IsSigned { | ||||
| 		ctx.ContextUser = ctx.Doer | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func mustEnableIssues(ctx *context.APIContext) { | ||||
| 	if !ctx.Repo.CanRead(unit.TypeIssues) { | ||||
| 		if log.IsTrace() { | ||||
| @@ -719,6 +830,10 @@ func mustEnableWiki(ctx *context.APIContext) { | ||||
| } | ||||
|  | ||||
| func mustNotBeArchived(ctx *context.APIContext) { | ||||
| 	if ctx.Repo.Repository == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ctx.Repo.Repository.IsArchived { | ||||
| 		ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) | ||||
| 		return | ||||
| @@ -1015,66 +1130,51 @@ func Routes() *web.Router { | ||||
| 			}, context.UserAssignmentAPI(), individualPermsChecker) | ||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)) | ||||
|  | ||||
| 		// Users (requires user scope) | ||||
| 		m.Group("/{username}/-", func() { | ||||
| 			m.Group("/projects", func() { | ||||
| 				m.Group("", func() { | ||||
| 					m.Get("", project_shared.ProjectHandler("org", project_shared.GetProjects)) | ||||
| 					m.Get("/{id}", project_shared.ProjectHandler("org", project_shared.GetProject)) | ||||
| 				}) | ||||
| 		// Projects | ||||
| 		m.Group("/orgs/{org}/projects", func() { | ||||
| 			m.Get("", reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true), org.GetProjects) | ||||
| 			m.Post("", reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), bind(api.CreateProjectOption{}), org.CreateProject) | ||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) | ||||
|  | ||||
| 		m.Group("/projects", func() { | ||||
| 			m.Group("/{project_id}", func() { | ||||
| 				m.Get("", project.GetProject) | ||||
| 				m.Get("/columns", project.GetProjectColumns) | ||||
|  | ||||
| 				m.Group("", func() { | ||||
| 					m.Post("", bind(api.CreateProjectOption{}), project_shared.ProjectHandler("org", project_shared.CreateProject)) | ||||
| 					m.Group("/{id}", func() { | ||||
| 						m.Post("", bind(api.CreateProjectColumnOption{}), project_shared.ProjectHandler("org", project_shared.AddColumnToProject)) | ||||
| 						m.Delete("", project_shared.ProjectHandler("org", project_shared.DeleteProject)) | ||||
| 						m.Put("", bind(api.EditProjectOption{}), 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{}), 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)) | ||||
| 						}) | ||||
| 					m.Patch("", bind(api.EditProjectOption{}), project.EditProject) | ||||
| 					m.Delete("", project.DeleteProject) | ||||
| 					m.Post("/{action:open|close}", project.ChangeProjectStatus) | ||||
| 					m.Group("/columns", func() { | ||||
| 						m.Post("", bind(api.CreateProjectColumnOption{}), project.AddColumnToProject) | ||||
| 						m.Patch("/move", project.MoveColumns) | ||||
| 						m.Patch("/{column_id}/move", project.MoveIssues) | ||||
| 					}) | ||||
| 				}, reqSelfOrAdmin(), reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true)) | ||||
| 			}, individualPermsChecker) | ||||
| 				}, reqRepoWriter(unit.TypeProjects), mustNotBeArchived, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), reqProjectOwner()) | ||||
| 			}) | ||||
|  | ||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), reqToken(), context.UserAssignmentAPI(), orgAssignment(), reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) | ||||
|  | ||||
| 		// Users (requires user scope) | ||||
| 		m.Group("/{username}/{reponame}", func() { | ||||
| 			m.Group("/projects", func() { | ||||
| 				m.Group("", func() { | ||||
| 					m.Get("", project_shared.ProjectHandler("repo", project_shared.GetProjects)) | ||||
| 					m.Get("/{id}", project_shared.ProjectHandler("repo", project_shared.GetProject)) | ||||
| 				}) | ||||
| 			m.Group("/columns/{column_id}", func() { | ||||
| 				m.Get("", project.GetProjectColumn) | ||||
|  | ||||
| 				m.Group("", func() { | ||||
| 					m.Post("", bind(api.CreateProjectOption{}), project_shared.ProjectHandler("repo", project_shared.CreateProject)) | ||||
| 					m.Group("/{id}", func() { | ||||
| 						m.Post("", bind(api.CreateProjectColumnOption{}), project_shared.ProjectHandler("repo", project_shared.AddColumnToProject)) | ||||
| 						m.Delete("", project_shared.ProjectHandler("repo", project_shared.DeleteProject)) | ||||
| 						m.Put("", bind(api.EditProjectOption{}), project_shared.ProjectHandler("repo", project_shared.EditProject)) | ||||
| 						m.Post("/move", project_shared.MoveColumns) | ||||
| 						m.Post("/{action:open|close}", project_shared.ChangeProjectStatus) | ||||
| 					m.Patch("", bind(api.EditProjectColumnOption{}), project.EditProjectColumn) | ||||
| 					m.Delete("", project.DeleteProjectColumn) | ||||
| 					m.Post("/default", project.SetDefaultProjectColumn) | ||||
| 				}, reqRepoWriter(unit.TypeProjects), mustNotBeArchived, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), reqProjectOwner()) | ||||
| 			}) | ||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), projectIDAssignmentAPI(), columnAssignment(), individualPermsChecker, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) | ||||
|  | ||||
| 						m.Group("/{columnID}", func() { | ||||
| 							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("/repos/{username}/{reponame}/projects", func() { | ||||
| 			m.Get("", repo.GetProjects) | ||||
| 			m.Group("", func() { | ||||
| 				m.Post("", bind(api.CreateProjectOption{}), repo.CreateProject) | ||||
| 				m.Put("/{type:issues|pulls}", reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.UpdateIssueProject) | ||||
| 			}, reqRepoWriter(unit.TypeProjects), mustNotBeArchived) | ||||
| 		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqToken(), repoAssignment(), individualPermsChecker, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects) | ||||
|  | ||||
| 			m.Group("/{type:issues|pulls}", func() { | ||||
| 				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) | ||||
| 		m.Post("/user/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), getAuthenticatedUser, reqSelfOrAdmin(), bind(api.CreateProjectOption{}), user.CreateProject) | ||||
|  | ||||
| 		m.Get("/users/{username}/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), context.UserAssignmentAPI(), individualPermsChecker, user.GetProjects) | ||||
|  | ||||
| 		// Users (requires user scope) | ||||
| 		m.Group("/users", func() { | ||||
|   | ||||
							
								
								
									
										114
									
								
								routers/api/v1/org/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								routers/api/v1/org/project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package org | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	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 for organization | ||||
| func CreateProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /orgs/{org}/projects project createProject | ||||
| 	// --- | ||||
| 	// summary: Create a new project | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: org | ||||
| 	//   in: path | ||||
| 	//   description: organization name that the project belongs to | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: Project data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/CreateProjectOption" | ||||
| 	// responses: | ||||
| 	//   "201": | ||||
| 	//     "$ref": "#/responses/Project" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "412": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	form := web.GetForm(ctx).(*api.CreateProjectOption) | ||||
|  | ||||
| 	project := &project_model.Project{ | ||||
| 		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.TypeOrganization, | ||||
| 		OwnerID:      ctx.ContextUser.ID, | ||||
| 	} | ||||
|  | ||||
| 	if err := project_model.NewProject(ctx, project); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "NewProject", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) | ||||
| } | ||||
|  | ||||
| // GetProjects returns a list of projects that belong to an organization | ||||
| func GetProjects(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /orgs/{org}/projects project getProjects | ||||
| 	// --- | ||||
| 	// summary: Get a list of projects | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: org | ||||
| 	//   in: path | ||||
| 	//   description: organization name that the project belongs to | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/ProjectList" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	sortType := ctx.FormTrim("sort") | ||||
|  | ||||
| 	isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" | ||||
|  | ||||
| 	searchOptions := project_model.SearchOptions{ | ||||
| 		IsClosed: optional.Some(isShowClosed), | ||||
| 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType), | ||||
| 		OwnerID:  ctx.ContextUser.ID, | ||||
| 		Type:     project_model.TypeOrganization, | ||||
| 	} | ||||
|  | ||||
| 	projects, err := db.Find[project_model.Project](ctx, &searchOptions) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("FindProjects", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) | ||||
| } | ||||
							
								
								
									
										208
									
								
								routers/api/v1/project/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								routers/api/v1/project/project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package project | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	project_model "code.gitea.io/gitea/models/project" | ||||
| 	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" | ||||
| ) | ||||
|  | ||||
| // GetProject returns a project | ||||
| func GetProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /projects/{project_id} project getProject | ||||
| 	// --- | ||||
| 	// summary: Get a project | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/Project" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		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 edits a project | ||||
| func EditProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /projects/{project_id} project editProject | ||||
| 	// --- | ||||
| 	// summary: Edit a project | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/Project" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "412": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	form := web.GetForm(ctx).(*api.EditProjectOption) | ||||
| 	projectID := ctx.PathParamInt64(":project_id") | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, projectID) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	project.Title = form.Title | ||||
| 	project.Description = form.Content | ||||
| 	project.CardType = project_model.CardType(form.CardType) | ||||
|  | ||||
| 	if err = project_model.UpdateProject(ctx, project); err != nil { | ||||
| 		ctx.ServerError("UpdateProjects", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToProject(ctx, project)) | ||||
| } | ||||
|  | ||||
| // DeleteProject deletes a project | ||||
| func DeleteProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /projects/{project_id} project deleteProject | ||||
| 	// --- | ||||
| 	// summary: Delete a project | ||||
| 	// description: Deletes a specific project for a given user and repository. | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     "$ref": "#/responses/empty" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	err := project_model.DeleteProjectByID(ctx, ctx.PathParamInt64(":project_id")) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("DeleteProjectByID", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| // ChangeProjectStatus updates the status of a project between "open" and "close" | ||||
| func ChangeProjectStatus(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /projects/{project_id}/{action} project changeProjectStatus | ||||
| 	// --- | ||||
| 	// summary: Change the status of a project | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// - name: action | ||||
| 	//   in: path | ||||
| 	//   description: action to perform (open or close) | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	//   enum: | ||||
| 	//   - open | ||||
| 	//   - close | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/Project" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	var toClose bool | ||||
| 	switch ctx.PathParam(":action") { | ||||
| 	case "open": | ||||
| 		toClose = false | ||||
| 	case "close": | ||||
| 		toClose = true | ||||
| 	default: | ||||
| 		ctx.NotFound("ChangeProjectStatus", nil) | ||||
| 		return | ||||
| 	} | ||||
| 	id := ctx.PathParamInt64(":project_id") | ||||
|  | ||||
| 	if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { | ||||
| 		ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, id) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToProject(ctx, project)) | ||||
| } | ||||
							
								
								
									
										425
									
								
								routers/api/v1/project/project_column.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										425
									
								
								routers/api/v1/project/project_column.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,425 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package project | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
|  | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	project_model "code.gitea.io/gitea/models/project" | ||||
| 	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" | ||||
| ) | ||||
|  | ||||
| // GetProjectColumn returns a project column | ||||
| func GetProjectColumn(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /projects/columns/{column_id} project getProject | ||||
| 	// --- | ||||
| 	// summary: Get a project column | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: column_id | ||||
| 	//   in: path | ||||
| 	//   description: column ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/Column" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToColumn(ctx, column)) | ||||
| } | ||||
|  | ||||
| // GetProjectColumns returns a list of project columns | ||||
| func GetProjectColumns(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /projects/{project_id}/columns project getProject | ||||
| 	// --- | ||||
| 	// summary: Get a list of project columns | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/ColumnList" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	columns, err := project.GetColumns(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetColumnsByProjectID", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToColumns(ctx, columns)) | ||||
| } | ||||
|  | ||||
| // AddColumnToProject adds a new column to a project | ||||
| func AddColumnToProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /projects/{project_id}/columns project addColumnToProject | ||||
| 	// --- | ||||
| 	// summary: Add a column to a project | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: column data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/CreateProjectColumnOption" | ||||
| 	// responses: | ||||
| 	//   "201": | ||||
| 	//     "$ref": "#/responses/Column" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "412": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	var project *project_model.Project | ||||
|  | ||||
| 	projectID := ctx.PathParamInt64(":project_id") | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, projectID) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	form := web.GetForm(ctx).(*api.CreateProjectColumnOption) | ||||
| 	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)) | ||||
| } | ||||
|  | ||||
| // EditProjectColumn edits a project column | ||||
| func EditProjectColumn(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /projects/columns/{column_id} project editProjectColumn | ||||
| 	// --- | ||||
| 	// summary: Edit a project column | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: column_id | ||||
| 	//   in: path | ||||
| 	//   description: column ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: column data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/EditProjectColumnOption" | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/Column" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "412": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	form := web.GetForm(ctx).(*api.EditProjectColumnOption) | ||||
| 	column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) | ||||
| 		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 deletes a project column | ||||
| func DeleteProjectColumn(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /projects/columns/{column_id} project deleteProjectColumn | ||||
| 	// --- | ||||
| 	// summary: Delete a project column | ||||
| 	// parameters: | ||||
| 	// - name: column_id | ||||
| 	//   in: path | ||||
| 	//   description: column ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     "$ref": "#/responses/empty" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64(":column_id")); err != nil { | ||||
| 		ctx.ServerError("DeleteProjectColumnByID", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| // SetDefaultProjectColumn set default column for issues/pulls | ||||
| func SetDefaultProjectColumn(ctx *context.APIContext) { | ||||
| 	// swagger:operation PUT /projects/columns/{column_id}/default project setDefaultProjectColumn | ||||
| 	// --- | ||||
| 	// summary: Set default column for issues/pulls | ||||
| 	// parameters: | ||||
| 	// - name: column_id | ||||
| 	//   in: path | ||||
| 	//   description: column ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     "$ref": "#/responses/empty" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := project_model.SetDefaultColumn(ctx, column.ProjectID, column.ID); err != nil { | ||||
| 		ctx.ServerError("SetDefaultColumn", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| // MoveColumns moves or keeps columns in a project and sorts them inside that project | ||||
| func MoveColumns(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /projects/{project_id}/columns/move project moveColumns | ||||
| 	// --- | ||||
| 	// summary: Move columns in a project | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: columns data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//    "$ref": "#/definitions/MovedColumnsOption" | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/ColumnList" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	form := &api.MovedColumnsOption{} | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	columns, err := project.GetColumns(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetColumns", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToColumns(ctx, columns)) | ||||
| } | ||||
|  | ||||
| // MoveIssues moves or keeps issues in a column and sorts them inside that column | ||||
| func MoveIssues(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /projects/{project_id}/columns/{column_id}/move project moveIssues | ||||
| 	// --- | ||||
| 	// summary: Move issues in a column | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: project_id | ||||
| 	//   in: path | ||||
| 	//   description: project ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// - name: column_id | ||||
| 	//   in: path | ||||
| 	//   description: column ID | ||||
| 	//   required: true | ||||
| 	//   type: integer | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: issues data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//    "$ref": "#/definitions/MovedIssuesOption" | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/IssueList" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":project_id")) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":column_id")) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	form := &api.MovedIssuesOption{} | ||||
| 	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, convert.ToAPIIssueList(ctx, ctx.Doer, movedIssues)) | ||||
| } | ||||
							
								
								
									
										242
									
								
								routers/api/v1/repo/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								routers/api/v1/repo/project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,242 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package repo | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"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/models/unit" | ||||
| 	"code.gitea.io/gitea/modules/optional" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/convert" | ||||
| ) | ||||
|  | ||||
| // GetProjects returns a list of projects for a given user and repository. | ||||
| func GetProjects(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{reponame}/projects project getProjects | ||||
| 	// --- | ||||
| 	// summary: Get a list of projects | ||||
| 	// description: Returns a list of projects for a given user and repository. | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the project | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: reponame | ||||
| 	//   in: path | ||||
| 	//   description: repository name. | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/ProjectList" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	sortType := ctx.FormTrim("sort") | ||||
|  | ||||
| 	isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" | ||||
|  | ||||
| 	searchOptions := project_model.SearchOptions{ | ||||
| 		IsClosed: optional.Some(isShowClosed), | ||||
| 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType), | ||||
| 		RepoID:   ctx.Repo.Repository.ID, | ||||
| 		Type:     project_model.TypeRepository, | ||||
| 	} | ||||
|  | ||||
| 	projects, err := db.Find[project_model.Project](ctx, &searchOptions) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("FindProjects", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) | ||||
| } | ||||
|  | ||||
| // CreateProject creates a new project | ||||
| func CreateProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /repos/{owner}/{reponame}/projects project createProject | ||||
| 	// --- | ||||
| 	// summary: Create a new project | ||||
| 	// description: Creates a new project for a given user and repository. | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the project | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: reponame | ||||
| 	//   in: path | ||||
| 	//   description: repository name. | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: Project data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/CreateProjectOption" | ||||
| 	// responses: | ||||
| 	//   "201": | ||||
| 	//     "$ref": "#/responses/Project" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "412": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	form := web.GetForm(ctx).(*api.CreateProjectOption) | ||||
|  | ||||
| 	project := &project_model.Project{ | ||||
| 		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, | ||||
| 		RepoID:       ctx.Repo.Repository.ID, | ||||
| 	} | ||||
|  | ||||
| 	if err := project_model.NewProject(ctx, project); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "NewProject", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) | ||||
| } | ||||
|  | ||||
| // UpdateIssueProject change an issue's project to another project in a repository | ||||
| func UpdateIssueProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation PUT /repos/{owner}/{reponame}/projects/{type} project updateIssueProject | ||||
| 	// --- | ||||
| 	// summary: Change an issue's project | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the project | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: reponame | ||||
| 	//   in: path | ||||
| 	//   description: repository name. | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: type | ||||
| 	//   in: path | ||||
| 	//   description: issue type (issues or pulls) | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	//   enum: | ||||
| 	//   - issues | ||||
| 	//   - pulls | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: issues data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//    "$ref": "#/definitions/UpdateIssuesOption" | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     "$ref": "#/responses/empty" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	form := &api.UpdateIssuesOption{} | ||||
|  | ||||
| 	if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { | ||||
| 		ctx.ServerError("DecodeMovedIssuesForm", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	issues := getActionIssues(ctx, form.Issues) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := issues.LoadProjects(ctx); err != nil { | ||||
| 		ctx.ServerError("LoadProjects", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if _, err := issues.LoadRepositories(ctx); err != nil { | ||||
| 		ctx.ServerError("LoadProjects", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	projectID := form.ProjectID | ||||
| 	for _, issue := range issues { | ||||
| 		if issue.Project != nil && issue.Project.ID == projectID { | ||||
| 			continue | ||||
| 		} | ||||
| 		if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { | ||||
| 			if errors.Is(err, util.ErrPermissionDenied) { | ||||
| 				continue | ||||
| 			} | ||||
| 			ctx.ServerError("IssueAssignOrRemoveProject", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| func getActionIssues(ctx *context.APIContext, issuesIDs []int64) issues_model.IssueList { | ||||
|  | ||||
| 	if len(issuesIDs) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	issues, err := issues_model.GetIssuesByIDs(ctx, issuesIDs) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetIssuesByIDs", err) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) | ||||
| 	prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) | ||||
| 	for _, issue := range issues { | ||||
| 		if issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 			ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect")) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled { | ||||
| 			ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if err = issue.LoadAttributes(ctx); err != nil { | ||||
| 			ctx.ServerError("LoadAttributes", err) | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	return issues | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -27,3 +27,10 @@ type swaggerResponseColumn struct { | ||||
| 	// in:body | ||||
| 	Body api.Column `json:"body"` | ||||
| } | ||||
|  | ||||
| // ColumnList | ||||
| // swagger:response ColumnList | ||||
| type swaggerResponseColumnList struct { | ||||
| 	// in:body | ||||
| 	Body []api.Column `json:"body"` | ||||
| } | ||||
|   | ||||
							
								
								
									
										109
									
								
								routers/api/v1/user/project.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								routers/api/v1/user/project.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package user | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	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 for a user | ||||
| func CreateProject(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /user/projects project createProject | ||||
| 	// --- | ||||
| 	// summary: Create a new project for user | ||||
| 	// consumes: | ||||
| 	// - application/json | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: body | ||||
| 	//   in: body | ||||
| 	//   description: Project data | ||||
| 	//   required: true | ||||
| 	//   schema: | ||||
| 	//     "$ref": "#/definitions/CreateProjectOption" | ||||
| 	// responses: | ||||
| 	//   "201": | ||||
| 	//     "$ref": "#/responses/Project" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "412": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	form := web.GetForm(ctx).(*api.CreateProjectOption) | ||||
|  | ||||
| 	project := &project_model.Project{ | ||||
| 		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.TypeIndividual, | ||||
| 		OwnerID:      ctx.ContextUser.ID, | ||||
| 	} | ||||
|  | ||||
| 	if err := project_model.NewProject(ctx, project); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "NewProject", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, convert.ToProject(ctx, project)) | ||||
| } | ||||
|  | ||||
| // GetProjects returns a list of projects that belong to a user | ||||
| func GetProjects(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /users/{username}/projects project getProjects | ||||
| 	// --- | ||||
| 	// summary: Get a list of projects | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: username | ||||
| 	//   in: path | ||||
| 	//   description: owner of the project | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/ProjectList" | ||||
| 	//   "403": | ||||
| 	//     "$ref": "#/responses/forbidden" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	//   "423": | ||||
| 	//     "$ref": "#/responses/repoArchivedError" | ||||
|  | ||||
| 	sortType := ctx.FormTrim("sort") | ||||
|  | ||||
| 	isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" | ||||
|  | ||||
| 	searchOptions := project_model.SearchOptions{ | ||||
| 		IsClosed: optional.Some(isShowClosed), | ||||
| 		OrderBy:  project_model.GetSearchOrderByBySortType(sortType), | ||||
| 		OwnerID:  ctx.ContextUser.ID, | ||||
| 		Type:     project_model.TypeIndividual, | ||||
| 	} | ||||
|  | ||||
| 	projects, err := db.Find[project_model.Project](ctx, &searchOptions) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("FindProjects", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, convert.ToProjects(ctx, projects)) | ||||
| } | ||||
							
								
								
									
										1539
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1539
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user