Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 1 addition & 1 deletion src/sentry/incidents/action_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def get_targets(self):
ExternalProviders.EMAIL,
self.project,
{member.user for member in target.member_set},
)
)[ExternalProviders.EMAIL]
targets = [(user.id, user.email) for user in users]
# TODO: We need some sort of verification system to make sure we're not being
# used as an email relay.
Expand Down
5 changes: 4 additions & 1 deletion src/sentry/mail/activity/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
UserAvatar,
UserOption,
)
from sentry.models.integration import ExternalProviders
from sentry.utils import json
from sentry.utils.assets import get_asset_url
from sentry.utils.avatar import get_email_avatar
Expand Down Expand Up @@ -40,7 +41,9 @@ def get_participants(self):
if not self.group:
return []

participants = GroupSubscription.objects.get_participants(group=self.group)
participants = GroupSubscription.objects.get_participants(group=self.group)[
ExternalProviders.EMAIL
]

if self.activity.user is not None and self.activity.user in participants:
receive_own_activity = (
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/mail/activity/new_processing_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, activity):
def get_participants(self):
users = NotificationSetting.objects.get_notification_recipients(
ExternalProviders.EMAIL, self.project
)
)[ExternalProviders.EMAIL]
return {user: GroupSubscriptionReason.processing_issue for user in users}

