Files
gitea/models/project/issue.go
T
8fd9536150 Tighten project board API for GitHub-style consumers
- Rename response timestamps to created_at / updated_at / closed_at
- Replace is_closed bool with state ("open" / "closed") via api.StateType
- Switch template_type / card_type / type to string enums with input validation
- Embed creator User object on Project and ProjectColumn (batched lookup)
- Add absolute html_url; drop relative url
- Add POST /repos/.../projects/{id}/issues/{issue_id}/move with optional sorting
- Validate column hex color and reject writes to closed projects
- Document issue-only project scope in swagger
- Push project-issue existence check into project_model.IsIssueInColumn
- Add project_service.ErrIssueNotInProject sentinel for the move endpoint

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-03 14:32:39 +05:30

98 lines
2.8 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"errors"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
)
// ProjectIssue saves relation from issue to a project
type ProjectIssue struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX"`
ProjectID int64 `xorm:"INDEX"`
// ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
// the sorting order on the column
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
}
func init() {
db.RegisterModel(new(ProjectIssue))
}
func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error {
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&ProjectIssue{})
return err
}
func IsIssueInColumn(ctx context.Context, issueID, projectID, columnID int64) (bool, error) {
return db.GetEngine(ctx).Exist(&ProjectIssue{
IssueID: issueID,
ProjectID: projectID,
ProjectColumnID: columnID,
})
}
// GetColumnIssueNextSorting returns the sorting value to append an issue at the end of the column.
func GetColumnIssueNextSorting(ctx context.Context, projectID, columnID int64) (int64, error) {
res := struct {
MaxSorting int64
IssueCount int64
}{}
if _, err := db.GetEngine(ctx).Select("max(sorting) AS max_sorting, count(*) AS issue_count").
Table("project_issue").
Where("project_id=?", projectID).
And("project_board_id=?", columnID).
Get(&res); err != nil {
return 0, err
}
return util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0), nil
}
func moveIssuesToAnotherColumn(ctx context.Context, oldColumn, newColumn *Column) error {
if oldColumn.ProjectID != newColumn.ProjectID {
return errors.New("columns have to be in the same project")
}
if oldColumn.ID == newColumn.ID {
return nil
}
movedIssues, err := oldColumn.GetIssues(ctx)
if err != nil {
return err
}
if len(movedIssues) == 0 {
return nil
}
nextSorting, err := GetColumnIssueNextSorting(ctx, newColumn.ProjectID, newColumn.ID)
if err != nil {
return err
}
return db.WithTx(ctx, func(ctx context.Context) error {
for i, issue := range movedIssues {
issue.ProjectColumnID = newColumn.ID
issue.Sorting = nextSorting + int64(i)
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
return err
}
}
return nil
})
}
// DeleteAllProjectIssueByIssueIDsAndProjectIDs delete all project's issues by issue's and project's ids
func DeleteAllProjectIssueByIssueIDsAndProjectIDs(ctx context.Context, issueIDs, projectIDs []int64) error {
_, err := db.GetEngine(ctx).In("project_id", projectIDs).In("issue_id", issueIDs).Delete(&ProjectIssue{})
return err
}