feat: more crud for kanban
runner nix smoke / nix label and flake smoke (push) Failing after 2m6s

This commit is contained in:
2026-09-28 00:08:44 +00:00
parent 335a74010d
commit 0cbf267474
10 changed files with 639 additions and 24 deletions
+6 -6
View File
@@ -1599,8 +1599,8 @@ func Routes() *web.Router {
})
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
Patch(reqToken(), mustNotBeArchived, bind(api.EditIssueOption{}), repo.EditIssue).
Delete(reqToken(), mustNotBeArchived, reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
m.Combo("/assignees").
Post(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
Delete(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
@@ -1614,10 +1614,10 @@ func Routes() *web.Router {
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
Delete(reqToken(), repo.ClearIssueLabels)
m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
Post(reqToken(), mustNotBeArchived, bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
Put(reqToken(), mustNotBeArchived, bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
Delete(reqToken(), mustNotBeArchived, repo.ClearIssueLabels)
m.Delete("/{id}", reqToken(), mustNotBeArchived, repo.DeleteIssueLabel)
})
m.Group("/times", func() {
m.Combo("").
@@ -15,6 +15,7 @@ import (
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
project_model "gitea.dev/models/project"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
@@ -529,6 +530,38 @@ func GetIssue(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
func validateProjectAssignments(ctx *context.APIContext, projectIDs []int64) (int, string) {
if unit.TypeProjects.UnitGlobalDisabled() || !ctx.Repo.Permission.CanWrite(unit.TypeProjects) {
return http.StatusForbidden, "project write permission is required"
}
projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
if !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
return http.StatusNotFound, ""
}
if len(projectIDs) == 0 {
return 0, ""
}
projects, err := project_model.GetProjectsMapByIDs(ctx, projectIDs)
if err != nil {
return http.StatusInternalServerError, err.Error()
}
seen := make(map[int64]struct{}, len(projectIDs))
for _, projectID := range projectIDs {
if _, duplicate := seen[projectID]; duplicate {
return http.StatusBadRequest, fmt.Sprintf("project %d is assigned more than once", projectID)
}
seen[projectID] = struct{}{}
project, ok := projects[projectID]
if !ok || !project.CanBeAccessedByOwnerRepo(ctx.Repo.Repository.OwnerID, ctx.Repo.Repository) {
return http.StatusBadRequest, fmt.Sprintf("project %d is not accessible from this repository", projectID)
}
if project.IsClosed {
return http.StatusForbidden, "project is closed"
}
}
return 0, ""
}
// CreateIssue create an issue of a repository
func CreateIssue(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
@@ -568,6 +601,16 @@ func CreateIssue(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateIssueOption)
if len(form.Projects) > 0 {
if status, message := validateProjectAssignments(ctx, form.Projects); status != 0 {
if status == http.StatusInternalServerError {
ctx.APIErrorInternal(errors.New(message))
} else {
ctx.APIError(status, message)
}
return
}
}
var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
@@ -727,6 +770,16 @@ func EditIssue(ctx *context.APIContext) {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error())
return
}
if form.Projects != nil {
if status, message := validateProjectAssignments(ctx, *form.Projects); status != 0 {
if status == http.StatusInternalServerError {
ctx.APIErrorInternal(errors.New(message))
} else {
ctx.APIError(status, message)
}
return
}
}
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
@@ -155,6 +155,19 @@ func TestAPIRepositoryProjects(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("ClosedProjectCreate", func(t *testing.T) {
project := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1})
require.NoError(t, project_model.ChangeProjectStatus(t.Context(), project, true))
defer func() {
require.NoError(t, project_model.ChangeProjectStatus(t.Context(), project, false))
}()
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/issues", api.CreateIssueOption{
Title: "should be rejected",
Projects: []int64{1},
}).AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("ArchivedRepository", func(t *testing.T) {
_, err := db.GetEngine(t.Context()).ID(repo.ID).Cols("is_archived").Update(&repo_model.Repository{IsArchived: true})
require.NoError(t, err)