Repository: getsentry/sentry
Pull request: ref(custom-views): Update tasks to delete custom views upon org or member deletion
This PR updates the Organization deletion task to make custom views a child of organization, thus deleting any custom views in the org upon org deletion. It also adds a new task for Organization members, and makes custom views a child of org members as well. 

Problem statement:
Custom Views: Delete member views upon removal
Since we have changed the database schema of a view to use `organization_id` and `user_id` instead of `org_member_id`, removing a member of an organization will not cascade delete their views, so we will need to do it manually. Eventually we may need to update this logic to only delete non-organization level views

Hints from issue discussion:

Commit message: Added to org deletion tests, created new organizationmember deletion test

Patch to review:
diff --git a/src/sentry/deletions/__init__.py b/src/sentry/deletions/__init__.py
index 9148230eeeee16..07fee3073f7d53 100644
--- a/src/sentry/deletions/__init__.py
+++ b/src/sentry/deletions/__init__.py
@@ -139,6 +139,7 @@ def load_defaults():
     default_manager.register(
         models.OrganizationIntegration, defaults.OrganizationIntegrationDeletionTask
     )
+    default_manager.register(models.OrganizationMember, defaults.OrganizationMemberDeletionTask)
     default_manager.register(models.OrganizationMemberTeam, BulkModelDeletionTask)
     default_manager.register(
         models.PlatformExternalIssue, defaults.PlatformExternalIssueDeletionTask
diff --git a/src/sentry/deletions/defaults/__init__.py b/src/sentry/deletions/defaults/__init__.py
index b5133c90a9148e..4df2e18dcf0243 100644
--- a/src/sentry/deletions/defaults/__init__.py
+++ b/src/sentry/deletions/defaults/__init__.py
@@ -10,6 +10,7 @@
 from .monitor_environment import *  # noqa: F401,F403
 from .organization import *  # noqa: F401,F403
 from .organizationintegration import *  # noqa: F401,F403
+from .organizationmember import *  # noqa: F401,F403
 from .platform_external_issue import *  # noqa: F401,F403
 from .project import *  # noqa: F401,F403
 from .pullrequest import *  # noqa: F401,F403
diff --git a/src/sentry/deletions/defaults/organization.py b/src/sentry/deletions/defaults/organization.py
index 659dc77fc3e766..beeb6762b6e63f 100644
--- a/src/sentry/deletions/defaults/organization.py
+++ b/src/sentry/deletions/defaults/organization.py
@@ -25,6 +25,7 @@ def get_child_relations(self, instance):
         from sentry.models.commitauthor import CommitAuthor
         from sentry.models.dashboard import Dashboard
         from sentry.models.environment import Environment
+        from sentry.models.groupsearchview import GroupSearchView
         from sentry.models.integrations.external_issue import ExternalIssue
         from sentry.models.organizationmember import OrganizationMember
         from sentry.models.project import Project
@@ -52,6 +53,7 @@ def get_child_relations(self, instance):
             PromptsActivity,
             ProjectTransactionThreshold,
             ArtifactBundle,
+            GroupSearchView,
         )
         relations.extend([ModelRelation(m, {"organization_id": instance.id}) for m in model_list])
         # Explicitly assign the task here as it was getting replaced with BulkModelDeletionTask in CI.
diff --git a/src/sentry/deletions/defaults/organizationmember.py b/src/sentry/deletions/defaults/organizationmember.py
new file mode 100644
index 00000000000000..16bd1399318260
--- /dev/null
+++ b/src/sentry/deletions/defaults/organizationmember.py
@@ -0,0 +1,10 @@
+from sentry.models.groupsearchview import GroupSearchView
+
+from ..base import ModelDeletionTask, ModelRelation
+
+
+class OrganizationMemberDeletionTask(ModelDeletionTask):
+    def get_child_relations(self, instance):
+        relations = [ModelRelation(GroupSearchView, {"user_id": instance.user_id})]
+
+        return relations
diff --git a/tests/sentry/deletions/test_organization.py b/tests/sentry/deletions/test_organization.py
index 44006c0e5c88b3..f41223b3379e26 100644
--- a/tests/sentry/deletions/test_organization.py
+++ b/tests/sentry/deletions/test_organization.py
@@ -12,6 +12,7 @@
 )
 from sentry.models.environment import Environment, EnvironmentProject
 from sentry.models.group import Group
+from sentry.models.groupsearchview import GroupSearchView
 from sentry.models.integrations.external_issue import ExternalIssue
 from sentry.models.organization import Organization, OrganizationStatus
 from sentry.models.organizationmapping import OrganizationMapping
@@ -98,6 +99,14 @@ def test_simple(self):
         widget_2_data_2 = DashboardWidgetQuery.objects.create(
             widget=widget_2, order=2, name="Outgoing data"
         )
+        custom_view = GroupSearchView.objects.create(
+            organization=org,
+            user_id=org_owner.id,
+            name="Custom View",
+            query="is:unresolved",
+            query_sort="date",
+            position=0,
+        )
 
         org.update(status=OrganizationStatus.PENDING_DELETION)
 
@@ -129,6 +138,7 @@ def test_simple(self):
         assert not DashboardWidgetQuery.objects.filter(
             id__in=[widget_1_data.id, widget_2_data_1.id, widget_2_data_2.id]
         ).exists()
+        assert not GroupSearchView.objects.filter(id=custom_view.id).exists()
 
     def test_no_delete_visible(self):
         org = self.create_organization(name="test")
diff --git a/tests/sentry/deletions/test_organizationmember.py b/tests/sentry/deletions/test_organizationmember.py
new file mode 100644
index 00000000000000..3cc1008bfd38be
--- /dev/null
+++ b/tests/sentry/deletions/test_organizationmember.py
@@ -0,0 +1,28 @@
+from sentry.models.groupsearchview import GroupSearchView
+from sentry.models.organizationmember import OrganizationMember
+from sentry.tasks.deletion.scheduled import run_scheduled_deletions
+from sentry.testutils.cases import APITestCase, TransactionTestCase
+from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
+
+
+class DeleteOrganizationMemberTest(APITestCase, TransactionTestCase, HybridCloudTestMixin):
+    def test_simple(self):
+        organization = self.create_organization(name="test")
+        user = self.create_user()
+        member = self.create_member(organization=organization, user=user)
+        custom_view = GroupSearchView.objects.create(
+            organization=organization,
+            user_id=user.id,
+            name="Custom View",
+            query="is:unresolved",
+            query_sort="date",
+            position=0,
+        )
+
+        self.ScheduledDeletion.schedule(instance=member, days=0)
+
+        with self.tasks():
+            run_scheduled_deletions()
+
+        assert not OrganizationMember.objects.filter(id=member.id).exists()
+        assert not GroupSearchView.objects.filter(id=custom_view.id).exists()


Code review comments:
diff_hunk: @@ -0,0 +1,10 @@
+from sentry.models.groupsearchview import GroupSearchView
+
+from ..base import ModelDeletionTask, ModelRelation
+
+
+class OrganizationMemberDeletionTask(ModelDeletionTask):
+    def get_child_relations(self, instance):
+        relations = [ModelRelation(GroupSearchView, {"user_id": instance.user_id})]
path: src/sentry/deletions/defaults/organizationmember.py
text: @user1:
Do we need to send the `organization_id` too? This seems like it would delete all of the user's views, but maybe I'm missing something

@author:
Yup I think you're right about that, fixing that now. I'll add a check in the tests to double check as well