Compare commits

...

60 Commits

Author SHA1 Message Date
dineshsalunke
7abd52b6bb
Merge d0f790068bd772447b1018d319d255ba20c68833 into 97fc87af89a03470999a1a68e293417e36bd9b52 2025-07-02 20:24:34 -05:00
GiteaBot
97fc87af89 [skip ci] Updated translations via Crowdin 2025-07-03 00:37:58 +00:00
silverwind
6fe5c4c4d9
Exclude devtest.ts from tailwindcss (#34935)
Fix this leftover from the typescript migration.
2025-07-02 18:00:16 -04:00
GiteaBot
dd1fd89185 [skip ci] Updated translations via Crowdin 2025-07-02 00:37:55 +00:00
Denys Konovalov
d0f790068b Merge remote-tracking branch 'upstream/main' into feat/api-projects 2025-06-26 22:13:08 +02:00
Denys Konovalov
20a697eefd Fix formatting and swagger 2025-06-26 22:11:29 +02:00
Dinesh Salunke
fbe7e8e994 refactor: create projects method for usr, org and repo 2025-06-24 07:40:33 +05:30
Dinesh Salunke
596dcc01b5 refactor: return ApiError instead of Error from projects methods 2025-06-24 07:40:08 +05:30
Dinesh Salunke
68ea867522 Merge branch 'main' of https://github.com/go-gitea/gitea into feat/api-projects 2025-06-24 06:55:48 +05:30
Dinesh Salunke
30027ac0bd refactor: update the board type enums 2024-09-23 07:50:11 +05:30
Dinesh Salunke
185a594d32 refactor: update the context 2024-09-23 07:49:48 +05:30
Dinesh Salunke
20e9c58599 Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-09-23 07:31:54 +05:30
Dinesh Salunke
eeca89efb8 refactor: handle errors when converting to APIProject 2024-09-22 12:00:14 +05:30
Dinesh Salunke
d067b1d34f refactor: add BoardType to TemplateType property 2024-09-22 11:59:41 +05:30
Dinesh Salunke
016aa727c0 refactor: add creator property to the project 2024-09-22 11:59:23 +05:30
Dinesh Salunke
7383e7bf32 refactor: rename BoardType to TemplateType 2024-09-22 11:59:05 +05:30
Dinesh Salunke
a7aabc5349 refactor: add the missing parameter bodies for project 2024-09-22 09:17:06 +05:30
Dinesh Salunke
b931054fbd Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-09-22 08:51:27 +05:30
Denys Konovalov
a2a01dfbe0
Merge branch 'main' into feat/api-projects 2024-02-05 21:38:45 +01:00
Denys Konovalov
23d969738f
Merge branch 'main' into feat/api-projects 2024-01-27 14:28:27 +01:00
dineshsalunke
0a0837f47b chore: update the swagger file 2024-01-26 11:15:37 +05:30
dineshsalunke
c5c41e3da5 Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-01-26 10:09:12 +05:30
dineshsalunke
24a302a7ad Merge branch 'feat/api-projects' of https://github.com/dineshsalunke/gitea into feat/api-projects 2024-01-26 10:08:27 +05:30
dineshsalunke
d4ea5a57d4 fix: use the page field from parameters 2024-01-26 10:08:24 +05:30
dineshsalunke
975d98d8f1 chore: remove redundant comment 2024-01-26 10:08:00 +05:30
dineshsalunke
927da10445 chore: swagger doc typo corrections 2024-01-26 10:07:47 +05:30
Denys Konovalov
6a18cbe6de
Merge branch 'main' into feat/api-projects 2024-01-25 21:21:34 +01:00
Denys Konovalov
e751d6c5fe
Merge branch 'feat/api-projects' of github.com:dineshsalunke/gitea into feat/api-projects 2024-01-22 13:43:39 +01:00
Denys Konovalov
4475c4f6fc
actually fix test! 2024-01-22 13:43:10 +01:00
dineshsalunke
fac47d4790 Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-01-22 09:42:42 +05:30
Denys Konovalov
74043f7b4c
fix project sort test 2024-01-21 15:48:45 +01:00
Denys Konovalov
58e56cd0c6
more formatting fixes 2024-01-21 15:39:55 +01:00
Denys Konovalov
24babd6ca1
Merge branch 'feat/api-projects' of github.com:dineshsalunke/gitea into feat/api-projects 2024-01-21 15:33:04 +01:00
Denys Konovalov
065001c081
more reverts 2024-01-21 15:32:42 +01:00
Denys Konovalov
c7440bcc1f
Merge branch 'main' into feat/api-projects 2024-01-21 15:30:04 +01:00
Denys Konovalov
897c67b555
revert formatting changes 2024-01-21 15:29:23 +01:00
Denys Konovalov
34566ea494
Merge branch 'main' into feat/api-projects 2024-01-21 15:00:49 +01:00
dineshsalunke
d2fd13836c test: use the correct fields for the test case 2024-01-20 10:18:05 +05:30
dineshsalunke
7eebc7c820 Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-01-20 09:49:00 +05:30
dineshsalunke
cf17f3456b Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-01-19 18:37:55 +05:30
dineshsalunke
3b2943a002 test: use the add token auth method for token 2024-01-17 23:20:02 +05:30
dineshsalunke
8819a0a4fc refactor: use the add token auth method for token in tests 2024-01-17 22:55:31 +05:30
dineshsalunke
6238b9e746 fix: ignore errors when loading data for converting project to response 2024-01-17 22:55:07 +05:30
dineshsalunke
fec855e323 chore: fix swagger documentation 2024-01-17 22:14:39 +05:30
dineshsalunke
dbfecced25 fix: return error when converting project to api project 2024-01-17 21:45:36 +05:30
dineshsalunke
852c0df382 chore: fix lint issues 2024-01-17 21:44:47 +05:30
dineshsalunke
81c3d0ce34 fix: add missing swagger tags 2024-01-17 20:16:37 +05:30
dineshsalunke
aa32ad74af refactor: update for db find method usage 2024-01-17 19:55:42 +05:30
dineshsalunke
c266568eea Merge remote-tracking branch 'upstream/main' into feat/api-projects 2024-01-17 18:58:29 +05:30
dineshsalunke
5bbbade026 chore: remove unwanted indentation on the line 2024-01-16 21:04:27 +05:30
dineshsalunke
df22b6dd12 chore: remove the goline formatting 2024-01-16 20:44:55 +05:30
Dinesh Salunke
ac39273432 Merge branch 'feat/api-projects' of https://github.com/dineshsalunke/gitea into feat/api-projects 2024-01-16 20:38:19 +05:30
Dinesh Salunke
a0f8dbd928 chore: remove unnecessary go lines formatting 2024-01-16 20:38:15 +05:30
Giteabot
e0ae281674
Merge branch 'main' into feat/api-projects 2024-01-16 04:18:28 +08:00
Dinesh Salunke
0b8d198b89 Merge branch 'feat/api-projects' of https://github.com/dineshsalunke/gitea into feat/api-projects 2023-11-21 00:34:43 +05:30
Dinesh Salunke
922cd13869 Merge remote-tracking branch 'upstream/main' into feat/api-projects 2023-11-21 00:34:32 +05:30
Dinesh Salunke
b6d45c6476 Merge branch 'main' into feat/api-projects 2023-11-20 10:12:10 +05:30
Dinesh Salunke
b6f9d05180 feat: api for projects 2023-11-18 18:32:22 +05:30
Dinesh Salunke
6092b81563 refactor: add structs for project 2023-11-18 18:31:43 +05:30
Dinesh Salunke
4c0ec221fa test: add 2 fixtures for project 2023-11-18 18:30:34 +05:30
13 changed files with 1259 additions and 5 deletions

View File

@ -91,6 +91,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"`
TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type
CardType CardType
@ -121,6 +122,14 @@ func (p *Project) LoadOwner(ctx context.Context) (err error) {
return err
}
func (p *Project) LoadCreator(ctx context.Context) (err error) {
if p.Creator != nil {
return nil
}
p.Creator, err = user_model.GetUserByID(ctx, p.CreatorID)
return err
}
func (p *Project) LoadRepo(ctx context.Context) (err error) {
if p.RepoID == 0 || p.Repo != nil {
return nil

View File

@ -0,0 +1,43 @@
// 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"`
}
// 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"`
// 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"`
Owner *User `json:"owner"`
}

View File

@ -1969,6 +1969,7 @@ pulls.cmd_instruction_checkout_title=Basculer
pulls.cmd_instruction_checkout_desc=Depuis votre dépôt, basculer sur une nouvelle branche et tester des modifications.
pulls.cmd_instruction_merge_title=Fusionner
pulls.cmd_instruction_merge_desc=Fusionner les modifications et mettre à jour sur Gitea.
pulls.cmd_instruction_merge_warning=Attention : cette opération ne peut pas fusionner la demande dajout car la « détection automatique de fusion manuelle » na pas été activée
pulls.clear_merge_message=Effacer le message de fusion
pulls.clear_merge_message_hint=Effacer le message de fusion ne supprimera que le message de la révision, mais pas les pieds de révision générés tels que "Co-Authored-By:".
@ -2768,6 +2769,8 @@ branch.new_branch_from=`Créer une nouvelle branche à partir de "%s"`
branch.renamed=La branche %s à été renommée en %s.
branch.rename_default_or_protected_branch_error=Seuls les administrateurs peuvent renommer les branches par défaut ou protégées.
branch.rename_protected_branch_failed=Cette branche est protégée par des règles de protection basées sur des globs.
branch.commits_divergence_from=Divergence de révisions : %[1]d en retard et %[2]d en avance sur %[3]s
branch.commits_no_divergence=Identique à la branche %[1]s
tag.create_tag=Créer l'étiquette %s
tag.create_tag_operation=Créer une étiquette

View File

@ -2769,6 +2769,8 @@ branch.new_branch_from=`Cruthaigh brainse nua ó "%s"`
branch.renamed=Ainmníodh brainse %s go %s.
branch.rename_default_or_protected_branch_error=Ní féidir ach le riarthóirí brainsí réamhshocraithe nó cosanta a athainmniú.
branch.rename_protected_branch_failed=Tá an brainse seo faoi chosaint ag rialacha cosanta domhanda.
branch.commits_divergence_from=Déanann sé dialltacht a thiomnú: %[1]d taobh thiar agus %[2]d chun tosaigh ar %[3]s
branch.commits_no_divergence=Mar an gcéanna le brainse %[1]s
tag.create_tag=Cruthaigh clib %s
tag.create_tag_operation=Cruthaigh clib
@ -2782,6 +2784,7 @@ topic.done=Déanta
topic.count_prompt=Ní féidir leat níos mó ná 25 topaicí a roghnú
topic.format_prompt=Ní mór do thopaicí tosú le litir nó uimhir, is féidir daiseanna ('-') agus poncanna ('.') a áireamh, a bheith suas le 35 carachtar ar fad. Ní mór litreacha a bheith i litreacha beaga.
find_file.follow_symlink=Lean an nasc siombalach seo go dtí an áit a bhfuil sé ag pointeáil air
find_file.go_to_file=Téigh go dtí an comhad
find_file.no_matching=Níl aon chomhad meaitseála le fáil

View File

@ -1562,8 +1562,8 @@ issues.filter_project=Planeamento
issues.filter_project_all=Todos os planeamentos
issues.filter_project_none=Nenhum planeamento
issues.filter_assignee=Encarregado
issues.filter_assignee_no_assignee=Não atribuído
issues.filter_assignee_any_assignee=Atribuído a qualquer pessoa
issues.filter_assignee_no_assignee=Não atribuída
issues.filter_assignee_any_assignee=Atribuída a alguém
issues.filter_poster=Autor(a)
issues.filter_user_placeholder=Procurar utilizadores
issues.filter_user_no_select=Todos os utilizadores
@ -1969,6 +1969,7 @@ pulls.cmd_instruction_checkout_title=Checkout
pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações.
pulls.cmd_instruction_merge_title=Integrar
pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea.
pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque a opção "auto-identificar integração manual" não está habilitada.
pulls.clear_merge_message=Apagar mensagem de integração
pulls.clear_merge_message_hint=Apagar a mensagem de integração apenas remove o conteúdo da mensagem de cometimento e mantém os rodapés do git, tais como "Co-Autorado-Por …".
@ -2768,6 +2769,8 @@ branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"`
branch.renamed=O ramo %s foi renomeado para %s.
branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos.
branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob.
branch.commits_divergence_from=Divergência nos cometimentos: %[1]d atrás e %[2]d à frente de %[3]s
branch.commits_no_divergence=Idêntico ao ramo %[1]s
tag.create_tag=Criar etiqueta %s
tag.create_tag_operation=Criar etiqueta
@ -2781,6 +2784,7 @@ topic.done=Concluído
topic.count_prompt=Não pode escolher mais do que 25 tópicos
topic.format_prompt=Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') ou pontos ('.') e podem ter até 35 caracteres. As letras têm que ser minúsculas.
find_file.follow_symlink=Seguir esta ligação simbólica para onde ela está apontando
find_file.go_to_file=Ir para o ficheiro
find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente

View File

@ -89,6 +89,7 @@ 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/user"
@ -1161,6 +1162,11 @@ func Routes() *web.Router {
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
m.Group("/projects", func() {
m.Get("", projects.ListUserProjects)
m.Post("", bind(api.NewProjectPayload{}), projects.CreateUserProject)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope)
@ -1467,6 +1473,10 @@ func Routes() *web.Router {
}, reqAdmin(), reqToken())
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)
})
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
@ -1599,7 +1609,7 @@ func Routes() *web.Router {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
}, repoAssignment(), checkTokenPublicOnly())
}, repoAssignment())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
@ -1687,6 +1697,10 @@ func Routes() *web.Router {
m.Delete("", org.UnblockUser)
})
}, reqToken(), reqOrgOwnership())
m.Group("/projects", func() {
m.Post("", bind(api.NewProjectPayload{}), projects.CreateOrgProject)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).

View File

@ -0,0 +1,422 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package projects
import (
"net/http"
"code.gitea.io/gitea/models/db"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/setting"
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"
)
func innerCreateProject(ctx *context.APIContext, projectType project_model.Type) {
form := web.GetForm(ctx).(*api.NewProjectPayload)
project := &project_model.Project{
RepoID: 0,
OwnerID: ctx.Doer.ID,
Title: form.Title,
Description: form.Description,
CreatorID: ctx.Doer.ID,
TemplateType: project_model.TemplateType(form.BoardType),
Type: projectType,
}
if ctx.ContextUser != nil {
project.OwnerID = ctx.ContextUser.ID
}
if projectType == project_model.TypeRepository {
project.RepoID = ctx.Repo.Repository.ID
}
if err := project_model.NewProject(ctx, project); err != nil {
ctx.APIErrorInternal(err)
return
}
project, err := project_model.GetProjectByID(ctx, project.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
projectResponse, err := convert.ToAPIProject(ctx, project)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, projectResponse)
}
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"
innerCreateProject(ctx, project_model.TypeIndividual)
}
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"
innerCreateProject(ctx, project_model.TypeOrganization)
}
func CreateRepoProject(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"
innerCreateProject(ctx, project_model.TypeRepository)
}
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, err := project_model.GetProjectByID(ctx, ctx.FormInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
projectResponse, err := convert.ToAPIProject(ctx, project)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, projectResponse)
}
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.FormInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if project.Title != form.Title {
project.Title = form.Title
}
if project.Description != form.Description {
project.Description = form.Description
}
err = project_model.UpdateProject(ctx, project)
if err != nil {
ctx.APIErrorInternal(err)
return
}
projectResponse, err := convert.ToAPIProject(ctx, project)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, projectResponse)
}
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"
if err := project_model.DeleteProjectByID(ctx, ctx.FormInt64(":id")); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func ListUserProjects(ctx *context.APIContext) {
// swagger:operation GET /user/projects project projectListUserProjects
// ---
// summary: List user projects
// produces:
// - application/json
// parameters:
// - name: closed
// in: query
// description: include closed projects 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 := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
Type: project_model.TypeIndividual,
IsClosed: ctx.FormOptionalBool("closed"),
OwnerID: ctx.Doer.ID,
ListOptions: db.ListOptions{Page: ctx.FormInt("page")},
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum)
ctx.SetTotalCountHeader(count)
apiProjects, err := convert.ToAPIProjectList(ctx, projects)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiProjects)
}
func ListOrgProjects(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/projects project projectListOrgProjects
// ---
// summary: List org 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 projects 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 := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
OwnerID: ctx.Org.Organization.AsUser().ID,
ListOptions: db.ListOptions{Page: ctx.FormInt("page")},
IsClosed: ctx.FormOptionalBool("closed"),
Type: project_model.TypeOrganization,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum)
ctx.SetTotalCountHeader(count)
apiProjects, err := convert.ToAPIProjectList(ctx, projects)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiProjects)
}
func ListRepoProjects(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 projects 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"
page := ctx.FormInt("page")
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
RepoID: ctx.Repo.Repository.ID,
IsClosed: ctx.FormOptionalBool("closed"),
Type: project_model.TypeRepository,
ListOptions: db.ListOptions{Page: page},
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), page)
ctx.SetTotalCountHeader(count)
apiProjects, err := convert.ToAPIProjectList(ctx, projects)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiProjects)
}

View File

@ -221,5 +221,9 @@ type swaggerParameterBodies struct {
UpdateVariableOption api.UpdateVariableOption
// in:body
LockIssueOption api.LockIssueOption
NewProjectPayload api.NewProjectPayload
// in:body
UpdateProjectPayload api.UpdateProjectPayload
LockIssueOption api.LockIssueOption
}

View File

@ -0,0 +1,22 @@
// Copyright 2023 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 swaggerResponseProject struct {
// in:body
Body api.Project `json:"body"`
}
// ProjectList
// swagger:response ProjectList
type swaggerResponseProjectList struct {
// in:body
Body []api.Project `json:"body"`
}

View File

@ -0,0 +1,74 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
project_model "code.gitea.io/gitea/models/project"
api "code.gitea.io/gitea/modules/structs"
)
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,
Created: project.CreatedUnix.AsTime(),
Updated: project.UpdatedUnix.AsTime(),
Closed: project.ClosedDateUnix.AsTime(),
}
if err := project.LoadRepo(ctx); err != nil {
return nil, err
}
if project.Repo != nil {
apiProject.Repo = &api.RepositoryMeta{
ID: project.RepoID,
Name: project.Repo.Name,
Owner: project.Repo.OwnerName,
FullName: project.Repo.FullName(),
}
}
if err := project.LoadCreator(ctx); err != nil {
return nil, err
}
if project.Creator != nil {
apiProject.Creator = &api.User{
ID: project.Creator.ID,
UserName: project.Creator.Name,
FullName: project.Creator.FullName,
}
}
if err := project.LoadOwner(ctx); err != nil {
return nil, err
}
if project.Owner != nil {
apiProject.Owner = &api.User{
ID: project.Owner.ID,
UserName: project.Owner.Name,
FullName: project.Owner.FullName,
}
}
return apiProject, nil
}
func ToAPIProjectList(ctx context.Context, projects []*project_model.Project) ([]*api.Project, error) {
result := make([]*api.Project, len(projects))
var err error
for i := range projects {
result[i], err = ToAPIProject(ctx, projects[i])
if err != nil {
break
}
}
if err != nil {
return nil, err
}
return result, nil
}

