Commit 6e614bd5 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'issue_44230' into 'master'

Apply notification settings level of groups to all child objects

Closes #44230

See merge request gitlab-org/gitlab-ce!19191
parents 27aa2e54 afe5d7d5
...@@ -11,6 +11,7 @@ class Group < Namespace ...@@ -11,6 +11,7 @@ class Group < Namespace
include GroupDescendant include GroupDescendant
include TokenAuthenticatable include TokenAuthenticatable
include WithUploads include WithUploads
include Gitlab::Utils::StrongMemoize
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members alias_method :members, :group_members
...@@ -26,7 +27,11 @@ class Group < Namespace ...@@ -26,7 +27,11 @@ class Group < Namespace
has_many :milestones has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
# Overridden on another method
# Left here just to be dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel' has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable' has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute' has_many :custom_attributes, class_name: 'GroupCustomAttribute'
...@@ -88,6 +93,15 @@ class Group < Namespace ...@@ -88,6 +93,15 @@ class Group < Namespace
end end
end end
# Overrides notification_settings has_many association
# This allows to apply notification settings from parent groups
# to child groups and projects.
def notification_settings
source_type = self.class.base_class.name
NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
end
def to_reference(_from = nil, full: nil) def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}" "#{self.class.reference_prefix}#{full_path}"
end end
...@@ -225,6 +239,12 @@ class Group < Namespace ...@@ -225,6 +239,12 @@ class Group < Namespace
members_with_parents.pluck(:user_id) members_with_parents.pluck(:user_id)
end end
def self_and_ancestors_ids
strong_memoize(:self_and_ancestors_ids) do
self_and_ancestors.pluck(:id)
end
end
def members_with_parents def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents # Avoids an unnecessary SELECT when the group has no parents
source_ids = source_ids =
......
class NotificationRecipient class NotificationRecipient
include Gitlab::Utils::StrongMemoize
attr_reader :user, :type, :reason attr_reader :user, :type, :reason
def initialize(user, type, **opts) def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription unless NotificationSetting.levels.key?(type) || type == :subscription
...@@ -142,10 +144,33 @@ class NotificationRecipient ...@@ -142,10 +144,33 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global? return project_setting unless project_setting.nil? || project_setting.global?
group_setting = @group && user.notification_settings_for(@group) group_setting = closest_non_global_group_notification_settting
return group_setting unless group_setting.nil? || group_setting.global? return group_setting unless group_setting.nil?
user.global_notification_setting user.global_notification_setting
end end
# Returns the notificaton_setting of the lowest group in hierarchy with non global level
def closest_non_global_group_notification_settting
return unless @group
return if indexed_group_notification_settings.empty?
notification_setting = nil
@group.self_and_ancestors_ids.each do |id|
notification_setting = indexed_group_notification_settings[id]
break if notification_setting
end
notification_setting
end
def indexed_group_notification_settings
strong_memoize(:indexed_group_notification_settings) do
@group.notification_settings.where(user_id: user.id)
.where.not(level: NotificationSetting.levels[:global])
.index_by(&:source_id)
end
end
end end
---
title: Apply notification settings level of groups to all child objects
merge_request:
author:
type: changed
...@@ -34,9 +34,14 @@ anything that is set at Global Settings. ...@@ -34,9 +34,14 @@ anything that is set at Global Settings.
![notification settings](img/notification_group_settings.png) ![notification settings](img/notification_group_settings.png)
Group Settings are taking precedence over Global Settings but are on a level below Project Settings. Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings:
```
Group < Subgroup < Project
```
This means that you can set a different level of notifications per group while still being able This means that you can set a different level of notifications per group while still being able
to have a finer level setting per project. to have a finer level setting per project or subgroup.
Organization like this is suitable for users that belong to different groups but don't have the Organization like this is suitable for users that belong to different groups but don't have the
same need for being notified for every group they are member of. same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
......
...@@ -67,6 +67,30 @@ describe Group do ...@@ -67,6 +67,30 @@ describe Group do
end end
end end
describe '#notification_settings', :nested_groups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:sub_group) { create(:group, parent_id: group.id) }
before do
group.add_developer(user)
sub_group.add_developer(user)
end
it 'also gets notification settings from parent groups' do
expect(sub_group.notification_settings.size).to eq(2)
expect(sub_group.notification_settings).to include(group.notification_settings.first)
end
context 'when sub group is deleted' do
it 'does not delete parent notification settings' do
expect do
sub_group.destroy
end.to change { NotificationSetting.count }.by(-1)
end
end
end
describe '#visibility_level_allowed_by_parent' do describe '#visibility_level_allowed_by_parent' do
let(:parent) { create(:group, :internal) } let(:parent) { create(:group, :internal) }
let(:sub_group) { build(:group, parent_id: parent.id) } let(:sub_group) { build(:group, parent_id: parent.id) }
......
...@@ -13,4 +13,48 @@ describe NotificationRecipient do ...@@ -13,4 +13,48 @@ describe NotificationRecipient do
expect(recipient.has_access?).to be_falsy expect(recipient.has_access?).to be_falsy
end end
context '#notification_setting' do
context 'for child groups', :nested_groups do
let!(:moved_group) { create(:group) }
let(:group) { create(:group) }
let(:sub_group_1) { create(:group, parent: group) }
let(:sub_group_2) { create(:group, parent: sub_group_1) }
let(:project) { create(:project, namespace: moved_group) }
before do
sub_group_2.add_owner(user)
moved_group.add_owner(user)
Groups::TransferService.new(moved_group, user).execute(sub_group_2)
moved_group.reload
end
context 'when notification setting is global' do
before do
user.notification_settings_for(group).global!
user.notification_settings_for(sub_group_1).mention!
user.notification_settings_for(sub_group_2).global!
user.notification_settings_for(moved_group).global!
end
it 'considers notification setting from the first parent without global setting' do
expect(subject.notification_setting.source).to eq(sub_group_1)
end
end
context 'when notification setting is not global' do
before do
user.notification_settings_for(group).global!
user.notification_settings_for(sub_group_1).mention!
user.notification_settings_for(sub_group_2).watch!
user.notification_settings_for(moved_group).disabled!
end
it 'considers notification setting from lowest group member in hierarchy' do
expect(subject.notification_setting.source).to eq(moved_group)
end
end
end
end
end end
...@@ -59,6 +59,20 @@ describe NotificationService, :mailer do ...@@ -59,6 +59,20 @@ describe NotificationService, :mailer do
should_email(participant) should_email(participant)
end end
context 'for subgroups', :nested_groups do
before do
build_group(project)
end
it 'emails the participant' do
create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant)
notification_trigger
should_email_nested_group_user(@pg_participant)
end
end
end end
shared_examples 'participating by assignee notification' do shared_examples 'participating by assignee notification' do
...@@ -239,34 +253,56 @@ describe NotificationService, :mailer do ...@@ -239,34 +253,56 @@ describe NotificationService, :mailer do
end end
describe 'new note on issue in project that belongs to a group' do describe 'new note on issue in project that belongs to a group' do
let(:group) { create(:group) }
before do before do
note.project.namespace_id = group.id note.project.namespace_id = group.id
note.project.group.add_user(@u_watcher, GroupMember::MASTER) group.add_user(@u_watcher, GroupMember::MASTER)
note.project.group.add_user(@u_custom_global, GroupMember::MASTER) group.add_user(@u_custom_global, GroupMember::MASTER)
note.project.save note.project.save
@u_watcher.notification_settings_for(note.project).participating! @u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(note.project.group).global! @u_watcher.notification_settings_for(group).global!
update_custom_notification(:new_note, @u_custom_global) update_custom_notification(:new_note, @u_custom_global)
reset_delivered_emails! reset_delivered_emails!
end end
it do shared_examples 'new note notifications' do
notification.new_note(note) it do
notification.new_note(note)
should_email(note.noteable.author)
should_email(note.noteable.assignees.first)
should_email(@u_mentioned)
should_email(@u_custom_global)
should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(@u_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
end
should_email(note.noteable.author) let(:group) { create(:group) }
should_email(note.noteable.assignees.first)
should_email(@u_mentioned) it_behaves_like 'new note notifications'
should_email(@u_custom_global)
should_not_email(@u_guest_custom) context 'which is a subgroup', :nested_groups do
should_not_email(@u_guest_watcher) let!(:parent) { create(:group) }
should_not_email(@u_watcher) let!(:group) { create(:group, parent: parent) }
should_not_email(note.author)
should_not_email(@u_participating) it_behaves_like 'new note notifications'
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant) it 'overrides child objects with global level' do
user = create(:user)
parent.add_developer(user)
user.notification_settings_for(parent).watch!
reset_delivered_emails!
notification.new_note(note)
should_email(user)
end
end end
end end
end end
...@@ -301,6 +337,31 @@ describe NotificationService, :mailer do ...@@ -301,6 +337,31 @@ describe NotificationService, :mailer do
should_email(member) should_email(member)
should_email(admin) should_email(admin)
end end
context 'on project that belongs to subgroup', :nested_groups do
let(:group_reporter) { create(:user) }
let(:group_guest) { create(:user) }
let(:parent_group) { create(:group) }
let(:child_group) { create(:group, parent: parent_group) }
let(:project) { create(:project, namespace: child_group) }
context 'when user is group guest member' do
before do
parent_group.add_reporter(group_reporter)
parent_group.add_guest(group_guest)
group_guest.notification_settings_for(parent_group).watch!
group_reporter.notification_settings_for(parent_group).watch!
reset_delivered_emails!
end
it 'does not email guest user' do
notification.new_note(note)
should_email(group_reporter)
should_not_email(group_guest)
end
end
end
end end
context 'issue note mention' do context 'issue note mention' do
...@@ -311,6 +372,7 @@ describe NotificationService, :mailer do ...@@ -311,6 +372,7 @@ describe NotificationService, :mailer do
before do before do
build_team(note.project) build_team(note.project)
build_group(note.project)
note.project.add_master(note.author) note.project.add_master(note.author)
add_users_with_subscription(note.project, issue) add_users_with_subscription(note.project, issue)
reset_delivered_emails! reset_delivered_emails!
...@@ -336,10 +398,20 @@ describe NotificationService, :mailer do ...@@ -336,10 +398,20 @@ describe NotificationService, :mailer do
should_email(@u_guest_watcher) should_email(@u_guest_watcher)
should_email(note.noteable.author) should_email(note.noteable.author)
should_email(note.noteable.assignees.first) should_email(note.noteable.assignees.first)
should_not_email(note.author) should_email_nested_group_user(@pg_watcher)
should_email(@u_mentioned) should_email(@u_mentioned)
should_not_email(@u_disabled)
should_email(@u_not_mentioned) should_email(@u_not_mentioned)
should_not_email(note.author)
should_not_email(@u_disabled)
should_not_email_nested_group_user(@pg_disabled)
end
it 'notifies parent group members with mention level', :nested_groups do
note = create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: "@#{@pg_mention.username}")
notification.new_note(note)
should_email_nested_group_user(@pg_mention)
end end
it 'filters out "mentioned in" notes' do it 'filters out "mentioned in" notes' do
...@@ -352,17 +424,18 @@ describe NotificationService, :mailer do ...@@ -352,17 +424,18 @@ describe NotificationService, :mailer do
end end
context 'project snippet note' do context 'project snippet note' do
let(:project) { create(:project, :public) } let!(:project) { create(:project, :public) }
let(:snippet) { create(:project_snippet, project: project, author: create(:user)) } let(:snippet) { create(:project_snippet, project: project, author: create(:user)) }
let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: snippet.project.id, note: '@all mentioned') } let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: project.id, note: '@all mentioned') }
before do before do
build_team(note.project) build_team(project)
build_group(project)
# make sure these users can read the project snippet! # make sure these users can read the project snippet!
project.add_guest(@u_guest_watcher) project.add_guest(@u_guest_watcher)
project.add_guest(@u_guest_custom) project.add_guest(@u_guest_custom)
add_member_for_parent_group(@pg_watcher, project)
note.project.add_master(note.author) note.project.add_master(note.author)
reset_delivered_emails! reset_delivered_emails!
end end
...@@ -370,7 +443,6 @@ describe NotificationService, :mailer do ...@@ -370,7 +443,6 @@ describe NotificationService, :mailer do
describe '#new_note' do describe '#new_note' do
it 'notifies the team members' do it 'notifies the team members' do
notification.new_note(note) notification.new_note(note)
# Notify all team members # Notify all team members
note.project.team.members.each do |member| note.project.team.members.each do |member|
# User with disabled notification should not be notified # User with disabled notification should not be notified
...@@ -449,6 +521,7 @@ describe NotificationService, :mailer do ...@@ -449,6 +521,7 @@ describe NotificationService, :mailer do
before do before do
build_team(note.project) build_team(note.project)
build_group(project)
reset_delivered_emails! reset_delivered_emails!
allow(note.noteable).to receive(:author).and_return(@u_committer) allow(note.noteable).to receive(:author).and_return(@u_committer)
update_custom_notification(:new_note, @u_guest_custom, resource: project) update_custom_notification(:new_note, @u_guest_custom, resource: project)
...@@ -463,11 +536,13 @@ describe NotificationService, :mailer do ...@@ -463,11 +536,13 @@ describe NotificationService, :mailer do
should_email(@u_guest_custom) should_email(@u_guest_custom)
should_email(@u_committer) should_email(@u_committer)
should_email(@u_watcher) should_email(@u_watcher)
should_email_nested_group_user(@pg_watcher)
should_not_email(@u_mentioned) should_not_email(@u_mentioned)
should_not_email(note.author) should_not_email(note.author)
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(@u_disabled) should_not_email(@u_disabled)
should_not_email(@u_lazy_participant) should_not_email(@u_lazy_participant)
should_not_email_nested_group_user(@pg_disabled)
end end
it do it do
...@@ -478,10 +553,12 @@ describe NotificationService, :mailer do ...@@ -478,10 +553,12 @@ describe NotificationService, :mailer do
should_email(@u_committer) should_email(@u_committer)
should_email(@u_watcher) should_email(@u_watcher)
should_email(@u_mentioned) should_email(@u_mentioned)
should_email_nested_group_user(@pg_watcher)
should_not_email(note.author) should_not_email(note.author)
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(@u_disabled) should_not_email(@u_disabled)
should_not_email(@u_lazy_participant) should_not_email(@u_lazy_participant)
should_not_email_nested_group_user(@pg_disabled)
end end
it do it do
...@@ -548,10 +625,13 @@ describe NotificationService, :mailer do ...@@ -548,10 +625,13 @@ describe NotificationService, :mailer do
should_email(@g_global_watcher) should_email(@g_global_watcher)
should_email(@g_watcher) should_email(@g_watcher)
should_email(@unsubscribed_mentioned) should_email(@unsubscribed_mentioned)
should_email_nested_group_user(@pg_watcher)
should_not_email(@u_mentioned) should_not_email(@u_mentioned)
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(@u_disabled) should_not_email(@u_disabled)
should_not_email(@u_lazy_participant) should_not_email(@u_lazy_participant)
should_not_email_nested_group_user(@pg_disabled)
should_not_email_nested_group_user(@pg_mention)
end end
it do it do
...@@ -1922,19 +2002,69 @@ describe NotificationService, :mailer do ...@@ -1922,19 +2002,69 @@ describe NotificationService, :mailer do
# Users in the project's group but not part of project's team # Users in the project's group but not part of project's team
# with different notification settings # with different notification settings
def build_group(project) def build_group(project)
group = create(:group, :public) group = create_nested_group
project.group = group project.update(namespace_id: group.id)
# Group member: global=disabled, group=watch # Group member: global=disabled, group=watch
@g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group) @g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group)
@g_watcher.notification_settings_for(nil).disabled! @g_watcher.notification_settings_for(nil).disabled!
# Group member: global=watch, group=global # Group member: global=watch, group=global
@g_global_watcher = create_global_setting_for(create(:user), :watch) @g_global_watcher ||= create_global_setting_for(create(:user), :watch)
group.add_users([@g_watcher, @g_global_watcher], :master) group.add_users([@g_watcher, @g_global_watcher], :master)
group group
end end
# Creates a nested group only if supported
# to avoid errors on MySQL
def create_nested_group
if Group.supports_nested_groups?
parent_group = create(:group, :public)
child_group = create(:group, :public, parent: parent_group)
# Parent group member: global=disabled, parent_group=watch, child_group=global
@pg_watcher ||= create_user_with_notification(:watch, 'parent_group_watcher', parent_group)
@pg_watcher.notification_settings_for(nil).disabled!
# Parent group member: global=global, parent_group=disabled, child_group=global
@pg_disabled ||= create_user_with_notification(:disabled, 'parent_group_disabled', parent_group)
@pg_disabled.notification_settings_for(nil).global!
# Parent group member: global=global, parent_group=mention, child_group=global
@pg_mention ||= create_user_with_notification(:mention, 'parent_group_mention', parent_group)
@pg_mention.notification_settings_for(nil).global!
# Parent group member: global=global, parent_group=participating, child_group=global
@pg_participant ||= create_user_with_notification(:participating, 'parent_group_participant', parent_group)
@pg_mention.notification_settings_for(nil).global!
child_group
else
create(:group, :public)
end
end
def add_member_for_parent_group(user, project)
return unless Group.supports_nested_groups?
project.reload
project.group.parent.add_master(user)
end
def should_email_nested_group_user(user, times: 1, recipients: email_recipients)
return unless Group.supports_nested_groups?
should_email(user, times: 1, recipients: email_recipients)
end
def should_not_email_nested_group_user(user, recipients: email_recipients)
return unless Group.supports_nested_groups?
should_not_email(user, recipients: email_recipients)
end
def add_users_with_subscription(project, issuable) def add_users_with_subscription(project, issuable)
@subscriber = create :user @subscriber = create :user
@unsubscriber = create :user @unsubscriber = create :user
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment