Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mail assignee when issue/pull request is assigned #8546

Merged
merged 23 commits into from
Oct 25, 2019
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fb0bb14
Send email to assigned user
davidsvantesson Oct 12, 2019
385c14d
Only send mail if enabled
davidsvantesson Oct 16, 2019
27bbc9d
Mail also when assigned through API
davidsvantesson Oct 16, 2019
b063676
Need to refactor functions from models to issue service
davidsvantesson Oct 16, 2019
1374db5
Refer to issue index rather than ID
davidsvantesson Oct 16, 2019
a3556c9
Merge branch 'master' into mail-assigned
davidsvantesson Oct 16, 2019
630dceb
Disable email notifications completly at initalization if global disable
davidsvantesson Oct 17, 2019
18187fa
Check of user enbled mail shall be in mail notification function only
davidsvantesson Oct 17, 2019
99f8db4
Initialize notifications from routers init function.
davidsvantesson Oct 18, 2019
ba31fed
Use the assigned comment when sending assigned mail
davidsvantesson Oct 18, 2019
04450f9
Refactor so that assignees always added as separate step when new iss…
davidsvantesson Oct 18, 2019
fea106d
Check error from AddAssignees
davidsvantesson Oct 18, 2019
5f6f77f
Check if user can be assiged to issue or pull request
davidsvantesson Oct 18, 2019
15b84ed
Missing return
davidsvantesson Oct 18, 2019
a7562d1
Refactor of CanBeAssigned check.
davidsvantesson Oct 19, 2019
075c7c6
Clarify function names (toggle rather than update/change), and clean up.
davidsvantesson Oct 19, 2019
e0b2648
Merge remote-tracking branch 'upstream/master' into mail-assigned
davidsvantesson Oct 19, 2019
0d5202c
Fix review comments.
davidsvantesson Oct 20, 2019
e761790
Merge branch 'master' into mail-assigned
lunny Oct 20, 2019
abc21a5
Flash error if assignees was not added when creating issue/pr
davidsvantesson Oct 21, 2019
430a3c8
Generate error if assignee users doesn't exist
davidsvantesson Oct 23, 2019
4c10b70
Merge branch 'master' into mail-assigned
lunny Oct 25, 2019
152fef9
Merge branch 'master' into mail-assigned
davidsvantesson Oct 25, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 4 additions & 47 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,6 @@ type NewIssueOptions struct {
Repo *Repository
Issue *Issue
LabelIDs []int64
AssigneeIDs []int64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removed this?

Copy link
Contributor Author

@davidsvantesson davidsvantesson Oct 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved adding assignees outside the creation of new issues. Reason is that each added assignee creates a new comment (issue event) which is used in the notification.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding assignee comment should be in the transaction but not in the notifications?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand now. The added assignee comment is used when sending the notification (the comment hash is included in the mail).

Copy link
Member

@guillep2k guillep2k Oct 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lunny If I've read that right, it's not a comment in the sense of a row in the comment table but a message for the contents of the e-mail. @davidsvantesson, perhaps it should be renamed to message to avoid confusion with the existing model/structure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I still misunderstand you. Adding an assignee creates a Comment in the database of type CommentTypeAssignees. CommentTypeComment is another type. Maybe IssueEvent would have been a better name to reflect it is not only comments. Anyhow the Comment (of type Assignees) is used when constructing the e-mail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidsvantesson OK, sorry, I got confused about that. I understand @lunny's concern then. However, I'm not sure I'd block the creation of the issue because of an error while doing the assignments. That's why I think the error Flash is useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input data is validated before issue creation, but it is of course possible it can still fail because of unconsidered reasons / bugs. I think it is a good idea to use error flash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an error flash now specifically for the case if add assignees fail when creating new issues/pr. Other error flashes I think can be for another PR.
I am not sure how to handle it for API creation. Right now it can potentially give error although the issue itself is created.

Attachments []string // In UUID format.
IsPull bool
}
Expand All @@ -918,40 +917,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
}

// 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 !isAdded {
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
}
}

