Skip to content

Commit 6aa3f8b

Browse files
davidsvantessonlunny
authored andcommitted
Mail assignee when issue/pull request is assigned (#8546)
* Send email to assigned user * Only send mail if enabled * Mail also when assigned through API * Need to refactor functions from models to issue service * Refer to issue index rather than ID * Disable email notifications completly at initalization if global disable * Check of user enbled mail shall be in mail notification function only * Initialize notifications from routers init function. * Use the assigned comment when sending assigned mail * Refactor so that assignees always added as separate step when new issue/pr. * Check error from AddAssignees * Check if user can be assiged to issue or pull request * Missing return * Refactor of CanBeAssigned check. CanBeAssigned shall have same check as UI. * Clarify function names (toggle rather than update/change), and clean up. * Fix review comments. * Flash error if assignees was not added when creating issue/pr * Generate error if assignee users doesn't exist
1 parent c34e58f commit 6aa3f8b

File tree

23 files changed

+333
-216
lines changed

23 files changed

+333
-216
lines changed

models/issue.go

+4-47
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,6 @@ type NewIssueOptions struct {
896896
Repo *Repository
897897
Issue *Issue
898898
LabelIDs []int64
899-
AssigneeIDs []int64
900899
Attachments []string // In UUID format.
901900
IsPull bool
902901
}
@@ -918,40 +917,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
918917
}
919918
}
920919

921-
// Keep the old assignee id thingy for compatibility reasons
922-
if opts.Issue.AssigneeID > 0 {
923-
isAdded := false
924-
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
925-
for _, aID := range opts.AssigneeIDs {
926-
if aID == opts.Issue.AssigneeID {
927-
isAdded = true
928-
break
929-
}
930-
}
931-
932-
if !isAdded {
933-
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
934-
}
935-
}
936-
937-
// Check for and validate assignees
938-
if len(opts.AssigneeIDs) > 0 {
939-
for _, assigneeID := range opts.AssigneeIDs {
940-
user, err := getUserByID(e, assigneeID)
941-
if err != nil {
942-
return fmt.Errorf("getUserByID [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
943-
}
944-
valid, err := canBeAssigned(e, user, opts.Repo)
945-
if err != nil {
946-
return fmt.Errorf("canBeAssigned [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
947-
}
948-
if !valid {
949-
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
950-
}
951-
}
952-
}
953-
954-
// Milestone and assignee validation should happen before insert actual object.
920+
// Milestone validation should happen before insert actual object.
955921
if _, err := e.SetExpr("`index`", "coalesce(MAX(`index`),0)+1").
956922
Where("repo_id=?", opts.Issue.RepoID).
957923
Insert(opts.Issue); err != nil {
@@ -976,14 +942,6 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
976942
}
977943
}
978944

979-
// Insert the assignees
980-
for _, assigneeID := range opts.AssigneeIDs {
981-
err = opts.Issue.changeAssignee(e, doer, assigneeID, true)
982-
if err != nil {
983-
return err
984-
}
985-
}
986-
987945
if opts.IsPull {
988946
_, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID)
989947
} else {
@@ -1041,11 +999,11 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
1041999
}
10421000

10431001
// NewIssue creates new issue with labels for repository.
1044-
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
1002+
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
10451003
// Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887
10461004
i := 0
10471005
for {
1048-
if err = newIssueAttempt(repo, issue, labelIDs, assigneeIDs, uuids); err == nil {
1006+
if err = newIssueAttempt(repo, issue, labelIDs, uuids); err == nil {
10491007
return nil
10501008
}
10511009
if !IsErrNewIssueInsert(err) {
@@ -1059,7 +1017,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []in
10591017
return fmt.Errorf("NewIssue: too many errors attempting to insert the new issue. Last error was: %v", err)
10601018
}
10611019

1062-
func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
1020+
func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
10631021
sess := x.NewSession()
10641022
defer sess.Close()
10651023
if err = sess.Begin(); err != nil {
@@ -1071,7 +1029,6 @@ func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeI
10711029
Issue: issue,
10721030
LabelIDs: labelIDs,
10731031
Attachments: uuids,
1074-
AssigneeIDs: assigneeIDs,
10751032
}); err != nil {
10761033
if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
10771034
return err

models/issue_assignees.go

+54-84
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ func getAssigneesByIssue(e Engine, issue *Issue) (assignees []*User, err error)
5858

5959
// IsUserAssignedToIssue returns true when the user is assigned to the issue
6060
func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) {
61-
isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
62-
return
61+
return isUserAssignedToIssue(x, issue, user)
62+
}
63+
64+
func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool, err error) {
65+
return e.Get(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
6366
}
6467

6568
// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
@@ -78,7 +81,7 @@ func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err e
7881

7982
if !found {
8083
// This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here
81-
if err := UpdateAssignee(issue, doer, assignee.ID); err != nil {
84+
if _, _, err := issue.ToggleAssignee(doer, assignee.ID); err != nil {
8285
return err
8386
}
8487
}
@@ -110,73 +113,56 @@ func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
110113
return
111114
}
112115

113-
// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue
114-
func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) {
115-
// Check if the user is already assigned
116-
isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID})
117-
if err != nil {
118-
return err
119-
}
120-
121-
if !isAssigned {
122-
return issue.ChangeAssignee(doer, assigneeID)
123-
}
124-
return nil
125-
}
126-
127-
// UpdateAssignee deletes or adds an assignee to an issue
128-
func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) {
129-
return issue.ChangeAssignee(doer, assigneeID)
130-
}
131-
132-
// ChangeAssignee changes the Assignee of this issue.
133-
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
116+
// ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
117+
func (issue *Issue) ToggleAssignee(doer *User, assigneeID int64) (removed bool, comment *Comment, err error) {
134118
sess := x.NewSession()
135119
defer sess.Close()
136120

137121
if err := sess.Begin(); err != nil {
138-
return err
122+
return false, nil, err
139123
}
140124

141-
if err := issue.changeAssignee(sess, doer, assigneeID, false); err != nil {
142-
return err
125+
removed, comment, err = issue.toggleAssignee(sess, doer, assigneeID, false)
126+
if err != nil {
127+
return false, nil, err
143128
}
144129

145130
if err := sess.Commit(); err != nil {
146-
return err
131+
return false, nil, err
147132
}
148133

149134
go HookQueue.Add(issue.RepoID)
150-
return nil
135+
136+
return removed, comment, nil
151137
}
152138

153-
func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (err error) {
154-
// Update the assignee
155-
removed, err := updateIssueAssignee(sess, issue, assigneeID)
139+
func (issue *Issue) toggleAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) {
140+
removed, err = toggleUserAssignee(sess, issue, assigneeID)
156141
if err != nil {
157-
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
142+
return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
158143
}
159144

160145
// Repo infos
161146
if err = issue.loadRepo(sess); err != nil {
162-
return fmt.Errorf("loadRepo: %v", err)
147+
return false, nil, fmt.Errorf("loadRepo: %v", err)
163148
}
164149

165150
// Comment
166-
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil {
167-
return fmt.Errorf("createAssigneeComment: %v", err)
151+
comment, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed)
152+
if err != nil {
153+
return false, nil, fmt.Errorf("createAssigneeComment: %v", err)
168154
}
169155

170156
// if pull request is in the middle of creation - don't call webhook
171157
if isCreate {
172-
return nil
158+
return removed, comment, err
173159
}
174160

175161
if issue.IsPull {
176162
mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypePullRequests)
177163

178164
if err = issue.loadPullRequest(sess); err != nil {
179-
return fmt.Errorf("loadPullRequest: %v", err)
165+
return false, nil, fmt.Errorf("loadPullRequest: %v", err)
180166
}
181167
issue.PullRequest.Issue = issue
182168
apiPullRequest := &api.PullRequestPayload{
@@ -190,9 +176,10 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
190176
} else {
191177
apiPullRequest.Action = api.HookIssueAssigned
192178
}
179+
// Assignee comment triggers a webhook
193180
if err := prepareWebhooks(sess, issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
194181
log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
195-
return nil
182+
return false, nil, err
196183
}
197184
} else {
198185
mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypeIssues)
@@ -208,67 +195,50 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
208195
} else {
209196
apiIssue.Action = api.HookIssueAssigned
210197
}
198+
// Assignee comment triggers a webhook
211199
if err := prepareWebhooks(sess, issue.Repo, HookEventIssues, apiIssue); err != nil {
212200
log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
213-
return nil
201+
return false, nil, err
214202
}
215203
}
216-
return nil
204+
return removed, comment, nil
217205
}
218206

219-
// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s)
220-
// Deleting is done the GitHub way (quote from their api documentation):
221-
// https://developer.github.com/v3/issues/#edit-an-issue
222-
// "assignees" (array): Logins for Users to assign to this issue.
223-
// Pass one or more user logins to replace the set of assignees on this Issue.
224-
// Send an empty array ([]) to clear all assignees from the Issue.
225-
func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) {
226-
var allNewAssignees []*User
207+
// toggles user assignee state in database
208+
func toggleUserAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {
227209

228-
// Keep the old assignee thingy for compatibility reasons
229-
if oneAssignee != "" {
230-
// Prevent double adding assignees
231-
var isDouble bool
232-
for _, assignee := range multipleAssignees {
233-
if assignee == oneAssignee {
234-
isDouble = true
235-
break
236-
}
237-
}
238-
239-
if !isDouble {
240-
multipleAssignees = append(multipleAssignees, oneAssignee)
241-
}
210+
// Check if the user exists
211+
assignee, err := getUserByID(e, assigneeID)
212+
if err != nil {
213+
return false, err
242214
}
243215

244-
// Loop through all assignees to add them
245-
for _, assigneeName := range multipleAssignees {
246-
assignee, err := GetUserByName(assigneeName)
247-
if err != nil {
248-
return err
216+
// Check if the submitted user is already assigned, if yes delete him otherwise add him
217+
var i int
218+
for i = 0; i < len(issue.Assignees); i++ {
219+
if issue.Assignees[i].ID == assigneeID {
220+
break
249221
}
250-
251-
allNewAssignees = append(allNewAssignees, assignee)
252222
}
253223

254-
// Delete all old assignees not passed
255-
if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil {
256-
return err
257-
}
224+
assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}
258225

259-
// Add all new assignees
260-
// Update the assignee. The function will check if the user exists, is already
261-
// assigned (which he shouldn't as we deleted all assignees before) and
262-
// has access to the repo.
263-
for _, assignee := range allNewAssignees {
264-
// Extra method to prevent double adding (which would result in removing)
265-
err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID)
226+
toBeDeleted := i < len(issue.Assignees)
227+
if toBeDeleted {
228+
issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i:]...)
229+
_, err = e.Delete(assigneeIn)
266230
if err != nil {
267-
return err
231+
return toBeDeleted, err
232+
}
233+
} else {
234+
issue.Assignees = append(issue.Assignees, assignee)
235+
_, err = e.Insert(assigneeIn)
236+
if err != nil {
237+
return toBeDeleted, err
268238
}
269239
}
270240

271-
return
241+
return toBeDeleted, nil
272242
}
273243

274244
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
@@ -292,7 +262,7 @@ func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string
292262
}
293263

294264
// Get the IDs of all assignees
295-
assigneeIDs = GetUserIDsByNames(multipleAssignees)
265+
assigneeIDs, err = GetUserIDsByNames(multipleAssignees, false)
296266

297267
return
298268
}

models/issue_assignees_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ func TestUpdateAssignee(t *testing.T) {
2020
// Assign multiple users
2121
user2, err := GetUserByID(2)
2222
assert.NoError(t, err)
23-
err = UpdateAssignee(issue, &User{ID: 1}, user2.ID)
23+
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user2.ID)
2424
assert.NoError(t, err)
2525

2626
user3, err := GetUserByID(3)
2727
assert.NoError(t, err)
28-
err = UpdateAssignee(issue, &User{ID: 1}, user3.ID)
28+
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user3.ID)
2929
assert.NoError(t, err)
3030

3131
user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
3232
assert.NoError(t, err)
33-
err = UpdateAssignee(issue, &User{ID: 1}, user1.ID)
33+
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user1.ID)
3434
assert.NoError(t, err)
3535

3636
// Check if he got removed

models/issue_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ func testInsertIssue(t *testing.T, title, content string) {
297297
Title: title,
298298
Content: content,
299299
}
300-
err := NewIssue(repo, &issue, nil, nil, nil)
300+
err := NewIssue(repo, &issue, nil, nil)
301301
assert.NoError(t, err)
302302

303303
var newIssue Issue

0 commit comments

Comments
 (0)