mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-05-25 12:00:01 +02:00
Multiple assignees (#3705)
This commit is contained in:
parent
238a997ec0
commit
95f2e2b57b
36 changed files with 1012 additions and 451 deletions
159
models/issue.go
159
models/issue.go
|
@ -37,7 +37,7 @@ type Issue struct {
|
|||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"INDEX"`
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *User `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
|
@ -56,6 +56,7 @@ type Issue struct {
|
|||
Comments []*Comment `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*User `xorm:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -140,22 +141,6 @@ func (issue *Issue) loadPoster(e Engine) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (issue *Issue) loadAssignee(e Engine) (err error) {
|
||||
if issue.Assignee == nil && issue.AssigneeID > 0 {
|
||||
issue.Assignee, err = getUserByID(e, issue.AssigneeID)
|
||||
if err != nil {
|
||||
issue.AssigneeID = -1
|
||||
issue.Assignee = NewGhostUser()
|
||||
if !IsErrUserNotExist(err) {
|
||||
return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
|
||||
}
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (issue *Issue) loadPullRequest(e Engine) (err error) {
|
||||
if issue.IsPull && issue.PullRequest == nil {
|
||||
issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
|
||||
|
@ -231,7 +216,7 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err = issue.loadAssignee(e); err != nil {
|
||||
if err = issue.loadAssignees(e); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -343,8 +328,11 @@ func (issue *Issue) APIFormat() *api.Issue {
|
|||
if issue.Milestone != nil {
|
||||
apiIssue.Milestone = issue.Milestone.APIFormat()
|
||||
}
|
||||
if issue.Assignee != nil {
|
||||
apiIssue.Assignee = issue.Assignee.APIFormat()
|
||||
if len(issue.Assignees) > 0 {
|
||||
for _, assignee := range issue.Assignees {
|
||||
apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat())
|
||||
}
|
||||
apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
|
||||
}
|
||||
if issue.IsPull {
|
||||
apiIssue.PullRequest = &api.PullRequestMeta{
|
||||
|
@ -605,19 +593,6 @@ func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) {
|
|||
return sess.Commit()
|
||||
}
|
||||
|
||||
// GetAssignee sets the Assignee attribute of this issue.
|
||||
func (issue *Issue) GetAssignee() (err error) {
|
||||
if issue.AssigneeID == 0 || issue.Assignee != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
issue.Assignee, err = GetUserByID(issue.AssigneeID)
|
||||
if IsErrUserNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadBy sets issue to be read by given user.
|
||||
func (issue *Issue) ReadBy(userID int64) error {
|
||||
if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
|
||||
|
@ -823,55 +798,6 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ChangeAssignee changes the Assignee field of this issue.
|
||||
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
|
||||
var oldAssigneeID = issue.AssigneeID
|
||||
issue.AssigneeID = assigneeID
|
||||
if err = UpdateIssueUserByAssignee(issue); err != nil {
|
||||
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err = issue.loadRepo(sess); err != nil {
|
||||
return fmt.Errorf("loadRepo: %v", err)
|
||||
}
|
||||
|
||||
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, oldAssigneeID, assigneeID); err != nil {
|
||||
return fmt.Errorf("createAssigneeComment: %v", err)
|
||||
}
|
||||
|
||||
issue.Assignee, err = GetUserByID(issue.AssigneeID)
|
||||
if err != nil && !IsErrUserNotExist(err) {
|
||||
log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Error not nil here means user does not exist, which is remove assignee.
|
||||
isRemoveAssignee := err != nil
|
||||
if issue.IsPull {
|
||||
issue.PullRequest.Issue = issue
|
||||
apiPullRequest := &api.PullRequestPayload{
|
||||
Index: issue.Index,
|
||||
PullRequest: issue.PullRequest.APIFormat(),
|
||||
Repository: issue.Repo.APIFormat(AccessModeNone),
|
||||
Sender: doer.APIFormat(),
|
||||
}
|
||||
if isRemoveAssignee {
|
||||
apiPullRequest.Action = api.HookIssueUnassigned
|
||||
} else {
|
||||
apiPullRequest.Action = api.HookIssueAssigned
|
||||
}
|
||||
if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
|
||||
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
go HookQueue.Add(issue.RepoID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTasks returns the amount of tasks in the issues content
|
||||
func (issue *Issue) GetTasks() int {
|
||||
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
||||
|
@ -887,6 +813,7 @@ type NewIssueOptions struct {
|
|||
Repo *Repository
|
||||
Issue *Issue
|
||||
LabelIDs []int64
|
||||
AssigneeIDs []int64
|
||||
Attachments []string // In UUID format.
|
||||
IsPull bool
|
||||
}
|
||||
|
@ -909,14 +836,32 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 {
|
||||
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
|
||||
// Keep the old assignee id thingy for compatibility reasons
|
||||
if opts.Issue.AssigneeID > 0 {
|
||||
isAdded := false
|
||||
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
|
||||
for _, aID := range opts.AssigneeIDs {
|
||||
if aID == opts.Issue.AssigneeID {
|
||||
isAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
opts.Issue.AssigneeID = 0
|
||||
opts.Issue.Assignee = nil
|
||||
|
||||
if !isAdded {
|
||||
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for and validate assignees
|
||||
if len(opts.AssigneeIDs) > 0 {
|
||||
for _, assigneeID := range opts.AssigneeIDs {
|
||||
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
|
||||
}
|
||||
if !valid {
|
||||
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -931,11 +876,10 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if opts.Issue.AssigneeID > 0 {
|
||||
if err = opts.Issue.loadRepo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil {
|
||||
// Insert the assignees
|
||||
for _, assigneeID := range opts.AssigneeIDs {
|
||||
err = opts.Issue.changeAssignee(e, doer, assigneeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -995,7 +939,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
|
|||
}
|
||||
|
||||
// NewIssue creates new issue with labels for repository.
|
||||
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
|
||||
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
|
@ -1007,7 +951,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
|
|||
Issue: issue,
|
||||
LabelIDs: labelIDs,
|
||||
Attachments: uuids,
|
||||
AssigneeIDs: assigneeIDs,
|
||||
}); err != nil {
|
||||
if IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("newIssue: %v", err)
|
||||
}
|
||||
|
||||
|
@ -1150,7 +1098,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error {
|
|||
}
|
||||
|
||||
if opts.AssigneeID > 0 {
|
||||
sess.And("issue.assignee_id=?", opts.AssigneeID)
|
||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
|
||||
}
|
||||
|
||||
if opts.PosterID > 0 {
|
||||
|
@ -1372,7 +1321,8 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
|
|||
}
|
||||
|
||||
if opts.AssigneeID > 0 {
|
||||
sess.And("issue.assignee_id = ?", opts.AssigneeID)
|
||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
|
||||
}
|
||||
|
||||
if opts.PosterID > 0 {
|
||||
|
@ -1438,13 +1388,15 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
|||
}
|
||||
case FilterModeAssign:
|
||||
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
|
||||
And("assignee_id = ?", opts.UserID).
|
||||
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", opts.UserID).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
|
||||
And("assignee_id = ?", opts.UserID).
|
||||
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", opts.UserID).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1466,7 +1418,8 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
|
|||
|
||||
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
|
||||
stats.AssignCount, err = x.Where(cond).
|
||||
And("assignee_id = ?", opts.UserID).
|
||||
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", opts.UserID).
|
||||
Count(new(Issue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1505,8 +1458,10 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
|
|||
|
||||
switch filterMode {
|
||||
case FilterModeAssign:
|
||||
openCountSession.And("assignee_id = ?", uid)
|
||||
closedCountSession.And("assignee_id = ?", uid)
|
||||
openCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", uid)
|
||||
closedCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", uid)
|
||||
case FilterModeCreate:
|
||||
openCountSession.And("poster_id = ?", uid)
|
||||
closedCountSession.And("poster_id = ?", uid)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue