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

Add Ability for User to Customize Email Notification Frequency #7813

Merged
merged 15 commits into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
2 changes: 2 additions & 0 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ MAX_FILE_SIZE = 1048576
[admin]
; Disallow regular (non-admin) users from creating organizations.
DISABLE_REGULAR_ORG_CREATION = false
; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
DEFAULT_EMAIL_NOTIFICATIONS = enabled

[security]
; Whether the installer is disabled
Expand Down
3 changes: 3 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request.
- `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed.

## Admin (`admin`)
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled

## Security (`security`)

- `INSTALL_LOCK`: **false**: Disallow access to the install page.
Expand Down
9 changes: 9 additions & 0 deletions models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
name: user1
full_name: User One
email: user1@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -22,6 +23,7 @@
full_name: " < U<se>r Tw<o > >< "
email: user2@example.com
keep_email_private: true
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -40,6 +42,7 @@
name: user3
full_name: " <<<< >> >> > >> > >>> >> "
email: user3@example.com
email_notifications_preference: onmention
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
Expand All @@ -56,6 +59,7 @@
name: user4
full_name: " "
email: user4@example.com
email_notifications_preference: onmention
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -72,6 +76,7 @@
name: user5
full_name: User Five
email: user5@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -89,6 +94,7 @@
name: user6
full_name: User Six
email: user6@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
Expand All @@ -105,6 +111,7 @@
name: user7
full_name: User Seven
email: user7@example.com
email_notifications_preference: disabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
Expand All @@ -121,6 +128,7 @@
name: user8
full_name: User Eight
email: user8@example.com
email_notifications_preference: enabled
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand All @@ -138,6 +146,7 @@
name: user9
full_name: User Nine
email: user9@example.com
email_notifications_preference: onmention
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
Expand Down
8 changes: 4 additions & 4 deletions models/issue_mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,17 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
}
if to.IsOrganization() {
if to.IsOrganization() || to.EmailNotifications() != EmailNotificationsEnabled {
continue
}

tos = append(tos, to.Email)
names = append(names, to.Name)
}
for i := range participants {
if participants[i].ID == doer.ID {
continue
} else if com.IsSliceContainsStr(names, participants[i].Name) {
if participants[i].ID == doer.ID ||
com.IsSliceContainsStr(names, participants[i].Name) ||
participants[i].EmailNotifications() != EmailNotificationsEnabled {
continue
}

Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ var migrations = []Migration{
NewMigration("add index on owner_id of repository and type, review_id of comment", addIndexOnRepositoryAndComment),
// v92 -> v93
NewMigration("remove orphaned repository index statuses", removeLingeringIndexStatus),
// v93 -> v94
NewMigration("add email notification enabled preference to user", addEmailNotificationEnabledToUser),
}

// Migrate database to current version
Expand Down
16 changes: 16 additions & 0 deletions models/migrations/v93.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2019 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 "github.com/go-xorm/xorm"

func addEmailNotificationEnabledToUser(x *xorm.Engine) error {
// Issue see models/user.go
gary-kim marked this conversation as resolved.
Show resolved Hide resolved
type User struct {
EmailNotificationsPreference string `xorm:"NOT NULL DEFAULT 'enabled'"`
gary-kim marked this conversation as resolved.
Show resolved Hide resolved
}

return x.Sync2(new(User))
}
35 changes: 29 additions & 6 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ const (
algoScrypt = "scrypt"
algoArgon2 = "argon2"
algoPbkdf2 = "pbkdf2"

// EmailNotificationsEnabled indicates that the user would like to receive all email notifications
EmailNotificationsEnabled = "enabled"
// EmailNotificationsOnMention indicates that the user would like to be notified via email when mentioned.
EmailNotificationsOnMention = "onmention"
// EmailNotificationsDisabled indicates that the user would not like to be notified via email.
EmailNotificationsDisabled = "disabled"
)

var (
Expand Down Expand Up @@ -87,10 +94,11 @@ type User struct {
Name string `xorm:"UNIQUE NOT NULL"`
FullName string
// Email is the primary email address (to be used for communication)
Email string `xorm:"NOT NULL"`
KeepEmailPrivate bool
Passwd string `xorm:"NOT NULL"`
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`
Email string `xorm:"NOT NULL"`
KeepEmailPrivate bool
EmailNotificationsPreference string `xorm:"NOT NULL DEFAULT 'enabled'"`
Passwd string `xorm:"NOT NULL"`
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'pbkdf2'"`

