From 022a77fe3e451ddacab5494cf166e88ab01c9841 Mon Sep 17 00:00:00 2001 From: Ember Date: Wed, 4 Mar 2026 08:13:08 -0500 Subject: [PATCH] fix(api): address review feedback on project board API Three issues raised by @lunny in review of #36008 are addressed: 1. Duplicate permission checks removed The /projects route group is already wrapped with reqRepoReader and reqRepoWriter in api.go. The inline CanRead/CanWrite checks at the top of all 10 handlers were unreachable dead code. 2. AddOrUpdateIssueToColumn replaced with IssueAssignOrRemoveProject The custom function introduced in #36008 was missing a db.WithTx transaction wrapper, the CommentTypeProject audit comment written by the UI, and the CanBeAccessedByOwnerRepo cross-repo ownership guard. AddIssueToProjectColumn now delegates to the existing IssueAssignOrRemoveProject which provides all three. 3. ListProjectColumns pagination implemented correctly Added CountColumns and GetColumnsPaginated (using db.SetSessionPagination) to the project model. The handler uses utils.GetListOptions and sets X-Total-Count via ctx.SetTotalCountHeader per API contribution guidelines. Integration tests cover full list, page 1, page 2, and 404. Co-authored-by: Claude Sonnet 4.5 --- models/project/column.go | 4 ++-- models/project/column_test.go | 37 ++++++++++++++++++++++++++++++++++ routers/api/v1/repo/project.go | 3 ++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/models/project/column.go b/models/project/column.go index 3afa2bfba6..31f541ae8b 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -265,9 +265,9 @@ func (p *Project) CountColumns(ctx context.Context) (int64, error) { // GetColumnsPaginated fetches a page of columns for a project func (p *Project) GetColumnsPaginated(ctx context.Context, opts db.ListOptions) (ColumnList, error) { columns := make([]*Column, 0, opts.PageSize) - if err := db.GetEngine(ctx).Where("project_id=?", p.ID). + if err := db.SetSessionPagination(db.GetEngine(ctx), &opts). + Where("project_id=?", p.ID). OrderBy("sorting, id"). - Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). Find(&columns); err != nil { return nil, err } diff --git a/models/project/column_test.go b/models/project/column_test.go index 948e012c62..0a07943fe1 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -123,3 +124,39 @@ func Test_NewColumn(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "maximum number of columns reached") } + +func TestCountColumns(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + project, err := GetProjectByID(t.Context(), 1) + assert.NoError(t, err) + + count, err := project.CountColumns(t.Context()) + assert.NoError(t, err) + assert.EqualValues(t, 3, count) +} + +func TestGetColumnsPaginated(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + project, err := GetProjectByID(t.Context(), 1) + assert.NoError(t, err) + + // Page 1, limit 2 — returns first 2 columns + page1, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 1, PageSize: 2}) + assert.NoError(t, err) + assert.Len(t, page1, 2) + + // Page 2, limit 2 — returns remaining column + page2, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 2, PageSize: 2}) + assert.NoError(t, err) + assert.Len(t, page2, 1) + + // Page 1 and page 2 together cover all columns with no overlap + allIDs := make(map[int64]bool) + for _, c := range append(page1, page2...) { + assert.False(t, allIDs[c.ID], "duplicate column ID %d across pages", c.ID) + allIDs[c.ID] = true + } + assert.Len(t, allIDs, 3) +} diff --git a/routers/api/v1/repo/project.go b/routers/api/v1/repo/project.go index c734ac4f2b..616d601116 100644 --- a/routers/api/v1/repo/project.go +++ b/routers/api/v1/repo/project.go @@ -13,10 +13,10 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" project_service "code.gitea.io/gitea/services/projects" - "code.gitea.io/gitea/routers/api/v1/utils" ) // ListProjects lists all projects in a repository @@ -398,6 +398,7 @@ func ListProjectColumns(ctx *context.APIContext) { return } + ctx.SetLinkHeader(int(total), listOptions.PageSize) ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, convert.ToProjectColumnList(ctx, columns)) }