From 7439d11c46869b91afd23769c9c93d933ce7452c Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 30 Jan 2025 13:11:42 -0800 Subject: [PATCH 1/7] Update GET endpoint to validate view projects --- .../organization_group_search_views.py | 32 ++- .../test_organization_group_search_views.py | 187 +++++++++++++++++- 2 files changed, 213 insertions(+), 6 deletions(-) diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index 17dec2f7352a65..ae28c9a6ffb198 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -18,7 +18,7 @@ GroupSearchViewValidator, GroupSearchViewValidatorResponse, ) -from sentry.models.groupsearchview import GroupSearchView +from sentry.models.groupsearchview import DEFAULT_TIME_FILTER, GroupSearchView from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.savedsearch import SortOptions @@ -31,6 +31,9 @@ "query": "is:unresolved issue.priority:[high, medium]", "querySort": SortOptions.DATE.value, "position": 0, + "isAllProjects": False, + "environments": [], + "timeFilters": DEFAULT_TIME_FILTER, "dateCreated": None, "dateUpdated": None, } @@ -65,18 +68,40 @@ def get(self, request: Request, organization: Organization) -> Response: ): return Response(status=status.HTTP_404_NOT_FOUND) + has_global_views = features.has("organizations:global-views", organization) + query = GroupSearchView.objects.filter(organization=organization, user_id=request.user.id) - # Return only the prioritized view if user has no custom views yet + # Return only the default view(s) if user has no custom views yet if not query.exists(): return self.paginate( request=request, paginator=SequencePaginator( - [(idx, view) for idx, view in enumerate(DEFAULT_VIEWS)] + [ + ( + idx, + { + **view, + "projects": ( + [] + if has_global_views + else [pick_default_project(organization, request.user)] + ), + }, + ) + for idx, view in enumerate(DEFAULT_VIEWS) + ] ), on_results=lambda results: serialize(results, request.user), ) + if not has_global_views: + for view in query: + if view.is_all_projects or view.projects.count() > 1 or view.projects.count() == 0: + view.is_all_projects = False + view.projects.set([pick_default_project(organization, request.user)]) + view.save() + return self.paginate( request=request, queryset=query, @@ -90,7 +115,6 @@ def put(self, request: Request, organization: Organization) -> Response: will delete any views that are not included in the request, add views if they are new, and update existing views if they are included in the request. This endpoint is explcititly designed to be used by our frontend. - """ if not features.has( "organizations:issue-stream-custom-views", organization, actor=request.user diff --git a/tests/sentry/issues/endpoints/test_organization_group_search_views.py b/tests/sentry/issues/endpoints/test_organization_group_search_views.py index 598db2536cb0ab..87054006e8886b 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_search_views.py +++ b/tests/sentry/issues/endpoints/test_organization_group_search_views.py @@ -3,6 +3,7 @@ from sentry.api.serializers.base import serialize from sentry.api.serializers.rest_framework.groupsearchview import GroupSearchViewValidatorResponse +from sentry.issues.endpoints.organization_group_search_views import DEFAULT_VIEWS from sentry.models.groupsearchview import GroupSearchView from sentry.testutils.cases import APITestCase, TransactionTestCase from sentry.testutils.helpers.features import with_feature @@ -366,7 +367,7 @@ def create_base_data_with_page_filters(self) -> list[GroupSearchView]: query_sort="date", position=0, time_filters={"period": "14d"}, - environments=["production"], + environments=[], ) first_custom_view_user_one.projects.set([self.project1]) @@ -378,7 +379,7 @@ def create_base_data_with_page_filters(self) -> list[GroupSearchView]: query_sort="new", position=1, time_filters={"period": "7d"}, - environments=["staging"], + environments=["staging", "production"], ) second_custom_view_user_one.projects.set([self.project1, self.project2, self.project3]) @@ -584,6 +585,188 @@ def test_invalid_project_ids(self) -> None: assert response.content == b'{"detail":"One or more projects do not exist"}' +class OrganizationGroupSearchViewsGetPageFiltersTest(APITestCase): + def create_base_data_with_page_filters(self) -> list[GroupSearchView]: + user_1 = self.user + self.user_2 = self.create_user() + self.create_member(organization=self.organization, user=self.user_2) + # User 3 has no views, will get the defaults + self.user_3 = self.create_user() + self.create_member(organization=self.organization, user=self.user_3) + + self.team_1 = self.create_team(organization=self.organization, slug="team-1") + self.team_2 = self.create_team(organization=self.organization, slug="team-2") + + # User 1 is on team 1 only + self.create_team_membership(user=user_1, team=self.team_1) + # User 2 is on team 1 and team 2 + self.create_team_membership(user=self.user_2, team=self.team_1) + self.create_team_membership(user=self.user_2, team=self.team_2) + # User 3 is on team 1 only + self.create_team_membership(user=self.user_3, team=self.team_1) + + # This project should NEVER get chosen as a default since it does not belong to any teams + self.project1 = self.create_project( + organization=self.organization, slug="project-a", teams=[] + ) + # This project should be User 2's default project since it's the alphabetically the first one + self.project2 = self.create_project( + organization=self.organization, slug="project-b", teams=[self.team_2] + ) + # This should be User 1's default project since it's the only one that the user has access to + self.project3 = self.create_project( + organization=self.organization, slug="project-c", teams=[self.team_1, self.team_2] + ) + + first_issue_view_user_one = GroupSearchView.objects.create( + name="Issue View One", + organization=self.organization, + user_id=user_1.id, + query="is:unresolved", + query_sort="date", + position=0, + is_all_projects=False, + time_filters={"period": "14d"}, + environments=[], + ) + first_issue_view_user_one.projects.set([self.project3]) + + second_issue_view_user_one = GroupSearchView.objects.create( + name="Issue View Two", + organization=self.organization, + user_id=user_1.id, + query="is:resolved", + query_sort="new", + position=1, + is_all_projects=False, + time_filters={"period": "7d"}, + environments=["staging", "production"], + ) + second_issue_view_user_one.projects.set([]) + + third_issue_view_user_one = GroupSearchView.objects.create( + name="Issue View Three", + organization=self.organization, + user_id=user_1.id, + query="is:ignored", + query_sort="freq", + position=2, + is_all_projects=True, + time_filters={"period": "30d"}, + environments=["development"], + ) + third_issue_view_user_one.projects.set([]) + + first_issue_view_user_two = GroupSearchView.objects.create( + name="Issue View One", + organization=self.organization, + user_id=self.user_2.id, + query="is:unresolved", + query_sort="date", + position=0, + is_all_projects=False, + time_filters={"period": "14d"}, + environments=[], + ) + first_issue_view_user_two.projects.set([]) + + def setUp(self) -> None: + self.create_base_data_with_page_filters() + self.url = reverse( + "sentry-api-0-organization-group-search-views", + kwargs={"organization_id_or_slug": self.organization.slug}, + ) + + @with_feature({"organizations:issue-stream-custom-views": True}) + @with_feature({"organizations:global-views": True}) + def test_basic_get_page_filters_with_global_filters(self): + self.login_as(user=self.user) + response = self.client.get(self.url) + + assert response.data[0]["timeFilters"] == {"period": "14d"} + assert response.data[0]["projects"] == [self.project3.id] + assert response.data[0]["environments"] == [] + assert response.data[0]["isAllProjects"] is False + + assert response.data[1]["timeFilters"] == {"period": "7d"} + assert response.data[1]["projects"] == [] + assert response.data[1]["environments"] == ["staging", "production"] + assert response.data[1]["isAllProjects"] is False + + assert response.data[2]["timeFilters"] == {"period": "30d"} + assert response.data[2]["projects"] == [] + assert response.data[2]["environments"] == ["development"] + assert response.data[2]["isAllProjects"] is True + + @with_feature({"organizations:issue-stream-custom-views": True}) + @with_feature({"organizations:global-views": False}) + def test_get_page_filters_without_global_filters(self): + self.login_as(user=self.user) + response = self.client.get(self.url) + + assert response.data[0]["timeFilters"] == {"period": "14d"} + assert response.data[0]["projects"] == [self.project3.id] + assert response.data[0]["environments"] == [] + assert response.data[0]["isAllProjects"] is False + + assert response.data[1]["timeFilters"] == {"period": "7d"} + assert response.data[1]["projects"] == [self.project3.id] + assert response.data[1]["environments"] == ["staging", "production"] + assert response.data[1]["isAllProjects"] is False + + assert response.data[2]["timeFilters"] == {"period": "30d"} + assert response.data[2]["projects"] == [self.project3.id] + assert response.data[2]["environments"] == ["development"] + assert response.data[2]["isAllProjects"] is False + + @with_feature({"organizations:issue-stream-custom-views": True}) + @with_feature({"organizations:global-views": False}) + def test_get_page_filters_without_global_filters_user_2(self): + self.login_as(user=self.user_2) + response = self.client.get(self.url) + + assert response.data[0]["timeFilters"] == {"period": "14d"} + assert response.data[0]["projects"] == [self.project2.id] + assert response.data[0]["environments"] == [] + assert response.data[0]["isAllProjects"] is False + + @with_feature({"organizations:issue-stream-custom-views": True}) + @with_feature({"organizations:global-views": True}) + def test_default_page_filters_with_global_views(self): + self.login_as(user=self.user_3) + response = self.client.get(self.url) + + default_view_queries = {view["query"] for view in DEFAULT_VIEWS} + received_queries = {view["query"] for view in response.data} + + assert default_view_queries == received_queries + + for view in response.data: + assert view["timeFilters"] == {"period": "14d"} + # Global views means default project should be "My Projects" + assert view["projects"] == [] + assert view["environments"] == [] + assert view["isAllProjects"] is False + + @with_feature({"organizations:issue-stream-custom-views": True}) + @with_feature({"organizations:global-views": False}) + def test_default_page_filters_without_global_views(self): + self.login_as(user=self.user_3) + response = self.client.get(self.url) + + default_view_queries = {view["query"] for view in DEFAULT_VIEWS} + received_queries = {view["query"] for view in response.data} + + assert default_view_queries == received_queries + + for view in response.data: + assert view["timeFilters"] == {"period": "14d"} + # No global views means default project should be a single project + assert view["projects"] == [self.project3.id] + assert view["environments"] == [] + assert view["isAllProjects"] is False + + class OrganizationGroupSearchViewsPutRegressionTest(APITestCase): endpoint = "sentry-api-0-organization-group-search-views" method = "put" From a43050a9419dc0b758ab341c08c11571922e0170 Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 30 Jan 2025 13:39:37 -0800 Subject: [PATCH 2/7] Fix type errors in test file --- .../test_organization_group_search_views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/sentry/issues/endpoints/test_organization_group_search_views.py b/tests/sentry/issues/endpoints/test_organization_group_search_views.py index 87054006e8886b..2c68f54ac840de 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_search_views.py +++ b/tests/sentry/issues/endpoints/test_organization_group_search_views.py @@ -586,7 +586,7 @@ def test_invalid_project_ids(self) -> None: class OrganizationGroupSearchViewsGetPageFiltersTest(APITestCase): - def create_base_data_with_page_filters(self) -> list[GroupSearchView]: + def create_base_data_with_page_filters(self) -> None: user_1 = self.user self.user_2 = self.create_user() self.create_member(organization=self.organization, user=self.user_2) @@ -679,7 +679,7 @@ def setUp(self) -> None: @with_feature({"organizations:issue-stream-custom-views": True}) @with_feature({"organizations:global-views": True}) - def test_basic_get_page_filters_with_global_filters(self): + def test_basic_get_page_filters_with_global_filters(self) -> None: self.login_as(user=self.user) response = self.client.get(self.url) @@ -700,7 +700,7 @@ def test_basic_get_page_filters_with_global_filters(self): @with_feature({"organizations:issue-stream-custom-views": True}) @with_feature({"organizations:global-views": False}) - def test_get_page_filters_without_global_filters(self): + def test_get_page_filters_without_global_filters(self) -> None: self.login_as(user=self.user) response = self.client.get(self.url) @@ -721,7 +721,7 @@ def test_get_page_filters_without_global_filters(self): @with_feature({"organizations:issue-stream-custom-views": True}) @with_feature({"organizations:global-views": False}) - def test_get_page_filters_without_global_filters_user_2(self): + def test_get_page_filters_without_global_filters_user_2(self) -> None: self.login_as(user=self.user_2) response = self.client.get(self.url) @@ -732,7 +732,7 @@ def test_get_page_filters_without_global_filters_user_2(self): @with_feature({"organizations:issue-stream-custom-views": True}) @with_feature({"organizations:global-views": True}) - def test_default_page_filters_with_global_views(self): + def test_default_page_filters_with_global_views(self) -> None: self.login_as(user=self.user_3) response = self.client.get(self.url) @@ -750,7 +750,7 @@ def test_default_page_filters_with_global_views(self): @with_feature({"organizations:issue-stream-custom-views": True}) @with_feature({"organizations:global-views": False}) - def test_default_page_filters_without_global_views(self): + def test_default_page_filters_without_global_views(self) -> None: self.login_as(user=self.user_3) response = self.client.get(self.url) From e46dc0525c680a2042431e6dd1d22a207a88dba7 Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 30 Jan 2025 14:12:30 -0800 Subject: [PATCH 3/7] Fix minor test issue --- .../issues/endpoints/test_organization_group_search_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sentry/issues/endpoints/test_organization_group_search_views.py b/tests/sentry/issues/endpoints/test_organization_group_search_views.py index 2c68f54ac840de..c047161960cdc1 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_search_views.py +++ b/tests/sentry/issues/endpoints/test_organization_group_search_views.py @@ -429,7 +429,7 @@ def test_not_including_page_filters_does_not_reset_them_for_existing_views(self) # Ensure these have not been changed assert views[0]["timeFilters"] == {"period": "14d"} assert views[0]["projects"] == [self.project1.id] - assert views[0]["environments"] == ["production"] + assert views[0]["environments"] == [] @with_feature({"organizations:issue-stream-custom-views": True}) @with_feature({"organizations:global-views": True}) From 40973ff8829f15cb30802e63c0bd5d99ca4b285b Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Thu, 30 Jan 2025 15:01:18 -0800 Subject: [PATCH 4/7] Make project validation more efficient --- .../endpoints/organization_group_search_views.py | 13 +++++++++---- .../test_organization_group_search_views.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index ae28c9a6ffb198..eca98e4fb2aed5 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -96,11 +96,16 @@ def get(self, request: Request, organization: Organization) -> Response: ) if not has_global_views: - for view in query: - if view.is_all_projects or view.projects.count() > 1 or view.projects.count() == 0: + views_to_updates = [] + default_project = pick_default_project(organization, request.user) + + for view in query.prefetch_related("projects"): + if view.is_all_projects or view.projects.count() != 1: view.is_all_projects = False - view.projects.set([pick_default_project(organization, request.user)]) - view.save() + view.projects.set([default_project]) + views_to_updates.append(view) + + GroupSearchView.objects.bulk_update(views_to_updates, ["is_all_projects"]) return self.paginate( request=request, diff --git a/tests/sentry/issues/endpoints/test_organization_group_search_views.py b/tests/sentry/issues/endpoints/test_organization_group_search_views.py index c047161960cdc1..2f4b86b290b9c5 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_search_views.py +++ b/tests/sentry/issues/endpoints/test_organization_group_search_views.py @@ -414,7 +414,7 @@ def test_not_including_page_filters_does_not_reset_them_for_existing_views(self) # Original Page filters assert views[0]["timeFilters"] == {"period": "14d"} assert views[0]["projects"] == [self.project1.id] - assert views[0]["environments"] == ["production"] + assert views[0]["environments"] == [] view = views[0] # Change nothing but the name From 181c0627690741eb937215995a5afca9a37867dc Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Fri, 31 Jan 2025 17:24:20 -0800 Subject: [PATCH 5/7] move project validation to serializer, do not udpate db entry after validating --- .../api/serializers/models/groupsearchview.py | 21 ++++++++- .../organization_group_search_views.py | 25 +++++------ .../test_organization_group_search_views.py | 43 ++++++++++++++----- 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/sentry/api/serializers/models/groupsearchview.py b/src/sentry/api/serializers/models/groupsearchview.py index c2f785d5436b26..0e591f07c9313d 100644 --- a/src/sentry/api/serializers/models/groupsearchview.py +++ b/src/sentry/api/serializers/models/groupsearchview.py @@ -21,15 +21,32 @@ class GroupSearchViewSerializerResponse(TypedDict): @register(GroupSearchView) class GroupSearchViewSerializer(Serializer): + def __init__(self, *args, **kwargs): + self.has_global_views = kwargs.pop("has_global_views", None) + self.default_project = kwargs.pop("default_project", None) + super().__init__(*args, **kwargs) + def serialize(self, obj, attrs, user, **kwargs) -> GroupSearchViewSerializerResponse: + if self.has_global_views is False: + is_all_projects = False + + projects = list(obj.projects.values_list("id", flat=True)) + num_projects = len(projects) + if num_projects != 1: + projects = [projects[0] if num_projects > 1 else self.default_project] + + else: + is_all_projects = obj.is_all_projects + projects = list(obj.projects.values_list("id", flat=True)) + return { "id": str(obj.id), "name": obj.name, "query": obj.query, "querySort": obj.query_sort, "position": obj.position, - "projects": list(obj.projects.values_list("id", flat=True)), - "isAllProjects": obj.is_all_projects, + "projects": projects, + "isAllProjects": is_all_projects, "environments": obj.environments, "timeFilters": obj.time_filters, "dateCreated": obj.date_added, diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index eca98e4fb2aed5..4604c4654bbf02 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -95,23 +95,26 @@ def get(self, request: Request, organization: Organization) -> Response: on_results=lambda results: serialize(results, request.user), ) + default_project = None if not has_global_views: - views_to_updates = [] default_project = pick_default_project(organization, request.user) - - for view in query.prefetch_related("projects"): - if view.is_all_projects or view.projects.count() != 1: - view.is_all_projects = False - view.projects.set([default_project]) - views_to_updates.append(view) - - GroupSearchView.objects.bulk_update(views_to_updates, ["is_all_projects"]) + if default_project is None: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={"detail": "You do not have access to any projects."}, + ) return self.paginate( request=request, queryset=query, order_by="position", - on_results=lambda x: serialize(x, request.user, serializer=GroupSearchViewSerializer()), + on_results=lambda x: serialize( + x, + request.user, + serializer=GroupSearchViewSerializer( + has_global_views=has_global_views, default_project=default_project + ), + ), ) def put(self, request: Request, organization: Organization) -> Response: @@ -204,8 +207,6 @@ def pick_default_project(org: Organization, user: User | AnonymousUser) -> int: .values_list("id", flat=True) .first() ) - if default_user_project is None: - raise ValidationError("You do not have access to any projects") return default_user_project diff --git a/tests/sentry/issues/endpoints/test_organization_group_search_views.py b/tests/sentry/issues/endpoints/test_organization_group_search_views.py index 2f4b86b290b9c5..e700dfdb5bc2d4 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_search_views.py +++ b/tests/sentry/issues/endpoints/test_organization_group_search_views.py @@ -587,23 +587,23 @@ def test_invalid_project_ids(self) -> None: class OrganizationGroupSearchViewsGetPageFiltersTest(APITestCase): def create_base_data_with_page_filters(self) -> None: - user_1 = self.user - self.user_2 = self.create_user() - self.create_member(organization=self.organization, user=self.user_2) - # User 3 has no views, will get the defaults - self.user_3 = self.create_user() - self.create_member(organization=self.organization, user=self.user_3) - self.team_1 = self.create_team(organization=self.organization, slug="team-1") self.team_2 = self.create_team(organization=self.organization, slug="team-2") # User 1 is on team 1 only + user_1 = self.user self.create_team_membership(user=user_1, team=self.team_1) # User 2 is on team 1 and team 2 - self.create_team_membership(user=self.user_2, team=self.team_1) - self.create_team_membership(user=self.user_2, team=self.team_2) - # User 3 is on team 1 only - self.create_team_membership(user=self.user_3, team=self.team_1) + self.user_2 = self.create_user() + self.create_member( + organization=self.organization, user=self.user_2, teams=[self.team_1, self.team_2] + ) + # User 3 has no views and should get the default views + self.user_3 = self.create_user() + self.create_member(organization=self.organization, user=self.user_3, teams=[self.team_1]) + # User 4 is part of no teams, should error out + self.user_4 = self.create_user() + self.create_member(organization=self.organization, user=self.user_4) # This project should NEVER get chosen as a default since it does not belong to any teams self.project1 = self.create_project( @@ -670,6 +670,19 @@ def create_base_data_with_page_filters(self) -> None: ) first_issue_view_user_two.projects.set([]) + first_issue_view_user_four = GroupSearchView.objects.create( + name="Issue View One", + organization=self.organization, + user_id=self.user_4.id, + query="is:unresolved", + query_sort="date", + position=0, + is_all_projects=False, + time_filters={"period": "14d"}, + environments=[], + ) + first_issue_view_user_four.projects.set([]) + def setUp(self) -> None: self.create_base_data_with_page_filters() self.url = reverse( @@ -766,6 +779,14 @@ def test_default_page_filters_without_global_views(self) -> None: assert view["environments"] == [] assert view["isAllProjects"] is False + @with_feature({"organizations:issue-stream-custom-views": True}) + @with_feature({"organizations:global-views": False}) + def test_error_when_no_projects_found(self) -> None: + self.login_as(user=self.user_4) + response = self.client.get(self.url) + assert response.status_code == 400 + assert response.data == {"detail": "You do not have access to any projects."} + class OrganizationGroupSearchViewsPutRegressionTest(APITestCase): endpoint = "sentry-api-0-organization-group-search-views" From 17624abe050b2e0718f4a807dcc7b511015d3e8b Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Mon, 3 Feb 2025 09:17:58 -0800 Subject: [PATCH 6/7] Fix typing --- src/sentry/issues/endpoints/organization_group_search_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index 4604c4654bbf02..f328272c113614 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -198,7 +198,7 @@ def bulk_update_views( _update_existing_view(org, user_id, view, position=idx) -def pick_default_project(org: Organization, user: User | AnonymousUser) -> int: +def pick_default_project(org: Organization, user: User | AnonymousUser) -> int | None: user_teams = Team.objects.get_for_user(organization=org, user=user) user_team_ids = [team.id for team in user_teams] default_user_project = ( From 6a5d70516ffd9aa8030569431d8a51026947514b Mon Sep 17 00:00:00 2001 From: MichaelSun48 Date: Mon, 3 Feb 2025 14:08:25 -0800 Subject: [PATCH 7/7] Prefetch projects --- .../issues/endpoints/organization_group_search_views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index f328272c113614..4491e177edd356 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -70,7 +70,9 @@ def get(self, request: Request, organization: Organization) -> Response: has_global_views = features.has("organizations:global-views", organization) - query = GroupSearchView.objects.filter(organization=organization, user_id=request.user.id) + query = GroupSearchView.objects.filter( + organization=organization, user_id=request.user.id + ).prefetch_related("projects") # Return only the default view(s) if user has no custom views yet if not query.exists():