Просмотр исходного кода

feat(notifications): new tables for notification settings (#54735)

This PR adds two notification tables that are explained
[here](https://www.notion.so/sentry/Fix-Notification-Setting-Data-Model-c510e815759f4c3f81cf4f62294235ca)
in depth. Basically, we are splitting apart the existing
`NotificationSetting` table into two tables: one to track the provider
and another to track if notifications are enabled/disabled. The purpose
is so we can remove the complicated mapping logic between the data model
and the UI layer: each UI option will map to just a single row instead
of one row per provider.
Stephen Cefali 1 год назад
Родитель
Сommit
5ae77e7433

+ 24 - 0
fixtures/backup/model_dependencies/detailed.json

@@ -1406,6 +1406,30 @@
       "CONTROL"
       "CONTROL"
     ]
     ]
   },
   },
+  "sentry.NotificationSettingOption": {
+    "foreign_keys": {
+      "user": {
+        "kind": "FlexibleForeignKey",
+        "model": "sentry.User"
+      }
+    },
+    "model": "sentry.NotificationSettingOption",
+    "silos": [
+      "CONTROL"
+    ]
+  },
+  "sentry.NotificationSettingProvider": {
+    "foreign_keys": {
+      "user": {
+        "kind": "FlexibleForeignKey",
+        "model": "sentry.User"
+      }
+    },
+    "model": "sentry.NotificationSettingProvider",
+    "silos": [
+      "CONTROL"
+    ]
+  },
   "sentry.Option": {
   "sentry.Option": {
     "foreign_keys": {},
     "foreign_keys": {},
     "model": "sentry.Option",
     "model": "sentry.Option",

+ 6 - 0
fixtures/backup/model_dependencies/flat.json

@@ -324,6 +324,12 @@
   "sentry.NotificationSetting": [
   "sentry.NotificationSetting": [
     "sentry.User"
     "sentry.User"
   ],
   ],
+  "sentry.NotificationSettingOption": [
+    "sentry.User"
+  ],
+  "sentry.NotificationSettingProvider": [
+    "sentry.User"
+  ],
   "sentry.Option": [],
   "sentry.Option": [],
   "sentry.OrgAuthToken": [
   "sentry.OrgAuthToken": [
     "sentry.User"
     "sentry.User"

+ 2 - 0
fixtures/backup/model_dependencies/sorted.json

@@ -39,6 +39,8 @@
   "sentry.LatestRepoReleaseEnvironment",
   "sentry.LatestRepoReleaseEnvironment",
   "sentry.User",
   "sentry.User",
   "sentry.NotificationSetting",
   "sentry.NotificationSetting",
+  "sentry.NotificationSettingOption",
+  "sentry.NotificationSettingProvider",
   "sentry.OrganizationMapping",
   "sentry.OrganizationMapping",
   "sentry.OrganizationMemberMapping",
   "sentry.OrganizationMemberMapping",
   "sentry.OrgAuthToken",
   "sentry.OrgAuthToken",

+ 1 - 1
migrations_lockfile.txt

@@ -7,5 +7,5 @@ will then be regenerated, and you should be able to merge without conflicts.
 
 
 nodestore: 0002_nodestore_no_dictfield
 nodestore: 0002_nodestore_no_dictfield
 replays: 0003_add_size_to_recording_segment
 replays: 0003_add_size_to_recording_segment
-sentry: 0529_remove_pagerduty_service
+sentry: 0530_new_notification_tables
 social_auth: 0002_default_auto_field
 social_auth: 0002_default_auto_field

+ 133 - 0
src/sentry/migrations/0530_new_notification_tables.py

@@ -0,0 +1,133 @@
+# Generated by Django 3.2.20 on 2023-08-15 17:22
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
+import sentry.db.models.fields.bounded
+import sentry.db.models.fields.foreignkey
+import sentry.db.models.fields.hybrid_cloud_foreign_key
+from sentry.new_migrations.migrations import CheckedMigration
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production. For
+    # the most part, this should only be used for operations where it's safe to run the migration
+    # after your code has deployed. So this should not be used for most operations that alter the
+    # schema of a table.
+    # Here are some things that make sense to mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that they can
+    #   be monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   have ops run this and not block the deploy. Note that while adding an index is a schema
+    #   change, it's completely safe to run the operation after the code has deployed.
+    is_dangerous = False
+
+    dependencies = [
+        ("sentry", "0529_remove_pagerduty_service"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="NotificationSettingProvider",
+            fields=[
+                (
+                    "id",
+                    sentry.db.models.fields.bounded.BoundedBigAutoField(
+                        primary_key=True, serialize=False
+                    ),
+                ),
+                ("date_updated", models.DateTimeField(default=django.utils.timezone.now)),
+                ("date_added", models.DateTimeField(default=django.utils.timezone.now, null=True)),
+                ("scope_type", models.CharField(max_length=32)),
+                ("scope_identifier", sentry.db.models.fields.bounded.BoundedBigIntegerField()),
+                (
+                    "team_id",
+                    sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey(
+                        "sentry.Team", db_index=True, null=True, on_delete="CASCADE"
+                    ),
+                ),
+                ("type", models.CharField(max_length=32)),
+                ("value", models.CharField(max_length=32)),
+                ("provider", models.CharField(max_length=32)),
+                (
+                    "user",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                "db_table": "sentry_notificationsettingprovider",
+            },
+        ),
+        migrations.CreateModel(
+            name="NotificationSettingOption",
+            fields=[
+                (
+                    "id",
+                    sentry.db.models.fields.bounded.BoundedBigAutoField(
+                        primary_key=True, serialize=False
+                    ),
+                ),
+                ("date_updated", models.DateTimeField(default=django.utils.timezone.now)),
+                ("date_added", models.DateTimeField(default=django.utils.timezone.now, null=True)),
+                ("scope_type", models.CharField(max_length=32)),
+                ("scope_identifier", sentry.db.models.fields.bounded.BoundedBigIntegerField()),
+                (
+                    "team_id",
+                    sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey(
+                        "sentry.Team", db_index=True, null=True, on_delete="CASCADE"
+                    ),
+                ),
+                ("type", models.CharField(max_length=32)),
+                ("value", models.CharField(max_length=32)),
+                (
+                    "user",
+                    sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                "db_table": "sentry_notificationsettingoption",
+            },
+        ),
+        migrations.AddConstraint(
+            model_name="notificationsettingprovider",
+            constraint=models.CheckConstraint(
+                check=models.Q(
+                    models.Q(("team_id__isnull", False), ("user_id__isnull", True)),
+                    models.Q(("team_id__isnull", True), ("user_id__isnull", False)),
+                    _connector="OR",
+                ),
+                name="notification_setting_provider_team_or_user_check",
+            ),
+        ),
+        migrations.AlterUniqueTogether(
+            name="notificationsettingprovider",
+            unique_together={
+                ("scope_type", "scope_identifier", "user_id", "team_id", "provider", "type")
+            },
+        ),
+        migrations.AddConstraint(
+            model_name="notificationsettingoption",
+            constraint=models.CheckConstraint(
+                check=models.Q(
+                    models.Q(("team_id__isnull", False), ("user_id__isnull", True)),
+                    models.Q(("team_id__isnull", True), ("user_id__isnull", False)),
+                    _connector="OR",
+                ),
+                name="notification_setting_option_team_or_user_check",
+            ),
+        ),
+        migrations.AlterUniqueTogether(
+            name="notificationsettingoption",
+            unique_together={("scope_type", "scope_identifier", "user_id", "team_id", "type")},
+        ),
+    ]

+ 2 - 0
src/sentry/models/__init__.py

@@ -63,6 +63,8 @@ from .latestappconnectbuildscheck import *  # NOQA
 from .latestreporeleaseenvironment import *  # NOQA
 from .latestreporeleaseenvironment import *  # NOQA
 from .lostpasswordhash import *  # NOQA
 from .lostpasswordhash import *  # NOQA
 from .notificationsetting import *  # NOQA
 from .notificationsetting import *  # NOQA
+from .notificationsettingoption import *  # NOQA
+from .notificationsettingprovider import *  # NOQA
 from .options import *  # NOQA
 from .options import *  # NOQA
 from .organization import *  # NOQA
 from .organization import *  # NOQA
 from .organizationaccessrequest import *  # NOQA
 from .organizationaccessrequest import *  # NOQA

+ 35 - 0
src/sentry/models/notificationsettingbase.py

@@ -0,0 +1,35 @@
+import sentry_sdk
+from django.conf import settings
+from django.db import models
+
+from sentry.db.models import BoundedBigIntegerField, DefaultFieldsModel, FlexibleForeignKey
+from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
+
+
+class NotificationSettingBase(DefaultFieldsModel):
+    __include_in_export__ = False
+
+    scope_type = models.CharField(max_length=32, null=False)
+    scope_identifier = BoundedBigIntegerField(null=False)
+    team_id = HybridCloudForeignKey("sentry.Team", null=True, db_index=True, on_delete="CASCADE")
+    user = FlexibleForeignKey(
+        settings.AUTH_USER_MODEL, null=True, db_index=True, on_delete=models.CASCADE
+    )
+    type = models.CharField(max_length=32, null=False)
+    value = models.CharField(max_length=32, null=False)
+
+    class Meta:
+        abstract = True
+
+    def save(self, *args, **kwargs):
+        try:
+            assert not (
+                self.user_id is None and self.team_id is None
+            ), "Notification setting missing user & team"
+        except AssertionError as err:
+            sentry_sdk.capture_exception(err)
+        super().save(*args, **kwargs)
+
+
+# REQUIRED for migrations to run
+from sentry.trash import *  # NOQA

+ 38 - 0
src/sentry/models/notificationsettingoption.py

@@ -0,0 +1,38 @@
+from django.db import models
+
+from sentry.db.models import control_silo_only_model, sane_repr
+
+from .notificationsettingbase import NotificationSettingBase
+
+
+@control_silo_only_model
+class NotificationSettingOption(NotificationSettingBase):
+    __include_in_export__ = False
+
+    class Meta:
+        app_label = "sentry"
+        db_table = "sentry_notificationsettingoption"
+        unique_together = (
+            (
+                "scope_type",
+                "scope_identifier",
+                "user_id",
+                "team_id",
+                "type",
+            ),
+        )
+        constraints = [
+            models.CheckConstraint(
+                check=models.Q(team_id__isnull=False, user_id__isnull=True)
+                | models.Q(team_id__isnull=True, user_id__isnull=False),
+                name="notification_setting_option_team_or_user_check",
+            )
+        ]
+
+    __repr__ = sane_repr(
+        "scope_str",
+        "scope_identifier",
+        "target",
+        "type_str",
+        "value_str",
+    )

+ 43 - 0
src/sentry/models/notificationsettingprovider.py

@@ -0,0 +1,43 @@
+from django.db import models
+
+from sentry.db.models import control_silo_only_model, sane_repr
+
+from .notificationsettingbase import NotificationSettingBase
+
+
+@control_silo_only_model
+class NotificationSettingProvider(NotificationSettingBase):
+    __include_in_export__ = False
+
+    provider = models.CharField(max_length=32, null=False)
+
+    class Meta:
+        app_label = "sentry"
+        db_table = "sentry_notificationsettingprovider"
+        unique_together = (
+            (
+                "scope_type",
+                "scope_identifier",
+                "user_id",
+                "team_id",
+                "provider",
+                "type",
+            ),
+        )
+        constraints = [
+            models.CheckConstraint(
+                check=models.Q(team_id__isnull=False, user_id__isnull=True)
+                | models.Q(team_id__isnull=True, user_id__isnull=False),
+                name="notification_setting_provider_team_or_user_check",
+            )
+        ]
+
+    __repr__ = sane_repr(
+        "scope_str",
+        "scope_identifier",
+        "user_id",
+        "team_id",
+        "provider_str",
+        "type_str",
+        "value_str",
+    )