diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml index 1bf8030f6aa57..31268372770e1 100644 --- a/models/fixtures/project.yml +++ b/models/fixtures/project.yml @@ -45,3 +45,15 @@ type: 2 created_unix: 1688973000 updated_unix: 1688973000 + +- + id: 5 + title: project on org17 + owner_id: 17 + repo_id: 0 + is_closed: false + creator_id: 1 + board_type: 1 + type: 3 + created_unix: 1688973000 + updated_unix: 1688973000 diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472c51..de26b5d438ec2 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -8,11 +8,12 @@ import ( "fmt" "regexp" + "xorm.io/builder" + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - - "xorm.io/builder" ) type ( @@ -56,8 +57,10 @@ type Board struct { Sorting int8 `xorm:"NOT NULL DEFAULT 0"` Color string `xorm:"VARCHAR(7)"` - ProjectID int64 `xorm:"INDEX NOT NULL"` - CreatorID int64 `xorm:"NOT NULL"` + ProjectID int64 `xorm:"INDEX NOT NULL"` + Project *Project `xorm:"-"` + CreatorID int64 `xorm:"NOT NULL"` + Creator *user_model.User `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -244,6 +247,22 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { return append([]*Board{defaultB}, boards...), nil } +func (p *Project) GetBoardsAndCount(ctx context.Context, projectID int64) (BoardList, int64, error) { + engine := db.GetEngine(ctx) + boards := make([]*Board, 0, 10) + + defaultB, err := p.getDefaultBoard(ctx) + if err != nil { + return nil, 0, err + } + + count, err := engine.Where("project_id=? AND `default`=?", projectID, false).OrderBy("Sorting").FindAndCount(&boards) + if err != nil { + return nil, 0, err + } + return append([]*Board{defaultB}, boards...), count + 1, nil +} + // getDefaultBoard return default board and create a dummy if none exist func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { var board Board @@ -294,3 +313,60 @@ func UpdateBoardSorting(ctx context.Context, bs BoardList) error { } return nil } + +// LoadBoardCreator load creator of project board. +func (b *Board) LoadBoardCreator(ctx context.Context) (err error) { + if b.Creator == nil { + b.Creator, err = user_model.GetUserByID(ctx, b.CreatorID) + if err != nil { + return fmt.Errorf("getUserByID [%d]: %v", b.CreatorID, err) + } + } + return nil +} + +// LoadProject load project of project board. +func (b *Board) LoadProject(ctx context.Context) (err error) { + if b.Project == nil { + b.Project, err = GetProjectByID(ctx, b.ProjectID) + if err != nil { + return fmt.Errorf("getProjectByID [%d]: %v", b.CreatorID, err) + } + } + return nil +} + +// LoadAttributes load projects and creators of project boards. +func (bl BoardList) LoadAttributes(ctx context.Context) (err error) { + creators := make(map[int64]*user_model.User) + projects := make(map[int64]*Project) + var ok bool + + for i := range bl { + if bl[i].Project == nil { + bl[i].Project, ok = projects[bl[i].ProjectID] + if !ok { + project, err := GetProjectByID(ctx, bl[i].ProjectID) + if err != nil { + return fmt.Errorf("getProjectByID [%d]: %v", bl[i].ProjectID, err) + } + bl[i].Project = project + projects[bl[i].ProjectID] = project + } + } + + if bl[i].Creator == nil { + bl[i].Creator, ok = creators[bl[i].CreatorID] + if !ok { + creator, err := user_model.GetUserByID(ctx, bl[i].CreatorID) + if err != nil { + return fmt.Errorf("getUserByID [%d]: %v", bl[i].CreatorID, err) + } + bl[i].Creator = creator + creators[bl[i].CreatorID] = creator + } + } + } + + return nil +} diff --git a/models/project/project.go b/models/project/project.go index 3a1bfe1dbd3ff..05aa3141c82dc 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "xorm.io/builder" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -14,8 +16,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - - "xorm.io/builder" ) type ( @@ -85,6 +85,9 @@ func (err ErrProjectBoardNotExist) Unwrap() error { return util.ErrNotExist } +// List is a list of projects +// type List []*Project + // Project represents a project board type Project struct { ID int64 `xorm:"pk autoincr"` @@ -95,6 +98,7 @@ type Project struct { RepoID int64 `xorm:"INDEX"` Repo *repo_model.Repository `xorm:"-"` CreatorID int64 `xorm:"NOT NULL"` + Creator *user_model.User `xorm:"-"` IsClosed bool `xorm:"INDEX"` BoardType BoardType CardType CardType @@ -428,6 +432,17 @@ func DeleteProjectByID(ctx context.Context, id int64) error { }) } +// LoadCreator load creator of the project. +func (p *Project) LoadCreator(ctx context.Context) (err error) { + if p.Creator == nil { + p.Creator, err = user_model.GetUserByID(ctx, p.CreatorID) + if err != nil { + return fmt.Errorf("getUserByID [%d]: %v", p.CreatorID, err) + } + } + return nil +} + func DeleteProjectByRepoID(ctx context.Context, repoID int64) error { switch { case setting.Database.Type.IsSQLite3(): diff --git a/models/project/project_list.go b/models/project/project_list.go new file mode 100644 index 0000000000000..0c4db04a5cd34 --- /dev/null +++ b/models/project/project_list.go @@ -0,0 +1,51 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package project + +import ( + "context" + "fmt" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" +) + +// List is a list of projects +type List []*Project + +// LoadAttributes load repos and creators of projects. +func (pl List) LoadAttributes(ctx context.Context) (err error) { + repos := make(map[int64]*repo_model.Repository) + creators := make(map[int64]*user_model.User) + var ok bool + + for i := range pl { + // Organization projects don't have a Repo assgined and the repo_id for it is 0 + // So lets make sure we handle that case as well + if pl[i].Repo == nil && pl[i].RepoID != 0 { + pl[i].Repo, ok = repos[pl[i].RepoID] + if !ok { + repo, err := repo_model.GetRepositoryByID(ctx, pl[i].RepoID) + if err != nil { + return fmt.Errorf("getRepositoryByID [%d]: %v", pl[i].RepoID, err) + } + pl[i].Repo = repo + repos[pl[i].RepoID] = repo + } + } + + if pl[i].Creator == nil { + pl[i].Creator, ok = creators[pl[i].CreatorID] + if !ok { + creator, err := user_model.GetUserByID(ctx, pl[i].CreatorID) + if err != nil { + return fmt.Errorf("getUserByID [%d]: %v", pl[i].CreatorID, err) + } + pl[i].Creator = creator + creators[pl[i].CreatorID] = creator + } + } + } + + return nil +} diff --git a/models/project/project_test.go b/models/project/project_test.go index 6b5bd5b371dd2..70b533c1d055a 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -37,8 +37,8 @@ func TestGetProjects(t *testing.T) { projects, _, err := FindProjects(db.DefaultContext, SearchOptions{RepoID: 1}) assert.NoError(t, err) - // 1 value for this repo exists in the fixtures - assert.Len(t, projects, 1) + // this repo has two projects in the fixtures + assert.Len(t, projects, 2) projects, _, err = FindProjects(db.DefaultContext, SearchOptions{RepoID: 3}) assert.NoError(t, err) diff --git a/modules/structs/project.go b/modules/structs/project.go new file mode 100644 index 0000000000000..917d080393ed8 --- /dev/null +++ b/modules/structs/project.go @@ -0,0 +1,70 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +import "time" + +// swagger:model +type NewProjectPayload struct { + // required:true + Title string `json:"title" binding:"Required"` + // required:true + BoardType uint8 `json:"board_type"` + // required:true + CardType uint8 `json:"card_type"` + Description string `json:"description"` +} + +// swagger:model +type UpdateProjectPayload struct { + // required:true + Title string `json:"title" binding:"Required"` + Description string `json:"description"` +} + +type Project struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + BoardType uint8 `json:"board_type"` + IsClosed bool `json:"is_closed"` + // 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"` + + Repo *RepositoryMeta `json:"repository"` + Creator *User `json:"creator"` +} + +type ProjectBoard struct { + ID int64 `json:"id"` + Title string `json:"title"` + Default bool `json:"default"` + Color string `json:"color"` + Sorting int8 `json:"sorting"` + Project *Project `json:"project"` + Creator *User `json:"creator"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` + // swagger:strfmt date-time + Updated time.Time `json:"updated_at"` +} + +// swagger:model +type NewProjectBoardPayload struct { + // required:true + Title string `json:"title"` + Default bool `json:"default"` + Color string `json:"color"` + Sorting int8 `json:"sorting"` +} + +// swagger:model +type UpdateProjectBoardPayload struct { + Title string `json:"title"` + Color string `json:"color"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index cadddb44c39ef..074999d0c5d7d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -68,6 +68,9 @@ import ( "net/http" "strings" + "gitea.com/go-chi/binding" + "github.com/go-chi/cors" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" @@ -88,18 +91,15 @@ 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/projects" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" + _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/auth" context_service "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" - - _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation - - "gitea.com/go-chi/binding" - "github.com/go-chi/cors" ) func sudo() func(ctx *context.APIContext) { @@ -996,6 +996,9 @@ func Routes() *web.Route { m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) m.Delete("", user.DeleteAvatar) }, reqToken()) + m.Combo("/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)). + Get(projects.ListUserProjects). + Post(bind(api.NewProjectPayload{}), projects.CreateUserProject) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) @@ -1363,6 +1366,9 @@ func Routes() *web.Route { Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone). Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) }) + m.Combo("/projects"). + Get(reqToken(), projects.ListRepositoryProjects). + Post(reqToken(), bind(api.NewProjectPayload{}), projects.CreateRepositoryProject) }, repoAssignment()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) @@ -1431,6 +1437,9 @@ func Routes() *web.Route { m.Delete("", org.DeleteAvatar) }, reqToken(), reqOrgOwnership()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) + m.Combo("/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)). + Get(reqToken(), reqOrgMembership(), projects.ListOrgProjects). + Post(reqToken(), reqOrgOwnership(), bind(api.NewProjectPayload{}), projects.CreateOrgProject) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). @@ -1496,6 +1505,13 @@ func Routes() *web.Route { m.Group("/topics", func() { m.Get("/search", repo.TopicSearch) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + + m.Group("/projects", func() { + m.Combo("/{id}"). + Get(projects.GetProject). + Put(bind(api.UpdateProjectPayload{}), projects.UpdateProject). + Delete(projects.DeleteProject) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken()) }, sudo()) return m diff --git a/routers/api/v1/projects/board.go b/routers/api/v1/projects/board.go new file mode 100644 index 0000000000000..45cb384fa4e47 --- /dev/null +++ b/routers/api/v1/projects/board.go @@ -0,0 +1,275 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +import ( + "net/http" + + perm "code.gitea.io/gitea/models/perm/access" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/convert" +) + +func GetProjectBoard(ctx *context.APIContext) { + // swagger:operation GET /projects/boards/{id} board boardGetProjectBoard + // --- + // summary: Get project board + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the board + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ProjectBoard" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if project_model.IsErrProjectBoardNotExist(err) { + ctx.Error(http.StatusNotFound, "GetProjectBoard", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetProjectBoard", err) + } + return + } + + board.LoadProject(ctx) + board.Project.LoadRepo(ctx) + permission, err := perm.GetUserRepoPermission(ctx, board.Project.Repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectBoard", err) + return + } + + if !permission.CanRead(unit.TypeProjects) { + ctx.Error(http.StatusUnauthorized, "GetProjectBoard", "board doesn't belong to repository") + return + } + + ctx.JSON(http.StatusOK, convert.ToAPIProjectBoard(board)) +} + +func UpdateProjectBoard(ctx *context.APIContext) { + // swagger:operation PATCH /projects/boards/{id} board boardUpdateProjectBoard + // --- + // summary: Update project board + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project board + // type: string + // required: true + // - name: board + // in: body + // required: true + // schema: { "$ref": "#/definitions/UpdateProjectBoardPayload" } + // responses: + // "200": + // "$ref": "#/responses/ProjectBoard" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.UpdateProjectBoardPayload) + + board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":id")) + if err != nil { + ctx.Error(http.StatusNotFound, "GetProjectBoard", err) + return + } + + board.LoadProject(ctx) + board.Project.LoadRepo(ctx) + permission, err := perm.GetUserRepoPermission(ctx, board.Project.Repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectBoard", err) + return + } + + if !permission.CanWrite(unit.TypeProjects) { + ctx.Error(http.StatusUnauthorized, "GetProjectBoard", "board doesn't belong to repository") + return + } + + board.Title = form.Title + if board.Color != form.Color { + board.Color = form.Color + } + + if err = project_model.UpdateBoard(ctx, board); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProjectBoard", err) + return + } + + board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":id")) + if err != nil { + ctx.Error(http.StatusNotFound, "GetProjectBoard", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToAPIProjectBoard(board)) +} + +func DeleteProjectBoard(ctx *context.APIContext) { + // swagger:operation DELETE /projects/boards/{id} board boardDeleteProjectBoard + // --- + // summary: Delete project board + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project board + // type: string + // required: true + // responses: + // "204": + // "description": "Project board deleted" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":id")) + if err != nil { + ctx.Error(http.StatusNotFound, "GetProjectBoard", err) + return + } + + board.LoadProject(ctx) + board.Project.LoadRepo(ctx) + permission, err := perm.GetUserRepoPermission(ctx, board.Project.Repo, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectBoard", err) + return + } + + if !permission.CanWrite(unit.TypeProjects) { + ctx.Error(http.StatusUnauthorized, "GetProjectBoard", "board doesn't belong to repository") + return + } + + if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":id")); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProjectBoard", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func ListProjectBoards(ctx *context.APIContext) { + // swagger:operation GET /projects/{id}/boards board boardGetProjectBoards + // --- + // summary: Get project boards + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectBoardList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + ctx.Error(http.StatusNotFound, "Boards", err) + return + } + boards, count, err := project.GetBoardsAndCount(ctx, ctx.ParamsInt64(":id")) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Boards", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiBoards, err := convert.ToAPIProjectBoardList(boards) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiBoards) +} + +func CreateProjectBoard(ctx *context.APIContext) { + // swagger:operation POST /projects/{id}/boards board boardCreateProjectBoard + // --- + // summary: Create project board + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // - name: board + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectBoardPayload" } + // responses: + // "201": + // "$ref": "#/responses/ProjectBoard" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.NewProjectBoardPayload) + + board := &project_model.Board{ + Title: form.Title, + Default: form.Default, + Sorting: form.Sorting, + Color: form.Color, + ProjectID: ctx.ParamsInt64(":id"), + CreatorID: ctx.Doer.ID, + } + + var err error + if err = project_model.NewBoard(ctx, board); err != nil { + ctx.Error(http.StatusInternalServerError, "CreateBoard", err) + return + } + + board, err = project_model.GetBoard(ctx, board.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetBoard", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIProjectBoard(board)) +} diff --git a/routers/api/v1/projects/issue.go b/routers/api/v1/projects/issue.go new file mode 100644 index 0000000000000..9850fcbd70454 --- /dev/null +++ b/routers/api/v1/projects/issue.go @@ -0,0 +1,4 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go new file mode 100644 index 0000000000000..8fe7f886ced59 --- /dev/null +++ b/routers/api/v1/projects/project.go @@ -0,0 +1,457 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +import ( + "net/http" + + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/convert" +) + +func GetProject(ctx *context.APIContext) { + // swagger:operation GET /projects/{id} project projectGetProject + // --- + // summary: Get project + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + project_id := ctx.ParamsInt64(":id") + project, err := project_model.GetProjectByID(ctx, project_id) + + if project.RepoID != ctx.Repo.Repository.ID { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + return + } + + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + } + return + } + ctx.JSON(http.StatusOK, convert.ToAPIProject(project)) +} + +func UpdateProject(ctx *context.APIContext) { + // swagger:operation PATCH /projects/{id} project projectUpdateProject + // --- + // summary: Update project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/UpdateProjectPayload" } + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.UpdateProjectPayload) + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + + if project.RepoID != ctx.Repo.Repository.ID { + ctx.Error(http.StatusInternalServerError, "UpdateProject", err) + return + } + + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "UpdateProject", err) + } + return + } + if form.Title != project.Title { + project.Title = form.Title + } + if form.Description != project.Description { + project.Description = form.Description + } + err = project_model.UpdateProject(ctx, project) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProject", err) + return + } + + ctx.JSON(http.StatusOK, project) +} + +func DeleteProject(ctx *context.APIContext) { + // swagger:operation DELETE /projects/{id} project projectDeleteProject + // --- + // summary: Delete project + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // responses: + // "204": + // "description": "Deleted the project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + project_id := ctx.ParamsInt64(":id") + project, err := project_model.GetProjectByID(ctx, project_id) + + if project.RepoID != ctx.Repo.Repository.ID { + ctx.Error(http.StatusInternalServerError, "DeleteProject", err) + return + } + + err = project_model.DeleteProjectByID(ctx, project_id) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProject", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateRepositoryProject(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/projects project projectCreateRepositoryProject + // --- + // summary: Create a repository project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of repo + // type: string + // required: true + // - name: repo + // in: path + // description: repo + // type: string + // required: true + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectPayload" } + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.NewProjectPayload) + project := &project_model.Project{ + RepoID: ctx.Repo.Repository.ID, + Title: form.Title, + Description: form.Description, + CreatorID: ctx.Doer.ID, + BoardType: project_model.BoardType(form.BoardType), + Type: project_model.TypeRepository, + } + + var err error + if err = project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + project, err = project_model.GetProjectByID(ctx, project.ID) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIProject(project)) +} + +func CreateUserProject(ctx *context.APIContext) { + // swagger:operation POST /user/projects project projectCreateUserProject + // --- + // summary: Create a user project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectPayload" } + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.NewProjectPayload) + project := &project_model.Project{ + RepoID: ctx.Repo.Repository.ID, + Title: form.Title, + Description: form.Description, + CreatorID: ctx.Doer.ID, + BoardType: project_model.BoardType(form.BoardType), + Type: project_model.TypeIndividual, + } + + var err error + if err = project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + project, err = project_model.GetProjectByID(ctx, project.ID) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIProject(project)) +} + +func CreateOrgProject(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/projects project projectCreateOrgProject + // --- + // summary: Create a organization project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: org + // in: path + // description: owner of repo + // type: string + // required: true + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectPayload" } + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.NewProjectPayload) + project := &project_model.Project{ + RepoID: ctx.Repo.Repository.ID, + Title: form.Title, + Description: form.Description, + CreatorID: ctx.Doer.ID, + BoardType: project_model.BoardType(form.BoardType), + Type: project_model.TypeOrganization, + } + + var err error + if err = project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + project, err = project_model.GetProjectByID(ctx, project.ID) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIProject(project)) +} + +func ListRepositoryProjects(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/projects project projectListRepositoryProjects + // --- + // summary: List repository projects + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repository + // type: string + // required: true + // - name: repo + // in: path + // description: repo + // type: string + // required: true + // - name: closed + // in: query + // description: include closed issues or not + // type: boolean + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + RepoID: ctx.Repo.Repository.ID, + Page: ctx.FormInt("page"), + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeRepository, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiProjects, err := convert.ToAPIProjectList(projects) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiProjects) +} + +func ListUserProjects(ctx *context.APIContext) { + // swagger:operation GET /user/projects project projectListUserProjects + // --- + // summary: List repository projects + // produces: + // - application/json + // parameters: + // - name: closed + // in: query + // description: include closed issues or not + // type: boolean + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + Page: ctx.FormInt("page"), + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeIndividual, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiProjects, err := convert.ToAPIProjectList(projects) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiProjects) +} + +func ListOrgProjects(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/projects project projectListOrgProjects + // --- + // summary: List repository projects + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: owner of the repository + // type: string + // required: true + // - name: closed + // in: query + // description: include closed issues or not + // type: boolean + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + Page: ctx.FormInt("page"), + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeOrganization, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiProjects, err := convert.ToAPIProjectList(projects) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, apiProjects) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 6f7859df62ed4..3309e3286a2b6 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -179,6 +179,18 @@ type swaggerParameterBodies struct { // in:body CreateWikiPageOptions api.CreateWikiPageOptions + // in:body + NewProjectPayload api.NewProjectPayload + + // in:body + UpdateProjectPayload api.UpdateProjectPayload + + // in:body + NewProjectBoardPayload api.NewProjectBoardPayload + + // in:body + UpdateProjectBoardPayload api.UpdateProjectBoardPayload + // in:body CreatePushMirrorOption api.CreatePushMirrorOption diff --git a/routers/api/v1/swagger/project.go b/routers/api/v1/swagger/project.go new file mode 100644 index 0000000000000..144571d4e41ce --- /dev/null +++ b/routers/api/v1/swagger/project.go @@ -0,0 +1,36 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package swagger + +import ( + api "code.gitea.io/gitea/modules/structs" +) + +// Project +// swagger:response Project +type swaggerProject struct { + // in:body + Body api.Project `json:"body"` +} + +// ProjectList +// swagger:response ProjectList +type swaggerProjectList struct { + // in:body + Body []api.Project `json:"body"` +} + +// ProjectBoard +// swagger:response ProjectBoard +type swaggerProjectBoard struct { + // in:body + Body api.ProjectBoard `json:"body"` +} + +// ProjectBoardList +// swagger:response ProjectBoardList +type swaggerProjectBoardList struct { + // in:body + Body []api.ProjectBoard `json:"body"` +} diff --git a/services/convert/board.go b/services/convert/board.go new file mode 100644 index 0000000000000..c21a41b064597 --- /dev/null +++ b/services/convert/board.go @@ -0,0 +1,56 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "code.gitea.io/gitea/models/db" + project_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" +) + +func ToAPIProjectBoard(board *project_model.Board) *api.ProjectBoard { + ctx := db.DefaultContext + + if err := board.LoadProject(ctx); err != nil { + return &api.ProjectBoard{} + } + if err := board.LoadBoardCreator(ctx); err != nil { + return &api.ProjectBoard{} + } + + apiProjectBoard := &api.ProjectBoard{ + ID: board.ID, + Title: board.Title, + Default: board.Default, + Color: board.Color, + Sorting: board.Sorting, + Created: board.CreatedUnix.AsTime(), + Updated: board.UpdatedUnix.AsTime(), + } + + apiProjectBoard.Project = &api.Project{ + ID: board.Project.ID, + Title: board.Project.Title, + Description: board.Project.Description, + } + + apiProjectBoard.Creator = &api.User{ + ID: board.Creator.ID, + UserName: board.Creator.Name, + FullName: board.Creator.FullName, + } + + return apiProjectBoard +} + +func ToAPIProjectBoardList(boards project_model.BoardList) ([]*api.ProjectBoard, error) { + if err := boards.LoadAttributes(db.DefaultContext); err != nil { + return nil, err + } + result := make([]*api.ProjectBoard, len(boards)) + for i := range boards { + result[i] = ToAPIProjectBoard(boards[i]) + } + return result, nil +} diff --git a/services/convert/project.go b/services/convert/project.go new file mode 100644 index 0000000000000..e193151332d7c --- /dev/null +++ b/services/convert/project.go @@ -0,0 +1,59 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "code.gitea.io/gitea/models/db" + project_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" +) + +func ToAPIProject(project *project_model.Project) *api.Project { + ctx := db.DefaultContext + + if err := project.LoadRepo(ctx); err != nil { + return &api.Project{} + } + if err := project.LoadCreator(ctx); err != nil { + return &api.Project{} + } + + apiProject := &api.Project{ + Title: project.Title, + Description: project.Description, + BoardType: uint8(project.BoardType), + IsClosed: project.IsClosed, + Created: project.CreatedUnix.AsTime(), + Updated: project.UpdatedUnix.AsTime(), + Closed: project.ClosedDateUnix.AsTime(), + } + + if project.Repo != nil { + apiProject.Repo = &api.RepositoryMeta{ + ID: project.Repo.ID, + Name: project.Repo.Name, + Owner: project.Repo.OwnerName, + FullName: project.Repo.FullName(), + } + } + + apiProject.Creator = &api.User{ + ID: project.Creator.ID, + UserName: project.Creator.Name, + FullName: project.Creator.FullName, + } + + return apiProject +} + +func ToAPIProjectList(projects project_model.List) ([]*api.Project, error) { + if err := projects.LoadAttributes(db.DefaultContext); err != nil { + return nil, err + } + result := make([]*api.Project, len(projects)) + for i := range projects { + result[i] = ToAPIProject(projects[i]) + } + return result, nil +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 75a45dc68ac56..97c3a52375aa2 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2311,6 +2311,97 @@ } } }, + "/orgs/{org}/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "List repository projects", + "operationId": "projectListOrgProjects", + "parameters": [ + { + "type": "string", + "description": "owner of the repository", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "include closed issues or not", + "name": "closed", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a organization project", + "operationId": "projectCreateOrgProject", + "parameters": [ + { + "type": "string", + "description": "owner of repo", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectPayload" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/public_members": { "get": { "produces": [ @@ -2912,6 +3003,294 @@ } } }, + "/projects/boards/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "board" + ], + "summary": "Get project board", + "operationId": "boardGetProjectBoard", + "parameters": [ + { + "type": "string", + "description": "id of the board", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectBoard" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "board" + ], + "summary": "Delete project board", + "operationId": "boardDeleteProjectBoard", + "parameters": [ + { + "type": "string", + "description": "id of the project board", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Project board deleted" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "board" + ], + "summary": "Update project board", + "operationId": "boardUpdateProjectBoard", + "parameters": [ + { + "type": "string", + "description": "id of the project board", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "board", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateProjectBoardPayload" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectBoard" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/projects/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get project", + "operationId": "projectGetProject", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "project" + ], + "summary": "Delete project", + "operationId": "projectDeleteProject", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Deleted the project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Update project", + "operationId": "projectUpdateProject", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateProjectPayload" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/projects/{id}/boards": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "board" + ], + "summary": "Get project boards", + "operationId": "boardGetProjectBoards", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectBoardList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "board" + ], + "summary": "Create project board", + "operationId": "boardCreateProjectBoard", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "board", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectBoardPayload" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/ProjectBoard" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/issues/search": { "get": { "produces": [ @@ -10121,21 +10500,126 @@ }, { "type": "string", - "description": "Status to mark notifications as. Defaults to read.", - "name": "to-status", - "in": "query" + "description": "Status to mark notifications as. Defaults to read.", + "name": "to-status", + "in": "query" + }, + { + "type": "string", + "format": "date-time", + "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", + "name": "last_read_at", + "in": "query" + } + ], + "responses": { + "205": { + "$ref": "#/responses/NotificationThreadList" + } + } + } + }, + "/repos/{owner}/{repo}/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "List repository projects", + "operationId": "projectListRepositoryProjects", + "parameters": [ + { + "type": "string", + "description": "owner of the repository", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "include closed issues or not", + "name": "closed", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a repository project", + "operationId": "projectCreateRepositoryProject", + "parameters": [ + { + "type": "string", + "description": "owner of repo", + "name": "owner", + "in": "path", + "required": true }, { "type": "string", - "format": "date-time", - "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", - "name": "last_read_at", - "in": "query" + "description": "repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectPayload" + } } ], "responses": { - "205": { - "$ref": "#/responses/NotificationThreadList" + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" } } } @@ -15481,6 +15965,83 @@ } } }, + "/user/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "List repository projects", + "operationId": "projectListUserProjects", + "parameters": [ + { + "type": "boolean", + "description": "include closed issues or not", + "name": "closed", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a user project", + "operationId": "projectCreateUserProject", + "parameters": [ + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectPayload" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/repos": { "get": { "produces": [ @@ -20744,6 +21305,55 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "NewProjectBoardPayload": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "default": { + "type": "boolean", + "x-go-name": "Default" + }, + "sorting": { + "type": "integer", + "format": "int8", + "x-go-name": "Sorting" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "NewProjectPayload": { + "type": "object", + "required": [ + "title", + "board_type" + ], + "properties": { + "board_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "BoardType" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "NodeInfo": { "description": "NodeInfo contains standardized way of exposing metadata about a server running one of the distributed social networks", "type": "object", @@ -21310,6 +21920,99 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Project": { + "type": "object", + "properties": { + "board_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "BoardType" + }, + "closed_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Closed" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "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" + }, + "repository": { + "$ref": "#/definitions/RepositoryMeta" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "ProjectBoard": { + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "creator": { + "$ref": "#/definitions/User" + }, + "default": { + "type": "boolean", + "x-go-name": "Default" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "project": { + "$ref": "#/definitions/Project" + }, + "sorting": { + "type": "integer", + "format": "int8", + "x-go-name": "Sorting" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "PublicKey": { "description": "PublicKey publickey is a user key to push code to repository", "type": "object", @@ -22687,6 +23390,37 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateProjectBoardPayload": { + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "UpdateProjectPayload": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateRepoAvatarOption": { "description": "UpdateRepoAvatarUserOption options when updating the repo avatar", "type": "object", @@ -23628,6 +24362,36 @@ } } }, + "Project": { + "description": "Project", + "schema": { + "$ref": "#/definitions/Project" + } + }, + "ProjectBoard": { + "description": "ProjectBoard", + "schema": { + "$ref": "#/definitions/ProjectBoard" + } + }, + "ProjectBoardList": { + "description": "ProjectBoardList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ProjectBoard" + } + } + }, + "ProjectList": { + "description": "ProjectList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Project" + } + } + }, "PublicKey": { "description": "PublicKey", "schema": { @@ -24112,4 +24876,4 @@ "TOTPHeader": [] } ] -} +} \ No newline at end of file diff --git a/tests/integration/api_project_board_test.go b/tests/integration/api_project_board_test.go new file mode 100644 index 0000000000000..946f34b60cf51 --- /dev/null +++ b/tests/integration/api_project_board_test.go @@ -0,0 +1,27 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integration + +import ( + "testing" +) + +func TestAPIListProjectBoads(t *testing.T) { +} + +func TestAPICreateProjectBoard(t *testing.T) { +} + +func TestAPIGetProjectBoard(t *testing.T) { +} + +func TestAPIGetProjectBoardReqPermission(t *testing.T) { +} + +func TestAPIUpdateProjectBoard(t *testing.T) { +} + +func TestAPIDeleteProjectBoard(t *testing.T) { +} diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go new file mode 100644 index 0000000000000..a45e345e491f5 --- /dev/null +++ b/tests/integration/api_project_test.go @@ -0,0 +1,64 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + "github.com/stretchr/testify/assert" +) + +func TestAPIListUserProjects(t *testing.T) { +} + +func TestAPIListOrgProjects(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}) + + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue) + link, _ := url.Parse(fmt.Sprintf("/api/v1/orgs/%s/projects", org.Name)) + + link.RawQuery = url.Values{"token": { token}}.Encode() + + req := NewRequest(t, "GET", link.String()) + var apiProjects []*api.Project + + resp := session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiProjects) + assert.Len(t, apiProjects, 1) + +} + +func TestAPIListRepoProjects(t *testing.T) { +} + +func TestAPICreateUserProject(t *testing.T) { +} + +func TestAPICreateOrgProject(t *testing.T) { +} + +func TestAPICreateRepoProject(t *testing.T) { +} + +func TestAPIGetProject(t *testing.T) { +} + +func TestAPIUpdateProject(t *testing.T) { +} + +func TestAPIDeleteProject(t *testing.T) { +}