def get_context(self):
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/mail/activity/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
User,
UserEmail,
)
from sentry.models.integration import ExternalProviders
from sentry.notifications.types import (
NotificationSettingOptionValues,
NotificationScopeType,
Expand Down Expand Up @@ -129,7 +128,6 @@ def get_participants(self):
# get all the involved users' settings for deploy-emails (user default
# saved without org set)
notification_settings = NotificationSetting.objects.get_for_users_by_parent(
ExternalProviders.EMAIL,
NotificationSettingTypes.DEPLOY,
users=users,
parent=self.organization,
Expand Down
10 changes: 7 additions & 3 deletions src/sentry/mail/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def get_sendable_user_objects(project):
"""
return NotificationSetting.objects.get_notification_recipients(
ExternalProviders.EMAIL, project
)
)[ExternalProviders.EMAIL]

def get_sendable_user_ids(self, project):
users = self.get_sendable_user_objects(project)
Expand Down Expand Up @@ -231,7 +231,6 @@ def get_send_to_owners(self, event, project):
sentry_orgmember_set__organizationmemberteam__team__id__in=teams_to_resolve,
).values_list("id", flat=True)
)

return send_to - self.disabled_users_from_project(project)
else:
metrics.incr(
Expand All @@ -247,7 +246,6 @@ def disabled_users_from_project(project: Project) -> Set[int]:
user_ids = project.member_set.values_list("user", flat=True)
users = User.objects.filter(id__in=user_ids)
notification_settings = NotificationSetting.objects.get_for_users_by_parent(
provider=ExternalProviders.EMAIL,
type=NotificationSettingTypes.ISSUE_ALERTS,
parent=project,
users=users,
Expand All @@ -263,7 +261,11 @@ def disabled_users_from_project(project: Project) -> Set[int]:
if settings:
# Check per-project settings first, fallback to project-independent settings.
project_setting = settings.get(NotificationScopeType.PROJECT)
if project_setting:
project_setting = project_setting[ExternalProviders.EMAIL]
user_setting = settings.get(NotificationScopeType.USER)
if user_setting:
user_setting = user_setting[ExternalProviders.EMAIL]
if project_setting == NotificationSettingOptionValues.NEVER or (
not project_setting and user_setting == NotificationSettingOptionValues.NEVER
):
Expand Down Expand Up @@ -526,6 +528,8 @@ def handle_user_report(self, payload, project, **kwargs):
group = Group.objects.get(id=payload["report"]["issue"]["id"])

participants = GroupSubscription.objects.get_participants(group=group)
if participants:
participants = participants[ExternalProviders.EMAIL]

if not participants:
return
Expand Down
28 changes: 15 additions & 13 deletions src/sentry/models/groupsubscription.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any, Mapping

from collections import defaultdict
from django.conf import settings
from django.db import IntegrityError, models, transaction
from django.utils import timezone
Expand All @@ -11,9 +12,8 @@
Model,
sane_repr,
)
from sentry.models.integration import ExternalProviders
from sentry.notifications.helpers import (
should_be_participating,
where_should_be_participating,
transform_to_notification_settings_by_user,
)
from sentry.notifications.types import NotificationSettingTypes
Expand Down Expand Up @@ -122,31 +122,33 @@ def get_participants(self, group) -> Mapping[Any, GroupSubscriptionReason]:
user_ids = [user.id for user in users]
subscriptions = self.filter(group=group, user_id__in=user_ids)
notification_settings = NotificationSetting.objects.get_for_users_by_parent(
ExternalProviders.EMAIL,
NotificationSettingTypes.WORKFLOW,
users=users,
parent=group.project,
)

subscriptions_by_user_id = {
subscription.user_id: subscription for subscription in subscriptions
}
notification_settings_by_user = transform_to_notification_settings_by_user(
notification_settings, users
)
return {
user: getattr(
subscriptions_by_user_id.get(user.id),
"reason",
GroupSubscriptionReason.implicit,
)
for user in users
if should_be_participating(
result = defaultdict(dict)

for user in users:
providers = where_should_be_participating(
user,
subscriptions_by_user_id,
notification_settings_by_user,
)
}
for provider in providers:
value = getattr(
subscriptions_by_user_id.get(user.id),
"reason",
GroupSubscriptionReason.implicit,
)
result[provider][user] = value

return result


class GroupSubscription(Model):
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/models/notificationsetting.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ class Meta:
)

__repr__ = sane_repr(
"scope_type",
"scope_str",
"scope_identifier",
"target",
"provider",
"type",
"value",
"provider_str",
"type_str",
"value_str",
)


Expand Down
95 changes: 52 additions & 43 deletions src/sentry/notifications/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import defaultdict
from typing import Any, Dict, Iterable, Mapping, Optional, Tuple
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple

from sentry.models.integration import ExternalProviders
from sentry.notifications.legacy_mappings import get_legacy_value
from sentry.notifications.types import (
NotificationScopeType,
Expand All @@ -9,94 +10,102 @@
)


def _get_setting_value_from_mapping(
def _get_setting_mapping_from_mapping(
notification_settings_by_user: Mapping[
Any, Mapping[NotificationScopeType, NotificationSettingOptionValues]
Any,
Mapping[NotificationScopeType, Mapping[ExternalProviders, NotificationSettingOptionValues]],
],
user: Any,
type: NotificationSettingTypes,
default: NotificationSettingOptionValues,
) -> NotificationSettingOptionValues:
) -> Mapping[ExternalProviders, NotificationSettingOptionValues]:
# XXX(CEO): may not respect granularity of a setting for Slack a setting for email
# but we'll worry about that later since we don't have a FE for it yet
specific_scope = get_scope_type(type)
notification_settings_option = notification_settings_by_user.get(user)
if notification_settings_option:
notification_setting_option = notification_settings_option.get(
notification_settings_mapping = notification_settings_by_user.get(user)
if notification_settings_mapping:
notification_setting_option = notification_settings_mapping.get(
specific_scope
) or notification_settings_option.get(NotificationScopeType.USER)
) or notification_settings_mapping.get(NotificationScopeType.USER)
if notification_setting_option:
return notification_setting_option
return default
return {ExternalProviders.EMAIL: NotificationSettingOptionValues.ALWAYS}


def should_user_be_notified(
def where_should_user_be_notified(
notification_settings_by_user: Mapping[
Any, Mapping[NotificationScopeType, NotificationSettingOptionValues]
Any,
Mapping[NotificationScopeType, Mapping[ExternalProviders, NotificationSettingOptionValues]],
],
user: Any,
) -> bool:
) -> List[ExternalProviders]:
"""
Given a mapping of default and specific notification settings by user,
determine if a user should receive an ISSUE_ALERT notification.
return the list of providers after verifying the user has opted into this notification.
"""
return (
_get_setting_value_from_mapping(
notification_settings_by_user,
user,
NotificationSettingTypes.ISSUE_ALERTS,
NotificationSettingOptionValues.ALWAYS,
)
== NotificationSettingOptionValues.ALWAYS
mapping = _get_setting_mapping_from_mapping(
notification_settings_by_user,
user,
NotificationSettingTypes.ISSUE_ALERTS,
)
return list(
filter(lambda elem: mapping[elem] == NotificationSettingOptionValues.ALWAYS, mapping)
)


def should_be_participating(
def where_should_be_participating(
user: Any,
subscriptions_by_user_id: Mapping[int, Any],
notification_settings_by_user: Mapping[
Any, Mapping[NotificationScopeType, NotificationSettingOptionValues]
Any,
Mapping[NotificationScopeType, Mapping[ExternalProviders, NotificationSettingOptionValues]],
],
) -> bool:
) -> List[ExternalProviders]:
"""
Given a mapping of users to subscriptions and a mapping of default and
specific notification settings by user, determine if a user should receive
specific notification settings by user, determine where a user should receive
a WORKFLOW notification. Unfortunately, this algorithm does not respect
NotificationSettingOptionValues.ALWAYS. If the user is unsubscribed from
the group, that overrides their notification preferences.
"""
value = _get_setting_value_from_mapping(
mapping = _get_setting_mapping_from_mapping(
notification_settings_by_user,
user,
NotificationSettingTypes.WORKFLOW,
NotificationSettingOptionValues.SUBSCRIBE_ONLY,
)

if value == NotificationSettingOptionValues.NEVER:
return False

subscription = subscriptions_by_user_id.get(user.id)
if subscription:
return bool(subscription.is_active)

return value == NotificationSettingOptionValues.ALWAYS
output = []
for provider, value in mapping.items():
subscription = subscriptions_by_user_id.get(user.id)
if (subscription and not subscription.is_active) or (
value == NotificationSettingOptionValues.NEVER
):
continue
if (subscription and subscription.is_active) or (
value == NotificationSettingOptionValues.ALWAYS
):
output.append(provider)
return output


def transform_to_notification_settings_by_user(
notification_settings: Iterable[Any],
users: Iterable[Any],
) -> Mapping[Any, Mapping[NotificationScopeType, NotificationSettingOptionValues]]:
) -> Mapping[
Any, Mapping[NotificationScopeType, Mapping[ExternalProviders, NotificationSettingOptionValues]]
]:
"""
Given a unorganized list of notification settings, create a mapping of
users to a map of notification scopes to setting values.
"""
actor_mapping = {user.actor_id: user for user in users}
notification_settings_by_user: Dict[
Any, Dict[NotificationScopeType, NotificationSettingOptionValues]
] = defaultdict(dict)
Any, Dict[NotificationScopeType, Dict[ExternalProviders, NotificationSettingOptionValues]]
] = defaultdict(lambda: defaultdict(dict))
for notification_setting in notification_settings:
user = actor_mapping.get(notification_setting.target_id)
notification_settings_by_user[user][
NotificationScopeType(notification_setting.scope_type)
] = NotificationSettingOptionValues(notification_setting.value)
scope_type = NotificationScopeType(notification_setting.scope_type)
value = NotificationSettingOptionValues(notification_setting.value)
provider = ExternalProviders(notification_setting.provider)
notification_settings_by_user[user][scope_type][provider] = value
return notification_settings_by_user


Expand Down
Loading