// MustChangePassword is an attribute that determines if a user
// is to change his/her password after registration.
Expand Down Expand Up @@ -719,6 +727,19 @@ func (u *User) IsMailable() bool {
return u.IsActive
}

// EmailNotifications returns the User's email notification preference
func (u *User) EmailNotifications() string {
return u.EmailNotificationsPreference
}

// SetEmailNotifications sets the user's email notification preference
func (u *User) SetEmailNotifications(set string) {
u.EmailNotificationsPreference = set
gary-kim marked this conversation as resolved.
Show resolved Hide resolved
if err := UpdateUserCols(u, "email_notifications_preference"); err != nil {
log.Error("SetEmailNotifications: %v", err)
}
}

func isUserExist(e Engine, uid int64, name string) (bool, error) {
if len(name) == 0 {
return false, nil
Expand Down Expand Up @@ -868,6 +889,7 @@ func CreateUser(u *User) (err error) {
}
u.HashPassword(u.Passwd)
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
u.MaxRepoCreation = -1
u.Theme = setting.UI.DefaultTheme

Expand Down Expand Up @@ -1253,7 +1275,8 @@ func getUserByName(e Engine, name string) (*User, error) {
return u, nil
}

// GetUserEmailsByNames returns a list of e-mails corresponds to names.
// GetUserEmailsByNames returns a list of e-mails corresponds to names of users
// that have their email notifications set to enabled or onmention.
func GetUserEmailsByNames(names []string) []string {
return getUserEmailsByNames(x, names)
}
Expand All @@ -1265,7 +1288,7 @@ func getUserEmailsByNames(e Engine, names []string) []string {
if err != nil {
continue
}
if u.IsMailable() {
if u.IsMailable() && u.EmailNotifications() != EmailNotificationsDisabled {
mails = append(mails, u.Email)
}
}
Expand Down
33 changes: 33 additions & 0 deletions models/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ func TestGetUserEmailsByNames(t *testing.T) {
// ignore none active user email
assert.Equal(t, []string{"user8@example.com"}, GetUserEmailsByNames([]string{"user8", "user9"}))
assert.Equal(t, []string{"user8@example.com", "user5@example.com"}, GetUserEmailsByNames([]string{"user8", "user5"}))

assert.Equal(t, []string{"user8@example.com"}, GetUserEmailsByNames([]string{"user8", "user7"}))
}

func TestUser_APIFormat(t *testing.T) {
Expand Down Expand Up @@ -196,6 +198,37 @@ func TestDeleteUser(t *testing.T) {
test(11)
}

func TestEmailNotificationPreferences(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
for _, test := range []struct {
expected string
userID int64
}{
{EmailNotificationsEnabled, 1},
{EmailNotificationsEnabled, 2},
{EmailNotificationsOnMention, 3},
{EmailNotificationsOnMention, 4},
{EmailNotificationsEnabled, 5},
{EmailNotificationsEnabled, 6},
{EmailNotificationsDisabled, 7},
{EmailNotificationsEnabled, 8},
{EmailNotificationsOnMention, 9},
} {
user := AssertExistsAndLoadBean(t, &User{ID: test.userID}).(*User)
assert.Equal(t, test.expected, user.EmailNotifications())

// Try all possible settings
user.SetEmailNotifications(EmailNotificationsEnabled)
assert.Equal(t, EmailNotificationsEnabled, user.EmailNotifications())

user.SetEmailNotifications(EmailNotificationsOnMention)
assert.Equal(t, EmailNotificationsOnMention, user.EmailNotifications())

user.SetEmailNotifications(EmailNotificationsDisabled)
assert.Equal(t, EmailNotificationsDisabled, user.EmailNotifications())
}
}

func TestHashPasswordDeterministic(t *testing.T) {
b := make([]byte, 16)
rand.Read(b)
Expand Down
4 changes: 4 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ var (
// Admin settings
Admin struct {
DisableRegularOrgCreation bool
DefaultEmailNotification string
}

// Picture settings
Expand Down Expand Up @@ -762,6 +763,9 @@ func NewContext() {
}
}

sec = Cfg.Section("admin")
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")

sec = Cfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
SecretKey = sec.Key("SECRET_KEY").MustString("!#@FDEWREWR&*(")
Expand Down
9 changes: 9 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,11 @@ confirm_delete_account = Confirm Deletion
delete_account_title = Delete User Account
delete_account_desc = Are you sure you want to permanently delete this user account?

email_notifications.enable = Enable Email Notifications
email_notifications.onmention = Only Email on Mention
email_notifications.disable = Disable Email Notifications
email_notifications.submit = Set Email Preference

[repo]
owner = Owner
repo_name = Repository Name
Expand Down Expand Up @@ -1126,6 +1131,10 @@ settings.basic_settings = Basic Settings
settings.mirror_settings = Mirror Settings
settings.sync_mirror = Synchronize Now
settings.mirror_sync_in_progress = Mirror synchronization is in progress. Check back in a minute.
settings.email_notifications.enable = Enable Email Notifications
settings.email_notifications.onmention = Only Email on Mention
settings.email_notifications.disable = Disable Email Notifications
settings.email_notifications.submit = Set Email Preference
settings.site = Website
settings.update_settings = Update Settings
settings.advanced_settings = Advanced Settings
Expand Down
2 changes: 1 addition & 1 deletion public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ footer .ui.left,footer .ui.right{line-height:40px}
.ui.form .dropzone .dz-error-message{top:140px}
.settings .content{margin-top:2px}
.settings .content .segment,.settings .content>.header{box-shadow:0 1px 2px 0 rgba(34,36,38,.15)}
.settings .list>.item .green{color:#21ba45}
.settings .list>.item .green:not(.ui.button){color:#21ba45}
.settings .list>.item:not(:first-child){border-top:1px solid #eaeaea;padding:1rem;margin:15px -1rem -1rem -1rem}
.settings .list>.item>.mega-octicon{display:table-cell}
.settings .list>.item>.mega-octicon+.content{display:table-cell;padding:0 0 0 .5em;vertical-align:top}
Expand Down
2 changes: 1 addition & 1 deletion public/less/_repository.less
Original file line number Diff line number Diff line change
Expand Up @@ -2137,7 +2137,7 @@

.list {
> .item {
.green {
.green:not(.ui.button) {
color: #21ba45;
}

Expand Down
14 changes: 14 additions & 0 deletions routers/user/setting/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func Account(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.User.Email
ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()

loadAccountData(ctx)

Expand Down Expand Up @@ -81,6 +82,19 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Set Email Notification Preference
if ctx.Query("_method") == "NOTIFICATION" {
preference := ctx.Query("preference")
if !(preference == models.EmailNotificationsEnabled ||
preference == models.EmailNotificationsOnMention ||
preference == models.EmailNotificationsDisabled) {
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.User.Name)
gary-kim marked this conversation as resolved.
Show resolved Hide resolved
}
ctx.User.SetEmailNotifications(preference)
gary-kim marked this conversation as resolved.
Show resolved Hide resolved
log.Trace("Email notifications preference made %s: %s", preference, ctx.User.Name)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}

if ctx.HasError() {
loadAccountData(ctx)
Expand Down
27 changes: 25 additions & 2 deletions templates/user/settings/account.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,30 @@
<div class="ui attached segment">
<div class="ui email list">
<div class="item">
{{.i18n.Tr "settings.email_desc"}}
<form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post">
{{.i18n.Tr "settings.email_desc"}}
<div class="right floated content">
<div class="field">
<button class="ui green button">{{$.i18n.Tr "settings.email_notifications.submit"}}</button>
</div>
</div>
<div class="right floated content">
{{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="NOTIFICATION">
<div class="field">
<div class="ui selection dropdown" tabindex="0">
<input name="preference" type="hidden" value="{{.EmailNotificationsPreference}}">
<i class="dropdown icon"></i>
<div class="text">{{$.i18n.Tr "settings.email_notifications"}}</div>
<div class="menu">
<div data-value="enabled" class="{{if eq .EmailNotificationsPreference "enabled"}}active selected {{end}}item">{{$.i18n.Tr "settings.email_notifications.enable"}}</div>
<div data-value="onmention" class="{{if eq .EmailNotificationsPreference "onmention"}}active selected {{end}}item">{{$.i18n.Tr "settings.email_notifications.onmention"}}</div>
<div data-value="disabled" class="{{if eq .EmailNotificationsPreference "disabled"}}active selected {{end}}item">{{$.i18n.Tr "settings.email_notifications.disable"}}</div>
</div>
</div>
</div>
</div>
</form>
</div>
{{range .Emails}}
<div class="item">
Expand Down Expand Up @@ -103,7 +126,7 @@
<i class="dropdown icon"></i>
<div class="text">
{{range $i,$a := .AllThemes}}
{{if eq $.SignedUser.Theme $a}}{{$a}}{{end}}
{{if eq $.SignedUser.Theme $a}}{{$a}}{{end}}
{{end}}
</div>

Expand Down