Browse Source

ref(integrations): Dual Write Usages (#24258)

Marcos Gaeta 4 years ago
parent
commit
8b553c55c6

+ 11 - 8
src/sentry/api/endpoints/project_details.py

@@ -25,15 +25,18 @@ from sentry.models import (
     AuditLogEntryEvent,
     Group,
     GroupStatus,
+    NotificationSetting,
     Project,
     ProjectBookmark,
     ProjectRedirect,
     ProjectStatus,
     ProjectTeam,
-    UserOption,
 )
 from sentry.grouping.enhancer import Enhancements, InvalidEnhancerConfig
 from sentry.grouping.fingerprinting import FingerprintingRules, InvalidFingerprintingConfig
+from sentry.models.integration import ExternalProviders
+from sentry.notifications.legacy_mappings import get_option_value_from_boolean
+from sentry.notifications.types import NotificationSettingTypes
 from sentry.tasks.deletion import delete_project
 from sentry.utils import json
 from sentry.utils.compat import filter
@@ -532,13 +535,13 @@ class ProjectDetailsEndpoint(ProjectEndpoint):
             if project.update_option("sentry:origins", result["allowedDomains"]):
                 changed_proj_settings["sentry:origins"] = result["allowedDomains"]
 
-        if result.get("isSubscribed"):
-            UserOption.objects.set_value(
-                user=request.user, key="mail:alert", value=1, project=project
-            )
-        elif result.get("isSubscribed") is False:
-            UserOption.objects.set_value(
-                user=request.user, key="mail:alert", value=0, project=project
+        if "isSubscribed" in result:
+            NotificationSetting.objects.update_settings(
+                ExternalProviders.EMAIL,
+                NotificationSettingTypes.ISSUE_ALERTS,
+                get_option_value_from_boolean(result.get("isSubscribed")),
+                user=request.user,
+                project=project,
             )
 
         if "dynamicSampling" in result:

+ 36 - 12
src/sentry/api/endpoints/user_notification_details.py

@@ -1,12 +1,18 @@
 from collections import defaultdict
-from rest_framework import serializers
+from rest_framework import serializers, status
 from rest_framework.response import Response
 
 from sentry.api.bases.user import UserEndpoint
 from sentry.api.fields.empty_integer import EmptyIntegerField
 from sentry.api.serializers import serialize, Serializer
 from sentry.models import UserOption
-from sentry.notifications.legacy_mappings import USER_OPTION_SETTINGS
+from sentry.models.integration import ExternalProviders
+from sentry.models.notificationsetting import NotificationSetting
+from sentry.notifications.legacy_mappings import (
+    get_option_value_from_int,
+    get_type_from_user_option_settings_key,
+    USER_OPTION_SETTINGS,
+)
 from sentry.notifications.types import UserOptionsSettingsKey
 
 
@@ -61,15 +67,33 @@ class UserNotificationDetailsEndpoint(UserEndpoint):
     def put(self, request, user):
         serializer = UserNotificationDetailsSerializer(data=request.data)
 
-        if serializer.is_valid():
-            for key, value in serializer.validated_data.items():
-                db_key = USER_OPTION_SETTINGS[UserOptionsSettingsKey(key)]["key"]
-                (uo, created) = UserOption.objects.get_or_create(
-                    user=user, key=db_key, project=None, organization=None
+        if not serializer.is_valid():
+            return Response(serializer.errors, status=400)
+
+        for key, value in serializer.validated_data.items():
+            try:
+                key = UserOptionsSettingsKey(key)
+            except ValueError:
+                return Response(
+                    {"detail": "Unknown key: %s." % key},
+                    status=status.HTTP_400_BAD_REQUEST,
                 )
-                # Convert integers and booleans to string representations of ints.
-                uo.update(value=str(int(value)))
 
-            return self.get(request, user)
-        else:
-            return Response(serializer.errors, status=400)
+            if key in [UserOptionsSettingsKey.DEPLOY, UserOptionsSettingsKey.WORKFLOW]:
+                type = get_type_from_user_option_settings_key(key)
+                NotificationSetting.objects.update_settings(
+                    ExternalProviders.EMAIL,
+                    type,
+                    get_option_value_from_int(type, int(value)),
+                    user=user,
+                )
+            else:
+                user_option, _ = UserOption.objects.get_or_create(
+                    key=USER_OPTION_SETTINGS[key]["key"],
+                    user=user,
+                    project=None,
+                    organization=None,
+                )
+                user_option.update(value=str(int(value)))
+
+        return self.get(request, user)

+ 110 - 104
src/sentry/api/endpoints/user_notification_fine_tuning.py

@@ -6,17 +6,27 @@ from sentry.api.bases.user import UserEndpoint
 from sentry.api.serializers import serialize
 from sentry.api.serializers.models import UserNotificationsSerializer
 from sentry.models import (
-    OrganizationMember,
-    OrganizationMemberTeam,
-    OrganizationStatus,
-    ProjectTeam,
+    NotificationSetting,
+    Project,
     UserOption,
     UserEmail,
 )
-from sentry.notifications.legacy_mappings import get_legacy_key_from_fine_tuning_key
+from sentry.models.integration import ExternalProviders
+from sentry.notifications.legacy_mappings import (
+    get_option_value_from_int,
+    get_type_from_fine_tuning_key,
+)
 from sentry.notifications.types import FineTuningAPIKey
 
 
+INVALID_EMAIL_MSG = (
+    "Invalid email value(s) provided. Email values must be verified emails for the given user."
+)
+INVALID_USER_MSG = (
+    "User does not belong to at least one of the requested organizations (org_id: %s)."
+)
+
+
 class UserNotificationFineTuningEndpoint(UserEndpoint):
     def get(self, request, user, notification_type):
         try:
@@ -28,12 +38,11 @@ class UserNotificationFineTuningEndpoint(UserEndpoint):
             )
 
         notifications = UserNotificationsSerializer()
-
         serialized = serialize(
             user,
             request.user,
             notifications,
-            notification_option_key=get_legacy_key_from_fine_tuning_key(notification_type),
+            notification_type=notification_type,
         )
         return Response(serialized)
 
@@ -68,54 +77,10 @@ class UserNotificationFineTuningEndpoint(UserEndpoint):
                 status=status.HTTP_404_NOT_FOUND,
             )
 
-        key = get_legacy_key_from_fine_tuning_key(notification_type)
-        filter_args = {"user": user, "key": key}
-
         if notification_type == FineTuningAPIKey.REPORTS:
-            (user_option, created) = UserOption.objects.get_or_create(**filter_args)
-
-            value = set(user_option.value or [])
-
-            # set of org ids that user is a member of
-            org_ids = self.get_org_ids(user)
-            for org_id, enabled in request.data.items():
-                org_id = int(org_id)
-                # We want "0" to be falsey
-                enabled = int(enabled)
-
-                # make sure user is in org
-                if org_id not in org_ids:
-                    return Response(
-                        {
-                            "detail": "User does not belong to at least one of the \
-                            requested orgs (org_id: %s)."
-                            % org_id
-                        },
-                        status=status.HTTP_403_FORBIDDEN,
-                    )
-
-                # list contains org ids that should have reports DISABLED
-                # so if enabled need to check if org_id exists in list (because by default
-                # they will have reports enabled)
-                if enabled and org_id in value:
-                    value.remove(org_id)
-                elif not enabled:
-                    value.add(org_id)
-
-            user_option.update(value=list(value))
-            return Response(status=status.HTTP_204_NO_CONTENT)
-
-        if notification_type in [
-            FineTuningAPIKey.ALERTS,
-            FineTuningAPIKey.WORKFLOW,
-            FineTuningAPIKey.EMAIL,
-        ]:
-            update_key = "project"
-            parent_ids = set(self.get_project_ids(user))
-        else:
-            update_key = "organization"
-            parent_ids = set(self.get_org_ids(user))
+            return self._handle_put_reports(user, request.data)
 
+        # Validate that all of the IDs are integers.
         try:
             ids_to_update = {int(i) for i in request.data.keys()}
         except ValueError:
@@ -124,8 +89,12 @@ class UserNotificationFineTuningEndpoint(UserEndpoint):
                 status=status.HTTP_400_BAD_REQUEST,
             )
 
-        # make sure that the ids we are going to update are a subset of the user's
-        # list of orgs or projects
+        # Make sure that the IDs we are going to update are a subset of the
+        # user's list of organizations or projects.
+        parents = (
+            user.get_orgs() if notification_type == FineTuningAPIKey.DEPLOY else user.get_projects()
+        )
+        parent_ids = {parent.id for parent in parents}
         if not ids_to_update.issubset(parent_ids):
             bad_ids = ids_to_update - parent_ids
             return Response(
@@ -139,58 +108,95 @@ class UserNotificationFineTuningEndpoint(UserEndpoint):
             )
 
         if notification_type == FineTuningAPIKey.EMAIL:
-            # make sure target emails exist and are verified
-            emails_to_check = set(request.data.values())
-            emails = UserEmail.objects.filter(
-                user=user, email__in=emails_to_check, is_verified=True
-            )
+            return self._handle_put_emails(user, request.data)
+
+        return self._handle_put_notification_settings(
+            user, notification_type, parents, request.data
+        )
+
+    @staticmethod
+    def _handle_put_reports(user, data):
+        user_option, _ = UserOption.objects.get_or_create(
+            user=user,
+            key="reports:disabled-organizations",
+        )
+
+        value = set(user_option.value or [])
+
+        # The set of IDs of the organizations of which the user is a member.
+        org_ids = {organization.id for organization in user.get_orgs()}
+        for org_id, enabled in data.items():
+            org_id = int(org_id)
+            # We want "0" to be falsey
+            enabled = int(enabled)
 
