summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorkolaente <konrad@kola-entertainments.de>2018-05-09 18:29:04 +0200
committerLauris BH <lauris@nix.lv>2018-05-09 19:29:04 +0300
commit95f2e2b57beedcdeb2b9623dc86e26f252fdd7bd (patch)
treeebfeb5b898f72fc57ac1fe9680374fdb4bc91aeb /models
parent238a997ec0b5d3e3bed0e55edcacebb44f86d373 (diff)
downloadgitea-95f2e2b57beedcdeb2b9623dc86e26f252fdd7bd.tar.gz
gitea-95f2e2b57beedcdeb2b9623dc86e26f252fdd7bd.zip
Multiple assignees (#3705)
Diffstat (limited to 'models')
-rw-r--r--models/error.go16
-rw-r--r--models/fixtures/issue.yml2
-rw-r--r--models/fixtures/issue_assignees.yml8
-rw-r--r--models/fixtures/issue_user.yml3
-rw-r--r--models/issue.go159
-rw-r--r--models/issue_assignees.go263
-rw-r--r--models/issue_assignees_test.go71
-rw-r--r--models/issue_comment.go112
-rw-r--r--models/issue_list.go46
-rw-r--r--models/issue_mail.go13
-rw-r--r--models/issue_user.go52
-rw-r--r--models/issue_user_test.go17
-rw-r--r--models/migrations/migrations.go13
-rw-r--r--models/migrations/v56.go12
-rw-r--r--models/migrations/v64.go129
-rw-r--r--models/models.go1
-rw-r--r--models/pull.go7
-rw-r--r--models/repo.go6
-rw-r--r--models/user.go6
-rw-r--r--models/webhook_dingtalk.go6
-rw-r--r--models/webhook_discord.go6
-rw-r--r--models/webhook_slack.go6
22 files changed, 699 insertions, 255 deletions
diff --git a/models/error.go b/models/error.go
index d24da2642e..cdb18d23ce 100644
--- a/models/error.go
+++ b/models/error.go
@@ -733,6 +733,22 @@ func (err ErrRepoFileAlreadyExist) Error() string {
return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
}
+// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo
+type ErrUserDoesNotHaveAccessToRepo struct {
+ UserID int64
+ RepoName string
+}
+
+// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.
+func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
+ _, ok := err.(ErrUserDoesNotHaveAccessToRepo)
+ return ok
+}
+
+func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
+ return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
+}
+
// __________ .__
// \______ \____________ ____ ____ | |__
// | | _/\_ __ \__ \ / \_/ ___\| | \
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index ff514e706c..4de8c4fa7e 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -3,7 +3,6 @@
repo_id: 1
index: 1
poster_id: 1
- assignee_id: 1
name: issue1
content: content for the first issue
is_closed: false
@@ -67,7 +66,6 @@
repo_id: 3
index: 1
poster_id: 1
- assignee_id: 1
name: issue6
content: content6
is_closed: false
diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml
new file mode 100644
index 0000000000..1be07df7d4
--- /dev/null
+++ b/models/fixtures/issue_assignees.yml
@@ -0,0 +1,8 @@
+-
+ id: 1
+ assignee_id: 1
+ issue_id: 1
+-
+ id: 2
+ assignee_id: 1
+ issue_id: 6
diff --git a/models/fixtures/issue_user.yml b/models/fixtures/issue_user.yml
index b3f98a71d3..8039b1e40f 100644
--- a/models/fixtures/issue_user.yml
+++ b/models/fixtures/issue_user.yml
@@ -3,7 +3,6 @@
uid: 1
issue_id: 1
is_read: true
- is_assigned: true
is_mentioned: false
-
@@ -11,7 +10,6 @@
uid: 2
issue_id: 1
is_read: true
- is_assigned: false
is_mentioned: false
-
@@ -19,5 +17,4 @@
uid: 4
issue_id: 1
is_read: false
- is_assigned: false
is_mentioned: false
diff --git a/models/issue.go b/models/issue.go
index 7f83d59842..ad354cc34e 100644
--- a/models/issue.go
+++ b/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)
diff --git a/models/issue_assignees.go b/models/issue_assignees.go
new file mode 100644
index 0000000000..3e68126a6c
--- /dev/null
+++ b/models/issue_assignees.go
@@ -0,0 +1,263 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/log"
+
+ api "code.gitea.io/sdk/gitea"
+ "github.com/go-xorm/xorm"
+)
+
+// IssueAssignees saves all issue assignees
+type IssueAssignees struct {
+ ID int64 `xorm:"pk autoincr"`
+ AssigneeID int64 `xorm:"INDEX"`
+ IssueID int64 `xorm:"INDEX"`
+}
+
+// This loads all assignees of an issue
+func (issue *Issue) loadAssignees(e Engine) (err error) {
+ // Reset maybe preexisting assignees
+ issue.Assignees = []*User{}
+
+ err = e.Table("`user`").
+ Join("INNER", "issue_assignees", "assignee_id = `user`.id").
+ Where("issue_assignees.issue_id = ?", issue.ID).
+ Find(&issue.Assignees)
+
+ if err != nil {
+ return err
+ }
+
+ // Check if we have at least one assignee and if yes put it in as `Assignee`
+ if len(issue.Assignees) > 0 {
+ issue.Assignee = issue.Assignees[0]
+ }
+
+ return
+}
+
+// GetAssigneesByIssue returns everyone assigned to that issue
+func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) {
+ err = issue.loadAssignees(x)
+ if err != nil {
+ return assignees, err
+ }
+
+ return issue.Assignees, nil
+}
+
+// IsUserAssignedToIssue returns true when the user is assigned to the issue
+func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) {
+ isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
+ return
+}
+
+// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
+func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err error) {
+ var found bool
+
+ for _, assignee := range issue.Assignees {
+
+ found = false
+ for _, alreadyAssignee := range assignees {
+ if assignee.ID == alreadyAssignee.ID {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ // This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here
+ if err := UpdateAssignee(issue, doer, assignee.ID); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// MakeAssigneeList concats a string with all names of the assignees. Useful for logs.
+func MakeAssigneeList(issue *Issue) (assigneeList string, err error) {
+ err = issue.loadAssignees(x)
+ if err != nil {
+ return "", err
+ }
+
+ for in, assignee := range issue.Assignees {
+ assigneeList += assignee.Name
+
+ if len(issue.Assignees) > (in + 1) {
+ assigneeList += ", "
+ }
+ }
+ return
+}
+
+// ClearAssigneeByUserID deletes all assignments of an user
+func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
+ _, err = sess.Delete(&IssueAssignees{AssigneeID: userID})
+ return
+}
+
+// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue
+func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) {
+ // Check if the user is already assigned
+ isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID})
+ if err != nil {
+ return err
+ }
+
+ if !isAssigned {
+ return issue.ChangeAssignee(doer, assigneeID)
+ }
+ return nil
+}
+
+// UpdateAssignee deletes or adds an assignee to an issue
+func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) {
+ return issue.ChangeAssignee(doer, assigneeID)
+}
+
+// ChangeAssignee changes the Assignee of this issue.
+func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ if err := issue.changeAssignee(sess, doer, assigneeID); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64) (err error) {
+
+ // Update the assignee
+ removed, err := updateIssueAssignee(sess, issue, assigneeID)
+ if err != nil {
+ return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
+ }
+
+ // Repo infos
+ if err = issue.loadRepo(sess); err != nil {
+ return fmt.Errorf("loadRepo: %v", err)
+ }
+
+ // Comment
+ if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil {
+ return fmt.Errorf("createAssigneeComment: %v", err)
+ }
+
+ if issue.IsPull {
+ issue.PullRequest = &PullRequest{Issue: issue}
+ apiPullRequest := &api.PullRequestPayload{
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: issue.Repo.APIFormat(AccessModeNone),
+ Sender: doer.APIFormat(),
+ }
+ if removed {
+ 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, removed, err)
+ return nil
+ }
+ }
+ go HookQueue.Add(issue.RepoID)
+ return nil
+}
+
+// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s)
+// Deleting is done the Github way (quote from their api documentation):
+// https://developer.github.com/v3/issues/#edit-an-issue
+// "assignees" (array): Logins for Users to assign to this issue.
+// Pass one or more user logins to replace the set of assignees on this Issue.
+// Send an empty array ([]) to clear all assignees from the Issue.
+func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) {
+ var allNewAssignees []*User
+
+ // Keep the old assignee thingy for compatibility reasons
+ if oneAssignee != "" {
+ // Prevent double adding assignees
+ var isDouble bool
+ for _, assignee := range multipleAssignees {
+ if assignee == oneAssignee {
+ isDouble = true
+ break
+ }
+ }
+
+ if !isDouble {
+ multipleAssignees = append(multipleAssignees, oneAssignee)
+ }
+ }
+
+ // Loop through all assignees to add them
+ for _, assigneeName := range multipleAssignees {
+ assignee, err := GetUserByName(assigneeName)
+ if err != nil {
+ return err
+ }
+
+ allNewAssignees = append(allNewAssignees, assignee)
+ }
+
+ // Delete all old assignees not passed
+ if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil {
+ return err
+ }
+
+ // Add all new assignees
+ // Update the assignee. The function will check if the user exists, is already
+ // assigned (which he shouldn't as we deleted all assignees before) and
+ // has access to the repo.
+ for _, assignee := range allNewAssignees {
+ // Extra method to prevent double adding (which would result in removing)
+ err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID)
+ if err != nil {
+ return err
+ }
+ }
+
+ return
+}
+
+// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
+func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
+
+ // Keeping the old assigning method for compatibility reasons
+ if oneAssignee != "" {
+
+ // Prevent double adding assignees
+ var isDouble bool
+ for _, assignee := range multipleAssignees {
+ if assignee == oneAssignee {
+ isDouble = true
+ break
+ }
+ }
+
+ if !isDouble {
+ multipleAssignees = append(multipleAssignees, oneAssignee)
+ }
+ }
+
+ // Get the IDs of all assignees
+ assigneeIDs = GetUserIDsByNames(multipleAssignees)
+
+ return
+}
diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go
new file mode 100644
index 0000000000..3247812198
--- /dev/null
+++ b/models/issue_assignees_test.go
@@ -0,0 +1,71 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUpdateAssignee(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ // Fake issue with assignees
+ issue, err := GetIssueByID(1)
+ assert.NoError(t, err)
+
+ // Assign multiple users
+ user2, err := GetUserByID(2)
+ assert.NoError(t, err)
+ err = UpdateAssignee(issue, &User{ID: 1}, user2.ID)
+ assert.NoError(t, err)
+
+ user3, err := GetUserByID(3)
+ assert.NoError(t, err)
+ err = UpdateAssignee(issue, &User{ID: 1}, user3.ID)
+ assert.NoError(t, err)
+
+ user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
+ assert.NoError(t, err)
+ err = UpdateAssignee(issue, &User{ID: 1}, user1.ID)
+ assert.NoError(t, err)
+
+ // Check if he got removed
+ isAssigned, err := IsUserAssignedToIssue(issue, user1)
+ assert.NoError(t, err)
+ assert.False(t, isAssigned)
+
+ // Check if they're all there
+ assignees, err := GetAssigneesByIssue(issue)
+ assert.NoError(t, err)
+
+ var expectedAssignees []*User
+ expectedAssignees = append(expectedAssignees, user2)
+ expectedAssignees = append(expectedAssignees, user3)
+
+ for in, assignee := range assignees {
+ assert.Equal(t, assignee.ID, expectedAssignees[in].ID)
+ }
+
+ // Check if the user is assigned
+ isAssigned, err = IsUserAssignedToIssue(issue, user2)
+ assert.NoError(t, err)
+ assert.True(t, isAssigned)
+
+ // This user should not be assigned
+ isAssigned, err = IsUserAssignedToIssue(issue, &User{ID: 4})
+ assert.NoError(t, err)
+ assert.False(t, isAssigned)
+
+ // Clean everyone
+ err = DeleteNotPassedAssignee(issue, user1, []*User{})
+ assert.NoError(t, err)
+
+ // Check they're gone
+ assignees, err = GetAssigneesByIssue(issue)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(assignees))
+}
diff --git a/models/issue_comment.go b/models/issue_comment.go
index aedb124863..2c5875c29c 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -81,23 +81,22 @@ const (
// Comment represents a comment in commit and issue page.
type Comment struct {
- ID int64 `xorm:"pk autoincr"`
- Type CommentType
- PosterID int64 `xorm:"INDEX"`
- Poster *User `xorm:"-"`
- IssueID int64 `xorm:"INDEX"`
- LabelID int64
- Label *Label `xorm:"-"`
- OldMilestoneID int64
- MilestoneID int64
- OldMilestone *Milestone `xorm:"-"`
- Milestone *Milestone `xorm:"-"`
- OldAssigneeID int64
- AssigneeID int64
- Assignee *User `xorm:"-"`
- OldAssignee *User `xorm:"-"`
- OldTitle string
- NewTitle string
+ ID int64 `xorm:"pk autoincr"`
+ Type CommentType
+ PosterID int64 `xorm:"INDEX"`
+ Poster *User `xorm:"-"`
+ IssueID int64 `xorm:"INDEX"`
+ LabelID int64
+ Label *Label `xorm:"-"`
+ OldMilestoneID int64
+ MilestoneID int64
+ OldMilestone *Milestone `xorm:"-"`
+ Milestone *Milestone `xorm:"-"`
+ AssigneeID int64
+ RemovedAssignee bool
+ Assignee *User `xorm:"-"`
+ OldTitle string
+ NewTitle string
CommitID int64
Line int64
@@ -247,18 +246,9 @@ func (c *Comment) LoadMilestone() error {
return nil
}
-// LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees
-func (c *Comment) LoadAssignees() error {
+// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
+func (c *Comment) LoadAssigneeUser() error {
var err error
- if c.OldAssigneeID > 0 {
- c.OldAssignee, err = getUserByID(x, c.OldAssigneeID)
- if err != nil {
- if !IsErrUserNotExist(err) {
- return err
- }
- c.OldAssignee = NewGhostUser()
- }
- }
if c.AssigneeID > 0 {
c.Assignee, err = getUserByID(x, c.AssigneeID)
@@ -324,21 +314,21 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
LabelID = opts.Label.ID
}
comment := &Comment{
- Type: opts.Type,
- PosterID: opts.Doer.ID,
- Poster: opts.Doer,
- IssueID: opts.Issue.ID,
- LabelID: LabelID,
- OldMilestoneID: opts.OldMilestoneID,
- MilestoneID: opts.MilestoneID,
- OldAssigneeID: opts.OldAssigneeID,
- AssigneeID: opts.AssigneeID,
- CommitID: opts.CommitID,
- CommitSHA: opts.CommitSHA,
- Line: opts.LineNum,
- Content: opts.Content,
- OldTitle: opts.OldTitle,
- NewTitle: opts.NewTitle,
+ Type: opts.Type,
+ PosterID: opts.Doer.ID,
+ Poster: opts.Doer,
+ IssueID: opts.Issue.ID,
+ LabelID: LabelID,
+ OldMilestoneID: opts.OldMilestoneID,
+ MilestoneID: opts.MilestoneID,
+ RemovedAssignee: opts.RemovedAssignee,
+ AssigneeID: opts.AssigneeID,
+ CommitID: opts.CommitID,
+ CommitSHA: opts.CommitSHA,
+ Line: opts.LineNum,
+ Content: opts.Content,
+ OldTitle: opts.OldTitle,
+ NewTitle: opts.NewTitle,
}
if _, err = e.Insert(comment); err != nil {
return nil, err
@@ -480,14 +470,14 @@ func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue
})
}
-func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldAssigneeID, assigneeID int64) (*Comment, error) {
+func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) {
return createComment(e, &CreateCommentOptions{
- Type: CommentTypeAssignees,
- Doer: doer,
- Repo: repo,
- Issue: issue,
- OldAssigneeID: oldAssigneeID,
- AssigneeID: assigneeID,
+ Type: CommentTypeAssignees,
+ Doer: doer,
+ Repo: repo,
+ Issue: issue,
+ RemovedAssignee: removedAssignee,
+ AssigneeID: assigneeID,
})
}
@@ -548,17 +538,17 @@ type CreateCommentOptions struct {
Issue *Issue
Label *Label
- OldMilestoneID int64
- MilestoneID int64
- OldAssigneeID int64
- AssigneeID int64
- OldTitle string
- NewTitle string
- CommitID int64
- CommitSHA string
- LineNum int64
- Content string
- Attachments []string // UUIDs of attachments
+ OldMilestoneID int64
+ MilestoneID int64
+ AssigneeID int64
+ RemovedAssignee bool
+ OldTitle string
+ NewTitle string
+ CommitID int64
+ CommitSHA string
+ LineNum int64
+ Content string
+ Attachments []string // UUIDs of attachments
}
// CreateComment creates comment of issue or commit.
diff --git a/models/issue_list.go b/models/issue_list.go
index 01a1a15f44..05130a6eef 100644
--- a/models/issue_list.go
+++ b/models/issue_list.go
@@ -154,38 +154,38 @@ func (issues IssueList) loadMilestones(e Engine) error {
return nil
}
-func (issues IssueList) getAssigneeIDs() []int64 {
- var ids = make(map[int64]struct{}, len(issues))
- for _, issue := range issues {
- if _, ok := ids[issue.AssigneeID]; !ok {
- ids[issue.AssigneeID] = struct{}{}
- }
- }
- return keysInt64(ids)
-}
-
func (issues IssueList) loadAssignees(e Engine) error {
- assigneeIDs := issues.getAssigneeIDs()
- if len(assigneeIDs) == 0 {
+ if len(issues) == 0 {
return nil
}
- assigneeMaps := make(map[int64]*User, len(assigneeIDs))
- err := e.
- In("id", assigneeIDs).
- Find(&assigneeMaps)
+ type AssigneeIssue struct {
+ IssueAssignee *IssueAssignees `xorm:"extends"`
+ Assignee *User `xorm:"extends"`
+ }
+
+ var assignees = make(map[int64][]*User, len(issues))
+ rows, err := e.Table("issue_assignees").
+ Join("INNER", "user", "`user`.id = `issue_assignees`.assignee_id").
+ In("`issue_assignees`.issue_id", issues.getIssueIDs()).
+ Rows(new(AssigneeIssue))
if err != nil {
return err
}
+ defer rows.Close()
- for _, issue := range issues {
- if issue.AssigneeID <= 0 {
- continue
- }
- var ok bool
- if issue.Assignee, ok = assigneeMaps[issue.AssigneeID]; !ok {
- issue.Assignee = NewGhostUser()
+ for rows.Next() {
+ var assigneeIssue AssigneeIssue
+ err = rows.Scan(&assigneeIssue)
+ if err != nil {
+ return err
}
+
+ assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
+ }
+
+ for _, issue := range issues {
+ issue.Assignees = assignees[issue.ID]
}
return nil
}
diff --git a/models/issue_mail.go b/models/issue_mail.go
index 08e4eed584..179bb6527b 100644
--- a/models/issue_mail.go
+++ b/models/issue_mail.go
@@ -46,9 +46,16 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
participants = append(participants, issue.Poster)
}
- // Assignee must receive any communications
- if issue.Assignee != nil && issue.AssigneeID > 0 && issue.AssigneeID != doer.ID {
- participants = append(participants, issue.Assignee)
+ // Assignees must receive any communications
+ assignees, err := GetAssigneesByIssue(issue)
+ if err != nil {
+ return err
+ }
+
+ for _, assignee := range assignees {
+ if assignee.ID != doer.ID {
+ participants = append(participants, assignee)
+ }
}
tos := make([]string, 0, len(watchers)) // List of email addresses.
diff --git a/models/issue_user.go b/models/issue_user.go
index 1aded0b440..de5a185aec 100644
--- a/models/issue_user.go
+++ b/models/issue_user.go
@@ -6,6 +6,8 @@ package models
import (
"fmt"
+
+ "github.com/go-xorm/xorm"
)
// IssueUser represents an issue-user relation.
@@ -14,7 +16,6 @@ type IssueUser struct {
UID int64 `xorm:"INDEX"` // User ID.
IssueID int64
IsRead bool
- IsAssigned bool
IsMentioned bool
}
@@ -32,9 +33,8 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
issueUsers := make([]*IssueUser, 0, len(assignees)+1)
for _, assignee := range assignees {
issueUsers = append(issueUsers, &IssueUser{
- IssueID: issue.ID,
- UID: assignee.ID,
- IsAssigned: assignee.ID == issue.AssigneeID,
+ IssueID: issue.ID,
+ UID: assignee.ID,
})
isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID
}
@@ -51,34 +51,38 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
return nil
}
-func updateIssueUserByAssignee(e Engine, issue *Issue) (err error) {
- if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil {
- return err
+func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {
+
+ // Check if the user exists
+ _, err = GetUserByID(assigneeID)
+ if err != nil {
+ return false, err
}
- // Assignee ID equals to 0 means clear assignee.
- if issue.AssigneeID > 0 {
- if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil {
- return err
+ // Check if the submitted user is already assigne, if yes delete him otherwise add him
+ var toBeDeleted bool
+ for _, assignee := range issue.Assignees {
+ if assignee.ID == assigneeID {
+ toBeDeleted = true
+ break
}
}
- return updateIssue(e, issue)
-}
+ assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}
-// UpdateIssueUserByAssignee updates issue-user relation for assignee.
-func UpdateIssueUserByAssignee(issue *Issue) (err error) {
- sess := x.NewSession()
- defer sess.Close()
- if err = sess.Begin(); err != nil {
- return err
- }
-
- if err = updateIssueUserByAssignee(sess, issue); err != nil {
- return err
+ if toBeDeleted {
+ _, err = e.Delete(assigneeIn)
+ if err != nil {
+ return toBeDeleted, err
+ }
+ } else {
+ _, err = e.Insert(assigneeIn)
+ if err != nil {
+ return toBeDeleted, err
+ }
}
- return sess.Commit()
+ return toBeDeleted, nil
}
// UpdateIssueUserByRead updates issue-user relation for reading.
diff --git a/models/issue_user_test.go b/models/issue_user_test.go
index e3ebfa8fa2..a333bc8619 100644
--- a/models/issue_user_test.go
+++ b/models/issue_user_test.go
@@ -32,23 +32,6 @@ func Test_newIssueUsers(t *testing.T) {
AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
}
-func TestUpdateIssueUserByAssignee(t *testing.T) {
- assert.NoError(t, PrepareTestDatabase())
- issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
- // artificially change assignee in issue_user table
- AssertSuccessfulInsert(t, &IssueUser{IssueID: issue.ID, UID: 5, IsAssigned: true})
- _, err := x.Cols("is_assigned").
- Update(&IssueUser{IsAssigned: false}, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID})
- assert.NoError(t, err)
-
- assert.NoError(t, UpdateIssueUserByAssignee(issue))
-
- // issue_user table should now be correct again
- AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}, "is_assigned=1")
- AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 5}, "is_assigned=0")
-}
-
func TestUpdateIssueUserByRead(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index aa9dd13107..e85da8de79 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -180,6 +180,8 @@ var migrations = []Migration{
NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP),
// v63 -> v64
NewMigration("add language column for user setting", addLanguageSetting),
+ // v64 -> v65
+ NewMigration("add multiple assignees", addMultipleAssignees),
}
// Migrate database to current version
@@ -229,7 +231,7 @@ Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to curr
return nil
}
-func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (err error) {
+func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) {
if tableName == "" || len(columnNames) == 0 {
return nil
}
@@ -245,17 +247,10 @@ func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (
}
cols += "DROP COLUMN `" + col + "`"
}
- if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
+ if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
}
case setting.UseMSSQL:
- sess := x.NewSession()
- defer sess.Close()
-
- if err = sess.Begin(); err != nil {
- return err
- }
-
cols := ""
for _, col := range columnNames {
if cols != "" {
diff --git a/models/migrations/v56.go b/models/migrations/v56.go
index 1f96cc543e..79f8ce0ba5 100644
--- a/models/migrations/v56.go
+++ b/models/migrations/v56.go
@@ -9,5 +9,15 @@ import (
)
func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) {
- return dropTableColumns(x, "org_user", "is_owner", "num_teams")
+ sess := x.NewSession()
+ defer sess.Close()
+
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err := dropTableColumns(sess, "org_user", "is_owner", "num_teams"); err != nil {
+ return err
+ }
+ return sess.Commit()
}
diff --git a/models/migrations/v64.go b/models/migrations/v64.go
new file mode 100644
index 0000000000..a281ac67e4
--- /dev/null
+++ b/models/migrations/v64.go
@@ -0,0 +1,129 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/go-xorm/xorm"
+)
+
+func addMultipleAssignees(x *xorm.Engine) error {
+
+ // Redeclare issue struct
+ type Issue struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
+ Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
+ PosterID int64 `xorm:"INDEX"`
+ Title string `xorm:"name"`
+ Content string `xorm:"TEXT"`
+ MilestoneID int64 `xorm:"INDEX"`
+ Priority int
+ AssigneeID int64 `xorm:"INDEX"`
+ IsClosed bool `xorm:"INDEX"`
+ IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
+ NumComments int
+ Ref string
+
+ DeadlineUnix util.TimeStamp `xorm:"INDEX"`
+ CreatedUnix util.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
+ ClosedUnix util.TimeStamp `xorm:"INDEX"`
+ }
+
+ allIssues := []Issue{}
+ err := x.Find(&allIssues)
+ if err != nil {
+ return err
+ }
+
+ // Create the table
+ type IssueAssignees struct {
+ ID int64 `xorm:"pk autoincr"`
+ AssigneeID int64 `xorm:"INDEX"`
+ IssueID int64 `xorm:"INDEX"`
+ }
+ err = x.Sync2(IssueAssignees{})
+ if err != nil {
+ return err
+ }
+
+ // Range over all issues and insert a new entry for each issue/assignee
+ sess := x.NewSession()
+ defer sess.Close()
+
+ err = sess.Begin()
+ if err != nil {
+ return err
+ }
+
+ for _, issue := range allIssues {
+ if issue.AssigneeID != 0 {
+ _, err := sess.Insert(IssueAssignees{IssueID: issue.ID, AssigneeID: issue.AssigneeID})
+ if err != nil {
+ sess.Rollback()
+ return err
+ }
+ }
+ }
+
+ // Updated the comment table
+ type Comment struct {
+ ID int64 `xorm:"pk autoincr"`
+ Type int
+ PosterID int64 `xorm:"INDEX"`
+ IssueID int64 `xorm:"INDEX"`
+ LabelID int64
+ OldMilestoneID int64
+ MilestoneID int64
+ OldAssigneeID int64
+ AssigneeID int64
+ RemovedAssignee bool
+ OldTitle string
+ NewTitle string
+
+ CommitID int64
+ Line int64
+ Content string `xorm:"TEXT"`
+ RenderedContent string `xorm:"-"`
+
+ CreatedUnix util.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
+
+ // Reference issue in commit message
+ CommitSHA string `xorm:"VARCHAR(40)"`
+ }
+ if err := x.Sync2(Comment{}); err != nil {
+ return err
+ }
+
+ // Migrate comments
+ // First update everything to not have nulls in db
+ if _, err := sess.Where("type = ?", 9).Cols("removed_assignee").Update(Comment{RemovedAssignee: false}); err != nil {
+ return err
+ }
+
+ allAssignementComments := []Comment{}
+ if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil {
+ return err
+ }
+
+ for _, comment := range allAssignementComments {
+ // Everytime where OldAssigneeID is > 0, the assignement was removed.
+ if comment.OldAssigneeID > 0 {
+ _, err = sess.ID(comment.ID).Update(Comment{RemovedAssignee: true})
+ }
+ }
+
+ if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil {
+ return err
+ }
+
+ if err := dropTableColumns(sess, "issue_user", "is_assigned"); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
diff --git a/models/models.go b/models/models.go
index 549d2eadc9..9213cd3b79 100644
--- a/models/models.go
+++ b/models/models.go
@@ -119,6 +119,7 @@ func init() {
new(RepoIndexerStatus),
new(LFSLock),
new(Reaction),
+ new(IssueAssignees),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/models/pull.go b/models/pull.go
index 5f4a6e2054..d23d885b6c 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -198,6 +198,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
Labels: apiIssue.Labels,
Milestone: apiIssue.Milestone,
Assignee: apiIssue.Assignee,
+ Assignees: apiIssue.Assignees,
State: apiIssue.State,
Comments: apiIssue.Comments,
HTMLURL: pr.Issue.HTMLURL(),
@@ -719,7 +720,7 @@ func (pr *PullRequest) testPatch() (err error) {
}
// NewPullRequest creates new pull request with labels for repository.
-func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) {
+func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
@@ -732,7 +733,11 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
LabelIDs: labelIDs,
Attachments: uuids,
IsPull: true,
+ AssigneeIDs: assigneeIDs,
}); err != nil {
+ if IsErrUserDoesNotHaveAccessToRepo(err) {
+ return err
+ }
return fmt.Errorf("newIssue: %v", err)
}
diff --git a/models/repo.go b/models/repo.go
index 43fb1e0c73..4a7eb859c4 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -600,9 +600,9 @@ func (repo *Repository) GetAssignees() (_ []*User, err error) {
return repo.getAssignees(x)
}
-// GetAssigneeByID returns the user that has write access of repository by given ID.
-func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
- return GetAssigneeByID(repo, userID)
+// GetUserIfHasWriteAccess returns the user that has write access of repository by given ID.
+func (repo *Repository) GetUserIfHasWriteAccess(userID int64) (*User, error) {
+ return GetUserIfHasWriteAccess(repo, userID)
}
// GetMilestoneByID returns the milestone belongs to repository by given ID.
diff --git a/models/user.go b/models/user.go
index 106d79ffcc..8f5ee6e5a7 100644
--- a/models/user.go
+++ b/models/user.go
@@ -993,7 +993,7 @@ func deleteUser(e *xorm.Session, u *User) error {
// ***** END: PublicKey *****
// Clear assignee.
- if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.ID); err != nil {
+ if err = clearAssigneeByUserID(e, u.ID); err != nil {
return fmt.Errorf("clear assignee: %v", err)
}
@@ -1110,8 +1110,8 @@ func GetUserByID(id int64) (*User, error) {
return getUserByID(x, id)
}
-// GetAssigneeByID returns the user with write access of repository by given ID.
-func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
+// GetUserIfHasWriteAccess returns the user with write access of repository by given ID.
+func GetUserIfHasWriteAccess(repo *Repository, userID int64) (*User, error) {
has, err := HasAccess(userID, repo, AccessModeWrite)
if err != nil {
return nil, err
diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go
index e25e989084..719ffcae73 100644
--- a/models/webhook_dingtalk.go
+++ b/models/webhook_dingtalk.go
@@ -118,8 +118,12 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload,
title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
text = p.PullRequest.Body
case api.HookIssueAssigned:
+ list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
+ if err != nil {
+ return &DingtalkPayload{}, err
+ }
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
- p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
+ list, p.Index, p.PullRequest.Title)
text = p.PullRequest.Body
case api.HookIssueUnassigned:
title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
diff --git a/models/webhook_discord.go b/models/webhook_discord.go
index 6d39d8b993..40d9d58992 100644
--- a/models/webhook_discord.go
+++ b/models/webhook_discord.go
@@ -191,8 +191,12 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta)
text = p.PullRequest.Body
color = warnColor
case api.HookIssueAssigned:
+ list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
+ if err != nil {
+ return &DiscordPayload{}, err
+ }
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
- p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
+ list, p.Index, p.PullRequest.Title)
text = p.PullRequest.Body
color = successColor
case api.HookIssueUnassigned:
diff --git a/models/webhook_slack.go b/models/webhook_slack.go
index dd25a8d7df..256819adc5 100644
--- a/models/webhook_slack.go
+++ b/models/webhook_slack.go
@@ -172,8 +172,12 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S
text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
attachmentText = SlackTextFormatter(p.PullRequest.Body)
case api.HookIssueAssigned:
+ list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
+ if err != nil {
+ return &SlackPayload{}, err
+ }
text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName,
- SlackLinkFormatter(setting.AppURL+p.PullRequest.Assignee.UserName, p.PullRequest.Assignee.UserName),
+ SlackLinkFormatter(setting.AppURL+list, list),
titleLink, senderLink)
case api.HookIssueUnassigned:
text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)