// Check for and validate assignees
if len(opts.AssigneeIDs) > 0 {
for _, assigneeID := range opts.AssigneeIDs {
user, err := getUserByID(e, assigneeID)
if err != nil {
return fmt.Errorf("getUserByID [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
}
valid, err := canBeAssigned(e, user, opts.Repo)
if err != nil {
return fmt.Errorf("canBeAssigned [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
}
if !valid {
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
}
}
}

// Milestone and assignee validation should happen before insert actual object.
// Milestone validation should happen before insert actual object.
if _, err := e.SetExpr("`index`", "coalesce(MAX(`index`),0)+1").
Where("repo_id=?", opts.Issue.RepoID).
Insert(opts.Issue); err != nil {
Expand All @@ -972,14 +938,6 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
}

// Insert the assignees
for _, assigneeID := range opts.AssigneeIDs {
err = opts.Issue.changeAssignee(e, doer, assigneeID, true)
if err != nil {
return err
}
}

if opts.IsPull {
_, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID)
} else {
Expand Down Expand Up @@ -1037,11 +995,11 @@ 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, assigneeIDs []int64, uuids []string) (err error) {
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
// Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887
i := 0
for {
if err = newIssueAttempt(repo, issue, labelIDs, assigneeIDs, uuids); err == nil {
if err = newIssueAttempt(repo, issue, labelIDs, uuids); err == nil {
return nil
}
if !IsErrNewIssueInsert(err) {
Expand All @@ -1055,7 +1013,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []in
return fmt.Errorf("NewIssue: too many errors attempting to insert the new issue. Last error was: %v", err)
}

func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
Expand All @@ -1067,7 +1025,6 @@ func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeI
Issue: issue,
LabelIDs: labelIDs,
Attachments: uuids,
AssigneeIDs: assigneeIDs,
}); err != nil {
if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
return err
Expand Down
138 changes: 54 additions & 84 deletions models/issue_assignees.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ func getAssigneesByIssue(e Engine, issue *Issue) (assignees []*User, err error)

// 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
return isUserAssignedToIssue(x, issue, user)
}

func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool, err error) {
return e.Get(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
}

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

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 {
if _, _, err := issue.ToggleAssignee(doer, assignee.ID); err != nil {
return err
}
}
Expand Down Expand Up @@ -110,73 +113,56 @@ func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
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) {
// ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
func (issue *Issue) ToggleAssignee(doer *User, assigneeID int64) (removed bool, comment *Comment, err error) {
sess := x.NewSession()
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
defer sess.Close()

if err := sess.Begin(); err != nil {
return err
return false, nil, err
}

if err := issue.changeAssignee(sess, doer, assigneeID, false); err != nil {
return err
removed, comment, err = issue.toggleAssignee(sess, doer, assigneeID, false)
if err != nil {
return false, nil, err
}

if err := sess.Commit(); err != nil {
return err
return false, nil, err
}

go HookQueue.Add(issue.RepoID)
return nil

return removed, comment, nil
}

func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (err error) {
// Update the assignee
removed, err := updateIssueAssignee(sess, issue, assigneeID)
func (issue *Issue) toggleAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) {
removed, err = toggleUserAssignee(sess, issue, assigneeID)
if err != nil {
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
}

// Repo infos
if err = issue.loadRepo(sess); err != nil {
return fmt.Errorf("loadRepo: %v", err)
return false, nil, fmt.Errorf("loadRepo: %v", err)
}

// Comment
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil {
return fmt.Errorf("createAssigneeComment: %v", err)
comment, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed)
if err != nil {
return false, nil, fmt.Errorf("createAssigneeComment: %v", err)
}

// if pull request is in the middle of creation - don't call webhook
if isCreate {
return nil
return removed, comment, err
}

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

if err = issue.loadPullRequest(sess); err != nil {
return fmt.Errorf("loadPullRequest: %v", err)
return false, nil, fmt.Errorf("loadPullRequest: %v", err)
}
issue.PullRequest.Issue = issue
apiPullRequest := &api.PullRequestPayload{
Expand All @@ -190,9 +176,10 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
} else {
apiPullRequest.Action = api.HookIssueAssigned
}
// Assignee comment triggers a webhook
if err := prepareWebhooks(sess, issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhooks part doesn't seem related with changing assignees. I thihk it should be moved to another function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it is, below is help text for PR trigger event. 😉

Pull request opened, closed, reopened, edited, approved, rejected, review comment, assigned, unassigned, label updated, label cleared or synchronized.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it is, below is help text for PR trigger event. 😉

Pull request opened, closed, reopened, edited, approved, rejected, review comment, assigned, unassigned, label updated, label cleared or synchronized.

Oh, that's not what I meant. 😄 What I meant is that this code seems to belong to services rather than models. Anyway, it was here before, so refactoring should be for another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, yes you are probably right, but I will leave out that refactoring from this PR. I think we should have some design guidelines what type of code should be where. I think I've started to understand Gitea's code structure now....

log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
return nil
return false, nil, err
}
} else {
mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypeIssues)
Expand All @@ -208,67 +195,50 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in
} else {
apiIssue.Action = api.HookIssueAssigned
}
// Assignee comment triggers a webhook
if err := prepareWebhooks(sess, issue.Repo, HookEventIssues, apiIssue); err != nil {
log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
return nil
return false, nil, err
}
}
return nil
return removed, comment, 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
// toggles user assignee state in database
func toggleUserAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {

// 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)
}
// Check if the user exists
assignee, err := getUserByID(e, assigneeID)
if err != nil {
return false, err
}

// Loop through all assignees to add them
for _, assigneeName := range multipleAssignees {
assignee, err := GetUserByName(assigneeName)
if err != nil {
return err
// Check if the submitted user is already assigned, if yes delete him otherwise add him
var i int
for i = 0; i < len(issue.Assignees); i++ {
if issue.Assignees[i].ID == assigneeID {
break
}

allNewAssignees = append(allNewAssignees, assignee)
}

// Delete all old assignees not passed
if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil {
return err
}
assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}

// 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)
toBeDeleted := i < len(issue.Assignees)
if toBeDeleted {
issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i:]...)
_, err = e.Delete(assigneeIn)
if err != nil {
return err
return toBeDeleted, err
}
} else {
issue.Assignees = append(issue.Assignees, assignee)
_, err = e.Insert(assigneeIn)
if err != nil {
return toBeDeleted, err
}
}

return
return toBeDeleted, nil
}

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

// Get the IDs of all assignees
assigneeIDs = GetUserIDsByNames(multipleAssignees)
assigneeIDs, err = GetUserIDsByNames(multipleAssignees, false)

return
}
6 changes: 3 additions & 3 deletions models/issue_assignees_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ func TestUpdateAssignee(t *testing.T) {
// Assign multiple users
user2, err := GetUserByID(2)
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user2.ID)
_, _, err = issue.ToggleAssignee(&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)
_, _, err = issue.ToggleAssignee(&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)
_, _, err = issue.ToggleAssignee(&User{ID: 1}, user1.ID)
assert.NoError(t, err)

// Check if he got removed
Expand Down
2 changes: 1 addition & 1 deletion models/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func testInsertIssue(t *testing.T, title, content string) {
Title: title,
Content: content,
}
err := NewIssue(repo, &issue, nil, nil, nil)
err := NewIssue(repo, &issue, nil, nil)
assert.NoError(t, err)

var newIssue Issue
Expand Down
Loading