-            # Is there a better way to check this?
-            if len(emails) != len(emails_to_check):
+            # make sure user is in org
+            if org_id not in org_ids:
                 return Response(
-                    {
-                        "detail": "Invalid email value(s) provided. Email values \
-                        must be verified emails for the given user."
-                    },
-                    status=status.HTTP_400_BAD_REQUEST,
+                    {"detail": INVALID_USER_MSG % org_id}, status=status.HTTP_403_FORBIDDEN
                 )
 
+            # The list contains organization IDs that should have reports
+            # DISABLED. If enabled, we need to check if org_id exists in list
+            # (because by default they will have reports enabled.)
+            if enabled and org_id in value:
+                value.remove(org_id)
+            elif not enabled:
+                value.add(org_id)
+
+        user_option.update(value=list(value))
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+    @staticmethod
+    def _handle_put_emails(user, data):
+        # Make sure target emails exist and are verified
+        emails_to_check = set(data.values())
+        emails = UserEmail.objects.filter(user=user, email__in=emails_to_check, is_verified=True)
+
+        # TODO(mgaeta): Is there a better way to check this?
+        if len(emails) != len(emails_to_check):
+            return Response({"detail": INVALID_EMAIL_MSG}, status=status.HTTP_400_BAD_REQUEST)
+
+        project_ids = [int(id) for id in data.keys()]
+        projects_map = {
+            int(project.id): project for project in Project.objects.filter(id__in=project_ids)
+        }
+
         with transaction.atomic():
-            for id in request.data:
-                val = request.data[id]
-                int_val = int(val) if notification_type != FineTuningAPIKey.EMAIL else None
-
-                filter_args["%s_id" % update_key] = id
-
-                # 'email' doesn't have a default to delete, and it's a string
-                # -1 is a magic value to use "default" value, so just delete option
-                if int_val == -1:
-                    UserOption.objects.filter(**filter_args).delete()
-                else:
-                    user_option, _ = UserOption.objects.get_or_create(**filter_args)
-
-                    # Values have been saved as strings for `mail:alerts` *shrug*
-                    # `reports:disabled-organizations` requires an array of ids
-                    user_option.update(
-                        value=int_val if notification_type == FineTuningAPIKey.ALERTS else str(val)
-                    )
-
-            return Response(status=status.HTTP_204_NO_CONTENT)
-
-    def get_org_ids(self, user):
-        """ Get org ids for user """
-        return set(
-            OrganizationMember.objects.filter(
-                user=user, organization__status=OrganizationStatus.ACTIVE
-            ).values_list("organization_id", flat=True)
-        )
+            for id, value in data.items():
+                user_option, CREATED = UserOption.objects.get_or_create(
+                    user=user,
+                    key="mail:email",
+                    project=projects_map[int(id)],
+                )
+                user_option.update(value=str(value))
 
