* Send notifications to partecipants in issue comments Closes #1216 Includes test (still failing) * Do not include "labelers" to participants Fix test to expect what GetParticipants returntags/v1.21.12.1
| @@ -2,6 +2,18 @@ | |||||
| id: 1 | id: 1 | ||||
| type: 7 # label | type: 7 # label | ||||
| poster_id: 2 | poster_id: 2 | ||||
| issue_id: 1 | |||||
| issue_id: 1 # in repo_id 1 | |||||
| label_id: 1 | label_id: 1 | ||||
| content: "1" | content: "1" | ||||
| - | |||||
| id: 2 | |||||
| type: 0 # comment | |||||
| poster_id: 3 # user not watching (see watch.yml) | |||||
| issue_id: 1 # in repo_id 1 | |||||
| content: "good work!" | |||||
| - | |||||
| id: 3 | |||||
| type: 0 # comment | |||||
| poster_id: 5 # user not watching (see watch.yml) | |||||
| issue_id: 1 # in repo_id 1 | |||||
| content: "meh..." | |||||
| @@ -8,7 +8,7 @@ | |||||
| content: content1 | content: content1 | ||||
| is_closed: false | is_closed: false | ||||
| is_pull: false | is_pull: false | ||||
| num_comments: 0 | |||||
| num_comments: 2 | |||||
| created_unix: 946684800 | created_unix: 946684800 | ||||
| updated_unix: 978307200 | updated_unix: 978307200 | ||||
| @@ -1134,6 +1134,24 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) { | |||||
| return issues, nil | return issues, nil | ||||
| } | } | ||||
| // GetParticipantsByIssueID returns all users who are participated in comments of an issue. | |||||
| func GetParticipantsByIssueID(issueID int64) ([]*User, error) { | |||||
| userIDs := make([]int64, 0, 5) | |||||
| if err := x.Table("comment").Cols("poster_id"). | |||||
| Where("issue_id = ?", issueID). | |||||
| And("type = ?", CommentTypeComment). | |||||
| Distinct("poster_id"). | |||||
| Find(&userIDs); err != nil { | |||||
| return nil, fmt.Errorf("get poster IDs: %v", err) | |||||
| } | |||||
| if len(userIDs) == 0 { | |||||
| return nil, nil | |||||
| } | |||||
| users := make([]*User, 0, len(userIDs)) | |||||
| return users, x.In("id", userIDs).Find(&users) | |||||
| } | |||||
| // UpdateIssueMentions extracts mentioned people from content and | // UpdateIssueMentions extracts mentioned people from content and | ||||
| // updates issue-user relations for them. | // updates issue-user relations for them. | ||||
| func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error { | func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error { | ||||
| @@ -19,15 +19,27 @@ func (issue *Issue) mailSubject() string { | |||||
| } | } | ||||
| // mailIssueCommentToParticipants can be used for both new issue creation and comment. | // mailIssueCommentToParticipants can be used for both new issue creation and comment. | ||||
| // This function sends two list of emails: | |||||
| // 1. Repository watchers and users who are participated in comments. | |||||
| // 2. Users who are not in 1. but get mentioned in current issue/comment. | |||||
| func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) error { | func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) error { | ||||
| if !setting.Service.EnableNotifyMail { | if !setting.Service.EnableNotifyMail { | ||||
| return nil | return nil | ||||
| } | } | ||||
| // Mail watchers. | |||||
| watchers, err := GetWatchers(issue.RepoID) | watchers, err := GetWatchers(issue.RepoID) | ||||
| if err != nil { | if err != nil { | ||||
| return fmt.Errorf("GetWatchers [%d]: %v", issue.RepoID, err) | |||||
| return fmt.Errorf("GetWatchers [repo_id: %d]: %v", issue.RepoID, err) | |||||
| } | |||||
| participants, err := GetParticipantsByIssueID(issue.ID) | |||||
| if err != nil { | |||||
| return fmt.Errorf("GetParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err) | |||||
| } | |||||
| // In case the issue poster is not watching the repository, | |||||
| // even if we have duplicated in watchers, can be safely filtered out. | |||||
| if issue.PosterID != doer.ID { | |||||
| participants = append(participants, issue.Poster) | |||||
| } | } | ||||
| tos := make([]string, 0, len(watchers)) // List of email addresses. | tos := make([]string, 0, len(watchers)) // List of email addresses. | ||||
| @@ -48,6 +60,16 @@ func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) | |||||
| tos = append(tos, to.Email) | tos = append(tos, to.Email) | ||||
| names = append(names, to.Name) | names = append(names, to.Name) | ||||
| } | } | ||||
| for i := range participants { | |||||
| if participants[i].ID == doer.ID { | |||||
| continue | |||||
| } else if com.IsSliceContainsStr(names, participants[i].Name) { | |||||
| continue | |||||
| } | |||||
| tos = append(tos, participants[i].Email) | |||||
| names = append(names, participants[i].Name) | |||||
| } | |||||
| SendIssueCommentMail(issue, doer, tos) | SendIssueCommentMail(issue, doer, tos) | ||||
| // Mail mentioned people and exclude watchers. | // Mail mentioned people and exclude watchers. | ||||
| @@ -5,6 +5,7 @@ | |||||
| package models | package models | ||||
| import ( | import ( | ||||
| "sort" | |||||
| "testing" | "testing" | ||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| @@ -58,3 +59,26 @@ func TestGetIssuesByIDs(t *testing.T) { | |||||
| testSuccess([]int64{1, 2, 3}, []int64{}) | testSuccess([]int64{1, 2, 3}, []int64{}) | ||||
| testSuccess([]int64{1, 2, 3}, []int64{NonexistentID}) | testSuccess([]int64{1, 2, 3}, []int64{NonexistentID}) | ||||
| } | } | ||||
| func TestGetParticipantsByIssueID(t *testing.T) { | |||||
| assert.NoError(t, PrepareTestDatabase()) | |||||
| checkPartecipants := func(issueID int64, userIDs []int) { | |||||
| partecipants, err := GetParticipantsByIssueID(issueID) | |||||
| if assert.NoError(t, err) { | |||||
| partecipantsIDs := make([]int,len(partecipants)) | |||||
| for i,u := range partecipants { partecipantsIDs[i] = int(u.ID) } | |||||
| sort.Ints(partecipantsIDs) | |||||
| sort.Ints(userIDs) | |||||
| assert.Equal(t, userIDs, partecipantsIDs) | |||||
| } | |||||
| } | |||||
| // User 1 is issue1 poster (see fixtures/issue.yml) | |||||
| // User 2 only labeled issue1 (see fixtures/comment.yml) | |||||
| // Users 3 and 5 made actual comments (see fixtures/comment.yml) | |||||
| checkPartecipants(1, []int{3,5}) | |||||
| } | |||||