Prevent sending emails and notifications to inactive users (#2384)
* Filter inactive users before sending emails or creating browser notifications Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com> * fix formatting issues Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com> * included requested changes Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com> * optimized database queries * rebasing new master and add tablenames for clarification in xorm queries * remove escaped quotationmarks using backticks Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com>
This commit is contained in:
parent
b496e3e1cc
commit
d766d0c4e0
13 changed files with 89 additions and 25 deletions
|
@ -9,7 +9,7 @@
|
||||||
num_pulls: 2
|
num_pulls: 2
|
||||||
num_closed_pulls: 0
|
num_closed_pulls: 0
|
||||||
num_milestones: 2
|
num_milestones: 2
|
||||||
num_watches: 2
|
num_watches: 3
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 2
|
id: 2
|
||||||
|
|
|
@ -7,3 +7,8 @@
|
||||||
id: 2
|
id: 2
|
||||||
user_id: 4
|
user_id: 4
|
||||||
repo_id: 1
|
repo_id: 1
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 3
|
||||||
|
user_id: 10
|
||||||
|
repo_id: 1
|
||||||
|
|
|
@ -1204,8 +1204,11 @@ func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
|
||||||
func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
|
func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
|
||||||
userIDs := make([]int64, 0, 5)
|
userIDs := make([]int64, 0, 5)
|
||||||
if err := e.Table("comment").Cols("poster_id").
|
if err := e.Table("comment").Cols("poster_id").
|
||||||
Where("issue_id = ?", issueID).
|
Where("`comment`.issue_id = ?", issueID).
|
||||||
And("type = ?", CommentTypeComment).
|
And("`comment`.type = ?", CommentTypeComment).
|
||||||
|
And("`user`.is_active = ?", true).
|
||||||
|
And("`user`.prohibit_login = ?", false).
|
||||||
|
Join("INNER", "user", "`user`.id = `comment`.poster_id").
|
||||||
Distinct("poster_id").
|
Distinct("poster_id").
|
||||||
Find(&userIDs); err != nil {
|
Find(&userIDs); err != nil {
|
||||||
return nil, fmt.Errorf("get poster IDs: %v", err)
|
return nil, fmt.Errorf("get poster IDs: %v", err)
|
||||||
|
|
|
@ -36,9 +36,13 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, comment
|
||||||
return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
|
return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In case the issue poster is not watching the repository,
|
// In case the issue poster is not watching the repository and is active,
|
||||||
// even if we have duplicated in watchers, can be safely filtered out.
|
// even if we have duplicated in watchers, can be safely filtered out.
|
||||||
if issue.PosterID != doer.ID {
|
poster, err := GetUserByID(issue.PosterID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetUserByID [%d]: %v", issue.PosterID, err)
|
||||||
|
}
|
||||||
|
if issue.PosterID != doer.ID && poster.IsActive && !poster.ProhibitLogin {
|
||||||
participants = append(participants, issue.Poster)
|
participants = append(participants, issue.Poster)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,8 @@ func TestGetParticipantsByIssueID(t *testing.T) {
|
||||||
// User 1 is issue1 poster (see fixtures/issue.yml)
|
// User 1 is issue1 poster (see fixtures/issue.yml)
|
||||||
// User 2 only labeled issue1 (see fixtures/comment.yml)
|
// User 2 only labeled issue1 (see fixtures/comment.yml)
|
||||||
// Users 3 and 5 made actual comments (see fixtures/comment.yml)
|
// Users 3 and 5 made actual comments (see fixtures/comment.yml)
|
||||||
checkParticipants(1, []int{3, 5})
|
// User 3 is inactive, thus not active participant
|
||||||
|
checkParticipants(1, []int{5})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssue_AddLabel(t *testing.T) {
|
func TestIssue_AddLabel(t *testing.T) {
|
||||||
|
|
|
@ -90,7 +90,10 @@ func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) {
|
||||||
|
|
||||||
func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) {
|
func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) {
|
||||||
err = e.
|
err = e.
|
||||||
Where("issue_id = ?", issueID).
|
Where("`issue_watch`.issue_id = ?", issueID).
|
||||||
|
And("`user`.is_active = ?", true).
|
||||||
|
And("`user`.prohibit_login = ?", false).
|
||||||
|
Join("INNER", "user", "`user`.id = `issue_watch`.user_id").
|
||||||
Find(&watches)
|
Find(&watches)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,11 @@ func TestGetIssueWatchers(t *testing.T) {
|
||||||
|
|
||||||
iws, err := GetIssueWatchers(1)
|
iws, err := GetIssueWatchers(1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
// Watcher is inactive, thus 0
|
||||||
|
assert.Equal(t, 0, len(iws))
|
||||||
|
|
||||||
|
iws, err = GetIssueWatchers(2)
|
||||||
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, len(iws))
|
assert.Equal(t, 1, len(iws))
|
||||||
|
|
||||||
iws, err = GetIssueWatchers(5)
|
iws, err = GetIssueWatchers(5)
|
||||||
|
|
|
@ -130,6 +130,8 @@ var migrations = []Migration{
|
||||||
NewMigration("adds time tracking and stopwatches", addTimetracking),
|
NewMigration("adds time tracking and stopwatches", addTimetracking),
|
||||||
// v40 -> v41
|
// v40 -> v41
|
||||||
NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
|
NewMigration("migrate protected branch struct", migrateProtectedBranchStruct),
|
||||||
|
// v41 -> v42
|
||||||
|
NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
42
models/migrations/v41.go
Normal file
42
models/migrations/v41.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright 2017 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addDefaultValueToUserProhibitLogin(x *xorm.Engine) (err error) {
|
||||||
|
user := &models.User{
|
||||||
|
ProhibitLogin: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := x.Where("`prohibit_login` IS NULL").Cols("prohibit_login").Update(user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialect := x.Dialect().DriverName()
|
||||||
|
|
||||||
|
switch dialect {
|
||||||
|
case "mysql":
|
||||||
|
_, err = x.Exec("ALTER TABLE user MODIFY `prohibit_login` tinyint(1) NOT NULL DEFAULT 0")
|
||||||
|
case "postgres":
|
||||||
|
_, err = x.Exec("ALTER TABLE \"user\" ALTER COLUMN `prohibit_login` SET NOT NULL, ALTER COLUMN `prohibit_login` SET DEFAULT false")
|
||||||
|
case "mssql":
|
||||||
|
// xorm already set DEFAULT 0 for data type BIT in mssql
|
||||||
|
_, err = x.Exec(`ALTER TABLE [user] ALTER COLUMN "prohibit_login" BIT NOT NULL`)
|
||||||
|
case "sqlite3":
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error changing user prohibit_login column definition: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -16,9 +16,8 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) {
|
||||||
|
|
||||||
assert.NoError(t, CreateOrUpdateIssueNotifications(issue, 2))
|
assert.NoError(t, CreateOrUpdateIssueNotifications(issue, 2))
|
||||||
|
|
||||||
notf := AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification)
|
// Two watchers are inactive, thus only notification for user 10 is created
|
||||||
assert.Equal(t, NotificationStatusUnread, notf.Status)
|
notf := AssertExistsAndLoadBean(t, &Notification{UserID: 10, IssueID: issue.ID}).(*Notification)
|
||||||
notf = AssertExistsAndLoadBean(t, &Notification{UserID: 4, IssueID: issue.ID}).(*Notification)
|
|
||||||
assert.Equal(t, NotificationStatusUnread, notf.Status)
|
assert.Equal(t, NotificationStatusUnread, notf.Status)
|
||||||
CheckConsistencyFor(t, &Issue{ID: issue.ID})
|
CheckConsistencyFor(t, &Issue{ID: issue.ID})
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,11 @@ func WatchRepo(userID, repoID int64, watch bool) (err error) {
|
||||||
|
|
||||||
func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
|
func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
|
||||||
watches := make([]*Watch, 0, 10)
|
watches := make([]*Watch, 0, 10)
|
||||||
return watches, e.Find(&watches, &Watch{RepoID: repoID})
|
return watches, e.Where("`watch`.repo_id=?", repoID).
|
||||||
|
And("`user`.is_active=?", true).
|
||||||
|
And("`user`.prohibit_login=?", false).
|
||||||
|
Join("INNER", "user", "`user`.id = `watch`.user_id").
|
||||||
|
Find(&watches)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWatchers returns all watchers of given repository.
|
// GetWatchers returns all watchers of given repository.
|
||||||
|
|
|
@ -40,7 +40,8 @@ func TestGetWatchers(t *testing.T) {
|
||||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||||
watches, err := GetWatchers(repo.ID)
|
watches, err := GetWatchers(repo.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watches, repo.NumWatches)
|
// Two watchers are inactive, thus minus 2
|
||||||
|
assert.Len(t, watches, repo.NumWatches-2)
|
||||||
for _, watch := range watches {
|
for _, watch := range watches {
|
||||||
assert.EqualValues(t, repo.ID, watch.RepoID)
|
assert.EqualValues(t, repo.ID, watch.RepoID)
|
||||||
}
|
}
|
||||||
|
@ -77,22 +78,17 @@ func TestNotifyWatchers(t *testing.T) {
|
||||||
}
|
}
|
||||||
assert.NoError(t, NotifyWatchers(action))
|
assert.NoError(t, NotifyWatchers(action))
|
||||||
|
|
||||||
AssertExistsAndLoadBean(t, &Action{
|
// Two watchers are inactive, thus action is only created for user 8, 10
|
||||||
ActUserID: action.ActUserID,
|
|
||||||
UserID: 1,
|
|
||||||
RepoID: action.RepoID,
|
|
||||||
OpType: action.OpType,
|
|
||||||
})
|
|
||||||
AssertExistsAndLoadBean(t, &Action{
|
|
||||||
ActUserID: action.ActUserID,
|
|
||||||
UserID: 4,
|
|
||||||
RepoID: action.RepoID,
|
|
||||||
OpType: action.OpType,
|
|
||||||
})
|
|
||||||
AssertExistsAndLoadBean(t, &Action{
|
AssertExistsAndLoadBean(t, &Action{
|
||||||
ActUserID: action.ActUserID,
|
ActUserID: action.ActUserID,
|
||||||
UserID: 8,
|
UserID: 8,
|
||||||
RepoID: action.RepoID,
|
RepoID: action.RepoID,
|
||||||
OpType: action.OpType,
|
OpType: action.OpType,
|
||||||
})
|
})
|
||||||
|
AssertExistsAndLoadBean(t, &Action{
|
||||||
|
ActUserID: action.ActUserID,
|
||||||
|
UserID: 10,
|
||||||
|
RepoID: action.RepoID,
|
||||||
|
OpType: action.OpType,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ type User struct {
|
||||||
AllowGitHook bool
|
AllowGitHook bool
|
||||||
AllowImportLocal bool // Allow migrate repository by local path
|
AllowImportLocal bool // Allow migrate repository by local path
|
||||||
AllowCreateOrganization bool `xorm:"DEFAULT true"`
|
AllowCreateOrganization bool `xorm:"DEFAULT true"`
|
||||||
ProhibitLogin bool
|
ProhibitLogin bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
|
Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
|
||||||
|
|
Reference in a new issue