Skip to content

Commit 1a402d8

Browse files
committed
Send notifications to group-specific email address
- Select notification email by walking up group/subgroup path - Add settings UI to set group email notification address - Add tests
1 parent 6d73ce6 commit 1a402d8

File tree

20 files changed

+182
-44
lines changed

20 files changed

+182
-44
lines changed

app/assets/javascripts/profile/profile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default class Profile {
3939

4040
bindEvents() {
4141
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
42+
$('.js-group-notification-email').on('change', this.submitForm);
4243
$('#user_notification_email').on('change', this.submitForm);
4344
$('#user_notified_of_own_activity').on('change', this.submitForm);
4445
this.form.on('submit', this.onSubmitForm);

app/assets/stylesheets/pages/notifications.scss

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@
44
.dropdown-menu {
55
@extend .dropdown-menu-right;
66
}
7+
8+
@include media-breakpoint-down(sm) {
9+
.notification-dropdown {
10+
width: 100%;
11+
}
12+
13+
.notification-form {
14+
display: block;
15+
}
16+
17+
.notifications-btn,
18+
.btn-group {
19+
width: 100%;
20+
}
21+
22+
.table-section {
23+
border-top: 0;
24+
min-height: unset;
25+
26+
&:not(:first-child) {
27+
padding-top: 0;
28+
}
29+
}
30+
31+
.update-notifications {
32+
width: 100%;
33+
}
34+
}
735
}
836

937
.notification {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class Profiles::GroupsController < Profiles::ApplicationController
4+
include RoutableActions
5+
6+
def update
7+
group = find_routable!(Group, params[:id])
8+
notification_setting = current_user.notification_settings.find_by(source: group) # rubocop: disable CodeReuse/ActiveRecord
9+
10+
if notification_setting.update(update_params)
11+
flash[:notice] = "Notification settings for #{group.name} saved"
12+
else
13+
flash[:alert] = "Failed to save new settings for #{group.name}"
14+
end
15+
16+
redirect_back_or_default(default: profile_notifications_path)
17+
end
18+
19+
private
20+
21+
def update_params
22+
params.require(:notification_setting).permit(:notification_email)
23+
end
24+
end

app/mailers/emails/issues.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def import_issues_csv_email(user_id, project_id, results)
8383
@project = Project.find(project_id)
8484
@results = results
8585

86-
mail(to: @user.notification_email, subject: subject('Imported issues')) do |format|
86+
mail(to: recipient(@user.id, @project.group), subject: subject('Imported issues')) do |format|
8787
format.html { render layout: 'mailer' }
8888
format.text { render layout: 'mailer' }
8989
end
@@ -103,7 +103,7 @@ def setup_issue_mail(issue_id, recipient_id, closed_via: nil)
103103
def issue_thread_options(sender_id, recipient_id, reason)
104104
{
105105
from: sender(sender_id),
106-
to: recipient(recipient_id),
106+
to: recipient(recipient_id, @project.group),
107107
subject: subject("#{@issue.title} (##{@issue.iid})"),
108108
'X-GitLab-NotificationReason' => reason
109109
}

app/mailers/emails/members.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ def member_invite_declined_email(member_source_type, source_id, invite_email, cr
5858
@member_source_type = member_source_type
5959
@member_source = member_source_class.find(source_id)
6060
@invite_email = invite_email
61-
inviter = User.find(created_by_id)
6261

63-
mail(to: inviter.notification_email,
62+
mail(to: recipient(created_by_id, member_source_type == 'project' ? @member_source.group : @member_source),
6463
subject: subject('Invitation declined'))
6564
end
6665

app/mailers/emails/merge_requests.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def setup_merge_request_mail(merge_request_id, recipient_id, present: false)
110110
def merge_request_thread_options(sender_id, recipient_id, reason = nil)
111111
{
112112
from: sender(sender_id),
113-
to: recipient(recipient_id),
113+
to: recipient(recipient_id, @project.group),
114114
subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})"),
115115
'X-GitLab-NotificationReason' => reason
116116
}

app/mailers/emails/notes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def note_target_url_options
5151
def note_thread_options(recipient_id)
5252
{
5353
from: sender(@note.author_id),
54-
to: recipient(recipient_id),
54+
to: recipient(recipient_id, @group),
5555
subject: subject("#{@note.noteable.title} (#{@note.noteable.reference_link_text})")
5656
}
5757
end

app/mailers/emails/pages_domains.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def pages_domain_enabled_email(domain, recipient)
77
@project = domain.project
88

99
mail(
10-
to: recipient.notification_email,
10+
to: recipient(recipient.id, @project.group),
1111
subject: subject("GitLab Pages domain '#{domain.domain}' has been enabled")
1212
)
1313
end
@@ -17,7 +17,7 @@ def pages_domain_disabled_email(domain, recipient)
1717
@project = domain.project
1818

1919
mail(
20-
to: recipient.notification_email,
20+
to: recipient(recipient.id, @project.group),
2121
subject: subject("GitLab Pages domain '#{domain.domain}' has been disabled")
2222
)
2323
end
@@ -27,7 +27,7 @@ def pages_domain_verification_succeeded_email(domain, recipient)
2727
@project = domain.project
2828

2929
mail(
30-
to: recipient.notification_email,
30+
to: recipient(recipient.id, @project.group),
3131
subject: subject("Verification succeeded for GitLab Pages domain '#{domain.domain}'")
3232
)
3333
end
@@ -37,7 +37,7 @@ def pages_domain_verification_failed_email(domain, recipient)
3737
@project = domain.project
3838

3939
mail(
40-
to: recipient.notification_email,
40+
to: recipient(recipient.id, @project.group),
4141
subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'")
4242
)
4343
end

app/mailers/emails/projects.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,36 @@ def project_was_moved_email(project_id, user_id, old_path_with_namespace)
77
@project = Project.find project_id
88
@target_url = project_url(@project)
99
@old_path_with_namespace = old_path_with_namespace
10-
mail(to: @user.notification_email,
10+
mail(to: recipient(user_id, @project.group),
1111
subject: subject("Project was moved"))
1212
end
1313

1414
def project_was_exported_email(current_user, project)
1515
@project = project
16-
mail(to: current_user.notification_email,
16+
mail(to: recipient(current_user.id, project.group),
1717
subject: subject("Project was exported"))
1818
end
1919

2020
def project_was_not_exported_email(current_user, project, errors)
2121
@project = project
2222
@errors = errors
23-
mail(to: current_user.notification_email,
23+
mail(to: recipient(current_user.id, @project.group),
2424
subject: subject("Project export error"))
2525
end
2626

2727
def repository_cleanup_success_email(project, user)
2828
@project = project
2929
@user = user
3030

31-
mail(to: user.notification_email, subject: subject("Project cleanup has completed"))
31+
mail(to: recipient(user.id, project.group), subject: subject("Project cleanup has completed"))
3232
end
3333

3434
def repository_cleanup_failure_email(project, user, error)
3535
@project = project
3636
@user = user
3737
@error = error
3838

39-
mail(to: user.notification_email, subject: subject("Project cleanup failure"))
39+
mail(to: recipient(user.id, project.group), subject: subject("Project cleanup failure"))
4040
end
4141

4242
def repository_push_email(project_id, opts = {})

app/mailers/emails/remote_mirrors.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def remote_mirror_update_failed_email(remote_mirror_id, recipient_id)
66
@remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
77
@project = @remote_mirror.project
88

9-
mail(to: recipient(recipient_id), subject: subject('Remote mirror update failed'))
9+
mail(to: recipient(recipient_id, @project.group), subject: subject('Remote mirror update failed'))
1010
end
1111
end
1212
end

app/mailers/notify.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,31 @@ def sender(sender_id, send_from_user_email = false)
7373

7474
# Look up a User by their ID and return their email address
7575
#
76-
# recipient_id - User ID
76+
# recipient_id - User ID
77+
# notification_group - The parent group of the notification
7778
#
7879
# Returns a String containing the User's email address.
79-
def recipient(recipient_id)
80+
def recipient(recipient_id, notification_group = nil)
8081
@current_user = User.find(recipient_id)
81-
@current_user.notification_email
82+
83+
if notification_group
84+
group_notification_email = nil
85+
86+
# Get notification group's and ancestors' notification settings
87+
group_ids = notification_group.self_and_ancestors_ids
88+
notification_settings = notification_group.notification_settings.where(user: @current_user) # rubocop: disable CodeReuse/ActiveRecord
89+
90+
# Exploit notification_group.self_and_ancestors_ids being ordered from
91+
# most nested to least nested to iterate through group ancestors
92+
group_ids.each do |group_id|
93+
group_notification_email = notification_settings.find { |ns| ns.source_id == group_id }&.notification_email
94+
break if group_notification_email.present?
95+
end
96+
end
97+
98+
# Return group-specific email address if present, otherwise return global
99+
# email address
100+
group_notification_email.presence || @current_user.notification_email
82101
end
83102

84103
# Formats arguments into a String suitable for use as an email subject

app/views/profiles/emails/index.html.haml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
%li
2727
Your Commit Email will be used for web based operations, such as edits and merges.
2828
%li
29-
Your Notification Email will be used for account notifications.
29+
Your Default Notification Email will be used for account notifications if a
30+
= link_to 'group-specific email address', profile_notifications_path
31+
is not set.
3032
%li
3133
Your Public Email will be displayed on your public profile.
3234
%li
@@ -41,7 +43,7 @@
4143
- if @primary_email === current_user.public_email
4244
%span.badge.badge-info Public email
4345
- if @primary_email === current_user.notification_email
44-
%span.badge.badge-info Notification email
46+
%span.badge.badge-info Default notification email
4547
- @emails.each do |email|
4648
%li
4749
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
%li.notification-list-item
2-
%span.notification.fa.fa-holder.append-right-5
3-
- if setting.global?
4-
= notification_icon(current_user.global_notification_setting.level)
5-
- else
6-
= notification_icon(setting.level)
1+
.gl-responsive-table-row.notification-list-item
2+
.table-section.section-40
3+
%span.notification.fa.fa-holder.append-right-5
4+
- if setting.global?
5+
= notification_icon(current_user.global_notification_setting.level)
6+
- else
7+
= notification_icon(setting.level)
78

8-
%span.str-truncated
9-
= link_to group.name, group_path(group)
9+
%span.str-truncated
10+
= link_to group.name, group_path(group)
1011

11-
.float-right
12+
.table-section.section-30.text-right
1213
= render 'shared/notifications/button', notification_setting: setting
14+
15+
.table-section.section-30
16+
= form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
17+
= f.select :notification_email, @user.all_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'

app/views/profiles/notifications/show.html.haml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@
4141
%h5
4242
= _('Groups (%{count})') % { count: @group_notifications.count }
4343
%div
44-
%ul.bordered-list
45-
- @group_notifications.each do |setting|
46-
= render 'group_settings', setting: setting, group: setting.source
44+
- @group_notifications.each do |setting|
45+
= render 'group_settings', setting: setting, group: setting.source
4746
%h5
4847
= _('Projects (%{count})') % { count: @project_notifications.count }
4948
%p.account-well

app/views/shared/notifications/_button.html.haml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
- btn_class = local_assigns.fetch(:btn_class, nil)
22

33
- if notification_setting
4-
.js-notification-dropdown.notification-dropdown.home-panel-action-button.dropdown.inline
4+
.js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline
55
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
66
= hidden_setting_source_input(notification_setting)
77
= f.hidden_field :level, class: "notification_setting_level"
88
.js-notification-toggle-btns
99
%div{ class: ("btn-group" if notification_setting.custom?) }
1010
- if notification_setting.custom?
11-
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
11+
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
1212
= icon("bell", class: "js-notification-loading")
1313
= notification_title(notification_setting.level)
1414
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
1515
= icon('caret-down')
1616
.sr-only Toggle dropdown
1717
- else
1818
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
19-
= icon("bell", class: "js-notification-loading")
20-
= notification_title(notification_setting.level)
21-
= icon("caret-down")
19+
.float-left
20+
= icon("bell", class: "js-notification-loading")
21+
= notification_title(notification_setting.level)
22+
.float-right
23+
= icon("caret-down")
2224

2325
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
2426

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: Add ability to define notification email addresses for groups you belong to.
3+
merge_request: 25299
4+
author:
5+
type: added

config/routes/profile.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
delete :unlink
1818
end
1919
end
20-
resource :notifications, only: [:show, :update]
20+
21+
resource :notifications, only: [:show, :update] do
22+
resources :groups, only: :update
23+
end
24+
2125
resource :password, only: [:new, :create, :edit, :update] do
2226
member do
2327
put :reset

spec/mailers/emails/pages_domains_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
include EmailSpec::Matchers
66
include_context 'gitlab email notification'
77

8-
set(:project) { create(:project) }
98
set(:domain) { create(:pages_domain, project: project) }
10-
set(:user) { project.owner }
9+
set(:user) { project.creator }
1110

1211
shared_examples 'a pages domain email' do
12+
let(:test_recipient) { user }
13+
14+
it_behaves_like 'an email sent to a user'
1315
it_behaves_like 'an email sent from GitLab'
1416
it_behaves_like 'it should not have Gmail Actions links'
1517
it_behaves_like 'a user cannot unsubscribe through footer link'

spec/mailers/notify_spec.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545

4646
context 'for a project' do
4747
shared_examples 'an assignee email' do
48+
let(:test_recipient) { assignee }
49+
50+
it_behaves_like 'an email sent to a user'
51+
4852
it 'is sent to the assignee as the author' do
4953
sender = subject.header[:from].addrs.first
5054

@@ -618,8 +622,10 @@
618622
end
619623

620624
describe 'project was moved' do
625+
let(:test_recipient) { user }
621626
subject { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
622627

628+
it_behaves_like 'an email sent to a user'
623629
it_behaves_like 'an email sent from GitLab'
624630
it_behaves_like 'it should not have Gmail Actions links'
625631
it_behaves_like "a user cannot unsubscribe through footer link"
@@ -1083,8 +1089,6 @@ def invite_to_project(project, inviter:)
10831089
end
10841090

10851091
context 'for a group' do
1086-
set(:group) { create(:group) }
1087-
10881092
describe 'group access requested' do
10891093
let(:group) { create(:group, :public, :access_requestable) }
10901094
let(:group_member) do

0 commit comments

Comments
 (0)