-    def get_project_ids(self, user):
-        """ Get project ids that user has access to """
-        return set(
-            ProjectTeam.objects.filter(
-                team_id__in=OrganizationMemberTeam.objects.filter(
-                    organizationmember__user=user
-                ).values_list("team_id", flat=True)
-            ).values_list("project_id", flat=True)
-        )
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+    @staticmethod
+    def _handle_put_notification_settings(user, notification_type: FineTuningAPIKey, parents, data):
+        with transaction.atomic():
+            for parent in parents:
+                # We fetched every available parent but only care about the ones in `request.data`.
+                if str(parent.id) not in data:
+                    continue
+
+                # This partitioning always does the same thing because notification_type stays constant.
+                project_option, organization_option = (
+                    (None, parent)
+                    if notification_type == FineTuningAPIKey.DEPLOY
+                    else (parent, None)
+                )
+
+                type = get_type_from_fine_tuning_key(notification_type)
+                value = int(data[str(parent.id)])
+                NotificationSetting.objects.update_settings(
+                    ExternalProviders.EMAIL,
+                    type,
+                    get_option_value_from_int(type, value),
+                    user=user,
+                    project=project_option,
+                    organization=organization_option,
+                )
+
+        return Response(status=status.HTTP_204_NO_CONTENT)

+ 15 - 13
src/sentry/api/serializers/models/user_notifications.py

@@ -2,27 +2,29 @@ from collections import defaultdict
 
 from sentry.api.serializers import Serializer
 from sentry.models import UserOption
+from sentry.notifications.legacy_mappings import get_legacy_key_from_fine_tuning_key
+from sentry.notifications.types import FineTuningAPIKey
 
 
-# notification_option_key is one of:
-# - mail:alert
-# - workflow:notifications
-# - deploy-emails
-# - reports:disabled-organizations
-# - mail:email
 class UserNotificationsSerializer(Serializer):
     def get_attrs(self, item_list, user, **kwargs):
-        notification_option_key = kwargs["notification_option_key"]
-        filter_args = {}
+        notification_type = kwargs["notification_type"]
 
-        if notification_option_key in ["mail:alert", "workflow:notifications", "mail:email"]:
+        filter_args = {}
+        if notification_type in [
+            FineTuningAPIKey.ALERTS,
+            FineTuningAPIKey.EMAIL,
+            FineTuningAPIKey.WORKFLOW,
+        ]:
             filter_args["project__isnull"] = False
-        elif notification_option_key == "deploy":
+        elif notification_type == FineTuningAPIKey.DEPLOY:
             filter_args["organization__isnull"] = False
 
         data = list(
             UserOption.objects.filter(
-                key=notification_option_key, user__in=item_list, **filter_args
+                key=get_legacy_key_from_fine_tuning_key(notification_type),
+                user__in=item_list,
+                **filter_args,
             ).select_related("user", "project", "organization")
         )
 
@@ -34,11 +36,11 @@ class UserNotificationsSerializer(Serializer):
         return results
 
     def serialize(self, obj, attrs, user, **kwargs):
-        notification_option_key = kwargs["notification_option_key"]
+        notification_type = kwargs["notification_type"]
         data = {}
 
         for uo in attrs:
-            if notification_option_key == "reports:disabled-organizations":
+            if notification_type == FineTuningAPIKey.REPORTS:
                 # UserOption for key=reports:disabled-organizations saves a list of orgIds
                 # that should not receive reports
                 # This UserOption should have both project + organization = None

+ 1 - 1
src/sentry/models/groupsubscription.py

@@ -49,7 +49,7 @@ def get_user_options(key, user_ids, project, default):
         for option in UserOption.objects.filter(
             Q(project__isnull=True) | Q(project=project),
             user_id__in=user_ids,
-            key="workflow:notifications",
+            key=key,
         )
     }
 

+ 1 - 1
src/sentry/models/project.py

@@ -269,7 +269,7 @@ class Project(Model, PendingDeletionMixin):
                 for uo in UserOption.objects.filter(
                     key="subscribe_by_default", user__in=members_to_check
                 )
-                if uo.value == "0"
+                if str(uo.value) == "0"
             }
             member_set = [x for x in member_set if x not in disabled]
 

+ 12 - 0
src/sentry/models/user.py

@@ -320,6 +320,18 @@ class User(BaseModel, AbstractBaseUser):
             id__in=OrganizationMember.objects.filter(user=self).values("organization"),
         )
 
+    def get_projects(self):
+        from sentry.models import Project, ProjectStatus, ProjectTeam, OrganizationMemberTeam
+
+        return Project.objects.filter(
+            status=ProjectStatus.VISIBLE,
+            id__in=ProjectTeam.objects.filter(
+                team_id__in=OrganizationMemberTeam.objects.filter(
+                    organizationmember__user=self
+                ).values_list("team_id", flat=True)
+            ).values_list("project_id", flat=True),
+        )
+
     def get_orgs_require_2fa(self):
         from sentry.models import Organization, OrganizationStatus
 

