diff --git a/models/project/column.go b/models/project/column.go index 9b9d874997e..319eb3df747 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -34,6 +34,28 @@ const ( CardTypeImagesAndText ) +func (p CardType) ToString() string { + switch p { + case CardTypeImagesAndText: + return "ImagesAndText" + case CardTypeTextOnly: + fallthrough + default: + return "TextOnly" + } +} + +func ToCardType(s string) CardType { + switch s { + case "ImagesAndText": + return CardTypeImagesAndText + case "TextOnly": + fallthrough + default: + return CardTypeTextOnly + } +} + // ColumnColorPattern is a regexp witch can validate ColumnColor var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") diff --git a/models/project/template.go b/models/project/template.go index 06d5d2af148..9846a334639 100644 --- a/models/project/template.go +++ b/models/project/template.go @@ -25,6 +25,31 @@ const ( TemplateTypeBugTriage ) +func (p TemplateType) ToString() string { + switch p { + case TemplateTypeBasicKanban: + return "BasicKanban" + case TemplateTypeBugTriage: + return "BugTriage" + case TemplateTypeNone: + fallthrough + default: + return "" + } +} + +// ToTemplateType converts a string to a TemplateType +func ToTemplateType(s string) TemplateType { + switch s { + case "BasicKanban": + return TemplateTypeBasicKanban + case "BugTriage": + return TemplateTypeBugTriage + default: + return TemplateTypeNone + } +} + // GetTemplateConfigs retrieves the template configs of configurations project columns could have func GetTemplateConfigs() []TemplateConfig { return []TemplateConfig{ diff --git a/modules/structs/project.go b/modules/structs/project.go index 26f23d6fd67..193fa3743a6 100644 --- a/modules/structs/project.go +++ b/modules/structs/project.go @@ -5,37 +5,53 @@ package structs import "time" +// NewProjectOption options when creating a new project // swagger:model -type NewProjectPayload struct { +type NewProjectOption struct { // required:true - Title string `json:"title" binding:"Required"` + // Keep compatibility with Github API to use "name" instead of "title" + Name string `json:"name" binding:"Required"` // required:true - BoardType uint8 `json:"board_type"` + // enum: , BasicKanban, BugTriage + // Note: this is the same as TemplateType in models/project/template.go + TemplateType string `json:"template_type"` // required:true - CardType uint8 `json:"card_type"` - Description string `json:"description"` + // enum: TextOnly, ImagesAndText + CardType string `json:"card_type"` + // Keep compatibility with Github API to use "body" instead of "description" + Body string `json:"body"` } +// UpdateProjectOption options when updating a project // swagger:model -type UpdateProjectPayload struct { +type UpdateProjectOption struct { // required:true - Title string `json:"title" binding:"Required"` - Description string `json:"description"` + // Keep compatibility with Github API to use "name" instead of "title" + Name string `json:"name" binding:"Required"` + // Keep compatibility with Github API to use "body" instead of "description" + Body string `json:"body"` } +// Project represents a project // swagger:model type Project struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - TemplateType uint8 `json:"board_type"` - IsClosed bool `json:"is_closed"` + ID int64 `json:"id"` + // Keep compatibility with Github API to use "name" instead of "title" + Name string `json:"name"` + // Keep compatibility with Github API to use "body" instead of "description" + Body string `json:"body"` + // required:true + // enum: , BasicKanban, BugTriage + // Note: this is the same as TemplateType in models/project/template.go + TemplateType string `json:"template_type"` + // enum: open, closed + State string `json:"state"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time Updated time.Time `json:"updated_at"` // swagger:strfmt date-time - Closed time.Time `json:"closed_at"` + Closed *time.Time `json:"closed_at"` Repo *RepositoryMeta `json:"repository"` Creator *User `json:"creator"` diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8023987e443..3eb5ece485d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1165,7 +1165,7 @@ func Routes() *web.Router { m.Group("/projects", func() { m.Get("", projects.ListUserProjects) - m.Post("", bind(api.NewProjectPayload{}), projects.CreateUserProject) + m.Post("", bind(api.NewProjectOption{}), projects.CreateUserProject) }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) @@ -1475,7 +1475,7 @@ func Routes() *web.Router { m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) m.Group("/projects", func() { - m.Post("", bind(api.NewProjectPayload{}), projects.CreateRepoProject) + m.Post("", bind(api.NewProjectOption{}), projects.CreateRepoProject) }) }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) @@ -1699,7 +1699,7 @@ func Routes() *web.Router { }, reqToken(), reqOrgOwnership()) m.Group("/projects", func() { - m.Post("", bind(api.NewProjectPayload{}), projects.CreateOrgProject) + m.Post("", bind(api.NewProjectOption{}), projects.CreateOrgProject) }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) m.Group("/teams/{teamid}", func() { diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 22264038e74..ab99d9f3c0b 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -16,20 +16,20 @@ import ( ) func innerCreateProject(ctx *context.APIContext, projectType project_model.Type) { - form := web.GetForm(ctx).(*api.NewProjectPayload) + form := web.GetForm(ctx).(*api.NewProjectOption) project := &project_model.Project{ - RepoID: 0, - OwnerID: ctx.Doer.ID, - Title: form.Title, - Description: form.Description, + Title: form.Name, + Description: form.Body, CreatorID: ctx.Doer.ID, - TemplateType: project_model.TemplateType(form.BoardType), + TemplateType: project_model.ToTemplateType(form.TemplateType), Type: projectType, } - if ctx.ContextUser != nil { - project.OwnerID = ctx.ContextUser.ID + if ctx.ContextUser == nil { + ctx.APIError(http.StatusForbidden, "Not authenticated") + return } + project.OwnerID = ctx.ContextUser.ID if projectType == project_model.TypeRepository { project.RepoID = ctx.Repo.Repository.ID @@ -67,7 +67,7 @@ func CreateUserProject(ctx *context.APIContext) { // - name: project // in: body // required: true - // schema: { "$ref": "#/definitions/NewProjectPayload" } + // schema: { "$ref": "#/definitions/NewProjectOption" } // responses: // "201": // "$ref": "#/responses/Project" @@ -95,7 +95,7 @@ func CreateOrgProject(ctx *context.APIContext) { // - name: project // in: body // required: true - // schema: { "$ref": "#/definitions/NewProjectPayload" } + // schema: { "$ref": "#/definitions/NewProjectOption" } // responses: // "201": // "$ref": "#/responses/Project" @@ -128,7 +128,7 @@ func CreateRepoProject(ctx *context.APIContext) { // - name: project // in: body // required: true - // schema: { "$ref": "#/definitions/NewProjectPayload" } + // schema: { "$ref": "#/definitions/NewProjectOption" } // responses: // "201": // "$ref": "#/responses/Project" @@ -158,7 +158,7 @@ func GetProject(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - project, err := project_model.GetProjectByID(ctx, ctx.FormInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.APIError(http.StatusNotFound, err) @@ -193,7 +193,7 @@ func UpdateProject(ctx *context.APIContext) { // - name: project // in: body // required: true - // schema: { "$ref": "#/definitions/UpdateProjectPayload" } + // schema: { "$ref": "#/definitions/UpdateProjectOption" } // responses: // "200": // "$ref": "#/responses/Project" @@ -201,7 +201,7 @@ func UpdateProject(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - form := web.GetForm(ctx).(*api.UpdateProjectPayload) + form := web.GetForm(ctx).(*api.UpdateProjectOption) project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { @@ -211,11 +211,11 @@ func UpdateProject(ctx *context.APIContext) { } return } - if project.Title != form.Title { - project.Title = form.Title + if project.Title != form.Name { + project.Title = form.Name } - if project.Description != form.Description { - project.Description = form.Description + if project.Description != form.Body { + project.Description = form.Body } err = project_model.UpdateProject(ctx, project) @@ -249,7 +249,7 @@ func DeleteProject(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := project_model.DeleteProjectByID(ctx, ctx.FormInt64(":id")); err != nil { + if err := project_model.DeleteProjectByID(ctx, ctx.FormInt64("id")); err != nil { ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index cf2dc951153..19af6d82595 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -221,9 +221,11 @@ type swaggerParameterBodies struct { UpdateVariableOption api.UpdateVariableOption // in:body - NewProjectPayload api.NewProjectPayload + LockIssueOption api.LockIssueOption // in:body - UpdateProjectPayload api.UpdateProjectPayload - LockIssueOption api.LockIssueOption + NewProjectOption api.NewProjectOption + + // in:body + UpdateProjectOption api.UpdateProjectOption } diff --git a/services/convert/project.go b/services/convert/project.go index 1cb00696abd..94215a62e0d 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -8,17 +8,21 @@ import ( project_model "code.gitea.io/gitea/models/project" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Project, error) { apiProject := &api.Project{ - Title: project.Title, - Description: project.Description, - TemplateType: uint8(project.TemplateType), - IsClosed: project.IsClosed, + Name: project.Title, + Body: project.Description, + TemplateType: project.TemplateType.ToString(), + State: util.Iif(project.IsClosed, "closed", "open"), Created: project.CreatedUnix.AsTime(), Updated: project.UpdatedUnix.AsTime(), - Closed: project.ClosedDateUnix.AsTime(), + } + if !project.ClosedDateUnix.IsZero() { + tm := project.ClosedDateUnix.AsTime() + apiProject.Closed = &tm } if err := project.LoadRepo(ctx); err != nil { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e4a86d3895a..658f0a2519b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -26383,28 +26383,36 @@ "NewProjectPayload": { "type": "object", "required": [ - "title", - "board_type", + "name", + "template_type", "card_type" ], "properties": { - "board_type": { - "type": "integer", - "format": "uint8", - "x-go-name": "BoardType" - }, - "card_type": { - "type": "integer", - "format": "uint8", - "x-go-name": "CardType" - }, - "description": { + "body": { + "description": "Keep compatibility with Github API to use \"body\" instead of \"description\"", "type": "string", "x-go-name": "Description" }, - "title": { + "card_type": { + "type": "string", + "enum": [ + "TextOnly", + " ImagesAndText" + ], + "x-go-name": "CardType" + }, + "name": { "type": "string", "x-go-name": "Title" + }, + "template_type": { + "type": "string", + "enum": [ + "", + " BasicKanban", + " BugTriage" + ], + "x-go-name": "TemplateType" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -26983,11 +26991,14 @@ }, "Project": { "type": "object", + "required": [ + "template_type" + ], "properties": { - "board_type": { - "type": "integer", - "format": "uint8", - "x-go-name": "TemplateType" + "body": { + "description": "Keep compatibility with Github API to use \"body\" instead of \"description\"", + "type": "string", + "x-go-name": "Description" }, "closed_at": { "type": "string", @@ -27002,18 +27013,15 @@ "creator": { "$ref": "#/definitions/User" }, - "description": { - "type": "string", - "x-go-name": "Description" - }, "id": { "type": "integer", "format": "int64", "x-go-name": "ID" }, - "is_closed": { - "type": "boolean", - "x-go-name": "IsClosed" + "name": { + "description": "Keep compatibility with Github API to use \"name\" instead of \"title\"", + "type": "string", + "x-go-name": "Title" }, "owner": { "$ref": "#/definitions/User" @@ -27021,9 +27029,22 @@ "repository": { "$ref": "#/definitions/RepositoryMeta" }, - "title": { + "state": { "type": "string", - "x-go-name": "Title" + "enum": [ + "open", + " closed" + ], + "x-go-name": "State" + }, + "template_type": { + "type": "string", + "enum": [ + "", + " BasicKanban", + " BugTriage" + ], + "x-go-name": "TemplateType" }, "updated_at": { "type": "string", @@ -28590,14 +28611,15 @@ "UpdateProjectPayload": { "type": "object", "required": [ - "title" + "name" ], "properties": { - "description": { + "body": { + "description": "Keep compatibility with Github API to use \"body\" instead of \"description\"", "type": "string", "x-go-name": "Description" }, - "title": { + "name": { "type": "string", "x-go-name": "Title" } @@ -30150,10 +30172,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/LockIssueOption" - }, - "headers": { - "LockIssueOption": {} + "$ref": "#/definitions/UpdateProjectPayload" } }, "redirect": { diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 740610050a4..24115278af7 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -20,67 +20,70 @@ import ( func TestAPICreateUserProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban) + const title, description = "project_name", "project_description" + templateType := project_model.TemplateTypeBasicKanban.ToString() token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteUser) - req := NewRequestWithJSON(t, "POST", "/api/v1/user/projects", &api.NewProjectPayload{ - Title: title, - Description: description, - BoardType: boardType, + req := NewRequestWithJSON(t, "POST", "/api/v1/user/projects", &api.NewProjectOption{ + Name: title, + Body: description, + TemplateType: templateType, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) - assert.Equal(t, title, apiProject.Title) - assert.Equal(t, description, apiProject.Description) - assert.Equal(t, boardType, apiProject.TemplateType) + assert.Equal(t, title, apiProject.Name) + assert.Equal(t, description, apiProject.Body) + assert.Equal(t, templateType, apiProject.TemplateType) assert.Equal(t, "user2", apiProject.Creator.UserName) } func TestAPICreateOrgProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban) + const title, description = "project_name", "project_description" + templateType := project_model.TemplateTypeBasicKanban.ToString() orgName := "org17" token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) urlStr := fmt.Sprintf("/api/v1/orgs/%s/projects", orgName) - req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ - Title: title, - Description: description, - BoardType: boardType, + req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectOption{ + Name: title, + Body: description, + TemplateType: templateType, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) - assert.Equal(t, title, apiProject.Title) - assert.Equal(t, description, apiProject.Description) - assert.Equal(t, boardType, apiProject.TemplateType) + assert.Equal(t, title, apiProject.Name) + assert.Equal(t, description, apiProject.Body) + assert.Equal(t, templateType, apiProject.TemplateType) assert.Equal(t, "user2", apiProject.Creator.UserName) assert.Equal(t, "org17", apiProject.Owner.UserName) } func TestAPICreateRepoProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban) + const title, description = "project_name", "project_description" + templateType := project_model.TemplateTypeBasicKanban.ToString() ownerName := "user2" repoName := "repo1" token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName) - req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ - Title: title, - Description: description, - BoardType: boardType, + req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectOption{ + Name: title, + Body: description, + TemplateType: templateType, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) - assert.Equal(t, title, apiProject.Title) - assert.Equal(t, description, apiProject.Description) - assert.Equal(t, boardType, apiProject.TemplateType) + assert.Equal(t, title, apiProject.Name) + assert.Equal(t, description, apiProject.Body) + assert.Equal(t, templateType, apiProject.TemplateType) assert.Equal(t, "repo1", apiProject.Repo.Name) } @@ -139,7 +142,7 @@ func TestAPIGetProject(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiProject) - assert.Equal(t, "First project", apiProject.Title) + assert.Equal(t, "First project", apiProject.Name) assert.Equal(t, "repo1", apiProject.Repo.Name) assert.Equal(t, "user2", apiProject.Creator.UserName) } @@ -149,13 +152,13 @@ func TestAPIUpdateProject(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) - req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectPayload{Title: "First project updated"}).AddTokenAuth(token) + req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectOption{Name: "First project updated"}).AddTokenAuth(token) var apiProject *api.Project resp := MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiProject) - assert.Equal(t, "First project updated", apiProject.Title) + assert.Equal(t, "First project updated", apiProject.Name) } func TestAPIDeleteProject(t *testing.T) {