View File

@ -29,7 +29,7 @@ export default {
important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
content: [
isProduction && '!./templates/devtest/**/*',
isProduction && '!./web_src/js/standalone/devtest.js',
isProduction && '!./web_src/js/standalone/devtest.ts',
'!./templates/swagger/v1_json.tmpl',
'!./templates/user/auth/oidc_wellknown.tmpl',
'!**/*_test.go',

View File

@ -3311,6 +3311,94 @@
}
}
},
"/orgs/{org}/projects": {
"get": {
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "List org projects",
"operationId": "projectListOrgProjects",
"parameters": [
{
"type": "string",
"description": "owner of the repository",
"name": "org",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "include closed projects 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"
}
}
}
},
"/orgs/{org}/public_members": {
"get": {
"produces": [
@ -4137,6 +4225,106 @@
}
}
},
"/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"
}
}
}
},
"/repos/issues/search": {
"get": {
"produces": [
@ -13411,6 +13599,111 @@
}
}
},
"/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 projects 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",
"description": "repo",
"name": "repo",
"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"
}
}
}
},
"/repos/{owner}/{repo}/pulls": {
"get": {
"produces": [
@ -19661,6 +19954,83 @@
}
}
},
"/user/projects": {
"get": {
"produces": [
"application/json"
],
"tags": [
"project"
],
"summary": "List user projects",
"operationId": "projectListUserProjects",
"parameters": [
{
"type": "boolean",
"description": "include closed projects 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": [
@ -26006,6 +26376,35 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"NewProjectPayload": {
"type": "object",
"required": [
"title",
"board_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": {
"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",
@ -26578,6 +26977,58 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Project": {
"type": "object",
"properties": {
"board_type": {
"type": "integer",
"format": "uint8",
"x-go-name": "TemplateType"
},
"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"
},
"owner": {
"$ref": "#/definitions/User"
},
"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"
},
"PublicKey": {
"description": "PublicKey publickey is a user key to push code to repository",
"type": "object",
@ -28132,6 +28583,23 @@
},
"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",
@ -29202,6 +29670,21 @@
}
}
},
"Project": {
"description": "Project",
"schema": {
"$ref": "#/definitions/Project"
}
},
"ProjectList": {
"description": "ProjectList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Project"
}
}
},
"PublicKey": {
"description": "PublicKey",
"schema": {
@ -29664,6 +30147,9 @@
"description": "parameterBodies",
"schema": {
"$ref": "#/definitions/LockIssueOption"
},
"headers": {
"LockIssueOption": {}
}
},
"redirect": {

View File

@ -0,0 +1,170 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/unittest"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPICreateUserProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban)
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,
}).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, "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)
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,
}).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, "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)
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,
}).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, "repo1", apiProject.Repo.Name)
}
func TestAPIListUserProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue)
link, _ := url.Parse("/api/v1/user/projects")
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
var apiProjects []*api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProjects)
assert.Len(t, apiProjects, 1)
}
func TestAPIListOrgProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
orgName := "org17"
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue)
link, _ := url.Parse(fmt.Sprintf("/api/v1/orgs/%s/projects", orgName))
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
var apiProjects []*api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProjects)
assert.Len(t, apiProjects, 1)
}
func TestAPIListRepoProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ownerName := "user2"
repoName := "repo1"
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadIssue)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName))
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
var apiProjects []*api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProjects)
assert.Len(t, apiProjects, 1)
}
func TestAPIGetProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue)
link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1))
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
var apiProject *api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, "First project", apiProject.Title)
assert.Equal(t, "repo1", apiProject.Repo.Name)
assert.Equal(t, "user2", apiProject.Creator.UserName)
}
func TestAPIUpdateProject(t *testing.T) {
defer tests.PrepareTestEnv(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)
var apiProject *api.Project
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiProject)
assert.Equal(t, "First project updated", apiProject.Title)
}
func TestAPIDeleteProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue)
link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1))
req := NewRequest(t, "DELETE", link.String()).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &project_model.Project{ID: 1})
}