+ 11 - 49
src/sentry/models/useroption.py

@@ -1,11 +1,9 @@
 from django.conf import settings
-from django.db import models, transaction
+from django.db import models
 
 from sentry.db.models import FlexibleForeignKey, Model, sane_repr
 from sentry.db.models.fields import EncryptedPickledObjectField
 from sentry.db.models.manager import OptionManager
-from sentry.models.integration import ExternalProviders
-from sentry.notifications.legacy_mappings import get_key_value_from_legacy, get_key_from_legacy
 
 
 option_scope_error = "this is not a supported use case, scope to project OR organization"
@@ -38,24 +36,7 @@ class UserOptionManager(OptionManager):
         """
         This isn't implemented for user-organization scoped options yet, because it hasn't been needed.
         """
-        from sentry.models.notificationsetting import NotificationSetting
-
-        with transaction.atomic():
-            if key in [
-                "workflow:notifications",
-                "mail:alert",
-                "deploy-emails",
-                "subscribe_by_default",
-            ]:
-                type = get_key_from_legacy(key)
-                NotificationSetting.objects.remove_settings(
-                    ExternalProviders.EMAIL,
-                    type,
-                    user=user,
-                    project=project,
-                )
-
-            self.filter(user=user, project=project, key=key).delete()
+        self.filter(user=user, project=project, key=key).delete()
 
         if not hasattr(self, "_metadata"):
             return
@@ -67,40 +48,21 @@ class UserOptionManager(OptionManager):
         self._option_cache[metakey].pop(key, None)
 
     def set_value(self, user, key, value, **kwargs):
-        from sentry.models.notificationsetting import NotificationSetting
-
         project = kwargs.get("project")
         organization = kwargs.get("organization")
 
         if organization and project:
             raise NotImplementedError(option_scope_error)
 
-        with transaction.atomic():
-            if key in [
-                "workflow:notifications",
-                "mail:alert",
-                "deploy-emails",
-                "subscribe_by_default",
-            ]:
-                type, option_value = get_key_value_from_legacy(key, value)
-                NotificationSetting.objects.update_settings(
-                    ExternalProviders.EMAIL,
-                    type,
-                    option_value,
-                    user=user,
-                    project=project,
-                    organization=organization,
-                )
-
-            inst, created = self.get_or_create(
-                user=user,
-                project=project,
-                organization=organization,
-                key=key,
-                defaults={"value": value},
-            )
-            if not created and inst.value != value:
-                inst.update(value=value)
+        inst, created = self.get_or_create(
+            user=user,
+            project=project,
+            organization=organization,
+            key=key,
+            defaults={"value": value},
+        )
+        if not created and inst.value != value:
+            inst.update(value=value)
 
         metakey = self._make_key(user, project=project, organization=organization)
 

+ 33 - 34
src/sentry/notifications/manager.py

@@ -8,14 +8,18 @@ from sentry.notifications.types import (
     NotificationSettingTypes,
 )
 from sentry.models.useroption import UserOption
-from sentry.notifications.legacy_mappings import KEYS_TO_LEGACY_KEYS, KEY_VALUE_TO_LEGACY_VALUE
+from sentry.notifications.legacy_mappings import (
+    KEYS_TO_LEGACY_KEYS,
+    get_legacy_key,
+    get_legacy_value,
+)
 
 
 def validate(type: NotificationSettingTypes, value: NotificationSettingOptionValues):
     """
     :return: boolean. True if the "value" is valid for the "type".
     """
-    return _get_legacy_value(type, value) is not None
+    return get_legacy_value(type, value) is not None
 
 
 def _get_scope(user_id, project=None, organization=None):
@@ -55,27 +59,6 @@ def _get_target(user=None, team=None):
     raise Exception("target must be either a user or a team")
 
 
-def _get_legacy_key(type: NotificationSettingTypes):
-    """
-    Temporary mapping from new enum types to legacy strings.
-    :param type: NotificationSettingTypes enum
-    :return: String
-    """
-
-    return KEYS_TO_LEGACY_KEYS.get(type)
-
-
-def _get_legacy_value(type: NotificationSettingTypes, value: NotificationSettingOptionValues):
-    """
-    Temporary mapping from new enum types to legacy strings. Each type has a separate mapping.
-    :param type: NotificationSettingTypes enum
-    :param value: NotificationSettingOptionValues enum
-    :return: String
-    """
-
-    return str(KEY_VALUE_TO_LEGACY_VALUE.get(type, {}).get(value))
-
-
 class NotificationsManager(BaseManager):
     """
     TODO(mgaeta): Add a caching layer for notification settings
@@ -143,7 +126,7 @@ class NotificationsManager(BaseManager):
         )
 
         legacy_value = UserOption.objects.get_value(
-            user, _get_legacy_key(type), project=project, organization=organization
+            user, get_legacy_key(type), project=project, organization=organization
         )
 
         # TODO(mgaeta): This line will be valid after the "copy migration".
@@ -195,6 +178,15 @@ class NotificationsManager(BaseManager):
         )
         target = _get_target(user, team)
 
+        key = get_legacy_key(type)
+        legacy_value = get_legacy_value(type, value)
+
+        # Annoying HACK to translate "subscribe_by_default"
+        if type == NotificationSettingTypes.ISSUE_ALERTS:
+            legacy_value = int(legacy_value)
+            if project is None:
+                key = "subscribe_by_default"
+
         with transaction.atomic():
             setting, created = self.get_or_create(
                 provider=provider.value,
@@ -207,6 +199,10 @@ class NotificationsManager(BaseManager):
             if not created and setting.value != value.value:
                 setting.update(value=value.value)
 
+            UserOption.objects.set_value(
+                user, key=key, value=legacy_value, project=project, organization=organization
+            )
+
     def remove_settings(
         self,
         provider: ExternalProviders,
@@ -233,22 +229,25 @@ class NotificationsManager(BaseManager):
         )
         target = _get_target(user, team)
 
-        self.filter(
-            provider=provider.value,
-            type=type.value,
-            scope_type=scope_type,
-            scope_identifier=scope_identifier,
-            target=target,
-        ).delete()
+        with transaction.atomic():
+            self.filter(
+                provider=provider.value,
+                type=type.value,
+                scope_type=scope_type,
+                scope_identifier=scope_identifier,
+                target=target,
+            ).delete()
+
+            UserOption.objects.unset_value(user, project, get_legacy_key(type))
 
     def remove_settings_for_user(self, user, type: NotificationSettingTypes = None):
         if type:
             # We don't need a transaction because this is only used in tests.
-            UserOption.objects.filter(user=user, key=_get_legacy_key(type)).delete()
-            self.filter(target=user, type=type.value).delete()
+            UserOption.objects.filter(user=user, key=get_legacy_key(type)).delete()
+            self.filter(target=user.actor, type=type.value).delete()
         else:
             UserOption.objects.filter(user=user, key__in=KEYS_TO_LEGACY_KEYS.values()).delete()
-            self.filter(target=user).delete()
+            self.filter(target=user.actor).delete()
 
     @staticmethod
     def remove_settings_for_team():

+ 18 - 3
src/sentry/web/frontend/accounts.py

@@ -12,7 +12,18 @@ from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 from django.views.decorators.http import require_http_methods
 
-from sentry.models import UserEmail, LostPasswordHash, Project, UserOption, Authenticator
+from sentry.models import (
+    Authenticator,
+    LostPasswordHash,
+    NotificationSetting,
+    Project,
+    UserEmail,
+)
+from sentry.models.integration import ExternalProviders
+from sentry.notifications.types import (
+    NotificationSettingTypes,
+    NotificationSettingOptionValues,
+)
 from sentry.security import capture_security_activity
 from sentry.signals import email_verified
 from sentry.web.decorators import login_required, signed_auth_required, set_referrer_policy
@@ -235,8 +246,12 @@ def email_unsubscribe_project(request, project_id):
 
     if request.method == "POST":
         if "cancel" not in request.POST:
-            UserOption.objects.set_value(
-                user=request.user, key="mail:alert", value=0, project=project
+            NotificationSetting.objects.update_settings(
+                ExternalProviders.EMAIL,
+                NotificationSettingTypes.ISSUE_ALERTS,
+                NotificationSettingOptionValues.NEVER,
+                user=request.user,
+                project=project,
             )
         return HttpResponseRedirect(auth.get_login_url())
 

Some files were not shown because too many files changed in this diff