Skip to content

Commit f98a882

Browse files
authored
feat(custom-views): add custom views get endpoint (#71942)
This endpoint creates the `GET` `organizations/<org_id_or_slug>/group-search-views/` Endpoint along with some tests. This endpoint will be responsible for fetching a user's custom views within an organization. ⚠️ This PR is dependent on #71731 being ~~merged~~ run in production, and this branch will need to be rebased upon that happening
1 parent 9051647 commit f98a882

File tree

6 files changed

+222
-2
lines changed

6 files changed

+222
-2
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import TypedDict
2+
3+
from sentry.api.serializers import Serializer, register
4+
from sentry.models.groupsearchview import GroupSearchView
5+
from sentry.models.savedsearch import SORT_LITERALS
6+
7+
8+
class GroupSearchViewSerializerResponse(TypedDict):
9+
id: str
10+
name: str
11+
query: str
12+
querySort: SORT_LITERALS
13+
position: int
14+
dateCreated: str | None
15+
dateUpdated: str | None
16+
17+
18+
@register(GroupSearchView)
19+
class GroupSearchViewSerializer(Serializer):
20+
def serialize(self, obj, attrs, user, **kwargs) -> GroupSearchViewSerializerResponse:
21+
return {
22+
"id": str(obj.id),
23+
"name": obj.name,
24+
"query": obj.query,
25+
"querySort": obj.query_sort,
26+
"position": obj.position,
27+
"dateCreated": obj.date_added,
28+
"dateUpdated": obj.date_updated,
29+
}

src/sentry/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
GroupEventsEndpoint,
116116
OrganizationActivityEndpoint,
117117
OrganizationGroupIndexEndpoint,
118+
OrganizationGroupSearchViewsEndpoint,
118119
OrganizationReleasePreviousCommitsEndpoint,
119120
OrganizationSearchesEndpoint,
120121
ProjectStacktraceLinkEndpoint,
@@ -1700,6 +1701,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
17001701
),
17011702
name="sentry-api-0-organization-monitor-check-in-attachment",
17021703
),
1704+
re_path(
1705+
r"^(?P<organization_id_or_slug>[^\/]+)/group-search-views/$",
1706+
OrganizationGroupSearchViewsEndpoint.as_view(),
1707+
name="sentry-api-0-organization-group-search-views",
1708+
),
17031709
# Pinned and saved search
17041710
re_path(
17051711
r"^(?P<organization_id_or_slug>[^\/]+)/pinned-searches/$",

src/sentry/issues/endpoints/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .group_events import GroupEventsEndpoint
33
from .organization_activity import OrganizationActivityEndpoint
44
from .organization_group_index import OrganizationGroupIndexEndpoint
5+
from .organization_group_search_views import OrganizationGroupSearchViewsEndpoint
56
from .organization_release_previous_commits import OrganizationReleasePreviousCommitsEndpoint
67
from .organization_searches import OrganizationSearchesEndpoint
78
from .project_stacktrace_link import ProjectStacktraceLinkEndpoint
@@ -12,6 +13,7 @@
1213
"GroupEventsEndpoint",
1314
"OrganizationActivityEndpoint",
1415
"OrganizationGroupIndexEndpoint",
16+
"OrganizationGroupSearchViewsEndpoint",
1517
"OrganizationReleasePreviousCommitsEndpoint",
1618
"OrganizationSearchesEndpoint",
1719
"ProjectStacktraceLinkEndpoint",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from rest_framework import status
2+
from rest_framework.request import Request
3+
from rest_framework.response import Response
4+
5+
from sentry import features
6+
from sentry.api.api_owners import ApiOwner
7+
from sentry.api.api_publish_status import ApiPublishStatus
8+
from sentry.api.base import region_silo_endpoint
9+
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
10+
from sentry.api.paginator import SequencePaginator
11+
from sentry.api.serializers import serialize
12+
from sentry.api.serializers.models.groupsearchview import (
13+
GroupSearchViewSerializer,
14+
GroupSearchViewSerializerResponse,
15+
)
16+
from sentry.models.groupsearchview import GroupSearchView
17+
from sentry.models.organization import Organization
18+
from sentry.models.savedsearch import SortOptions
19+
20+
DEFAULT_VIEWS: list[GroupSearchViewSerializerResponse] = [
21+
{
22+
"id": "",
23+
"name": "Prioritized",
24+
"query": "is:unresolved issue.priority:[high, medium]",
25+
"querySort": SortOptions.DATE.value,
26+
"position": 0,
27+
"dateCreated": None,
28+
"dateUpdated": None,
29+
}
30+
]
31+
32+
33+
class MemberPermission(OrganizationPermission):
34+
scope_map = {
35+
"GET": ["member:read", "member:write"],
36+
}
37+
38+
39+
@region_silo_endpoint
40+
class OrganizationGroupSearchViewsEndpoint(OrganizationEndpoint):
41+
publish_status = {
42+
"GET": ApiPublishStatus.EXPERIMENTAL,
43+
}
44+
owner = ApiOwner.ISSUES
45+
permission_classes = (MemberPermission,)
46+
47+
def get(self, request: Request, organization: Organization) -> Response:
48+
"""
49+
List the current organization member's custom views
50+
`````````````````````````````````````````
51+
52+
Retrieve a list of custom views for the current organization member.
53+
"""
54+
if not features.has("organizations:issue-stream-custom-views", organization):
55+
return Response(status=status.HTTP_404_NOT_FOUND)
56+
57+
query = GroupSearchView.objects.filter(organization=organization, user_id=request.user.id)
58+
59+
# Return only the prioritized view if user has no custom views yet
60+
if not query.exists():
61+
return self.paginate(
62+
request=request,
63+
paginator=SequencePaginator(
64+
[(idx, view) for idx, view in enumerate(DEFAULT_VIEWS)]
65+
),
66+
on_results=lambda results: serialize(results, request.user),
67+
)
68+
69+
return self.paginate(
70+
request=request,
71+
queryset=query,
72+
order_by="position",
73+
on_results=lambda x: serialize(x, request.user, serializer=GroupSearchViewSerializer()),
74+
)

src/sentry/models/savedsearch.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any
1+
from enum import StrEnum
2+
from typing import Any, Literal
23

34
from django.db import models
45
from django.db.models import Q, UniqueConstraint
@@ -14,7 +15,7 @@
1415
from sentry.models.search_common import SearchType
1516

1617

17-
class SortOptions:
18+
class SortOptions(StrEnum):
1819
DATE = "date"
1920
NEW = "new"
2021
TRENDS = "trends"
@@ -34,6 +35,9 @@ def as_choices(cls):
3435
)
3536

3637

38+
SORT_LITERALS = Literal["date", "new", "trends", "freq", "user", "inbox"]
39+
40+
3741
class Visibility:
3842
ORGANIZATION = "organization"
3943
OWNER = "owner"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from sentry.api.serializers.base import serialize
2+
from sentry.models.groupsearchview import GroupSearchView
3+
from sentry.testutils.cases import APITestCase
4+
from sentry.testutils.helpers.features import with_feature
5+
6+
7+
class OrganizationGroupSearchViewsTest(APITestCase):
8+
endpoint = "sentry-api-0-organization-group-search-views"
9+
method = "get"
10+
11+
def create_base_data(self):
12+
user_1 = self.user
13+
self.user_2 = self.create_user()
14+
self.user_3 = self.create_user()
15+
16+
self.create_member(organization=self.organization, user=self.user_2)
17+
self.create_member(organization=self.organization, user=self.user_3)
18+
19+
first_custom_view_user_one = GroupSearchView.objects.create(
20+
name="Custom View One",
21+
organization=self.organization,
22+
user_id=user_1.id,
23+
query="is:unresolved",
24+
query_sort="date",
25+
position=0,
26+
)
27+
28+
# This is out of order to test that the endpoint returns the views in the correct order
29+
third_custom_view_user_one = GroupSearchView.objects.create(
30+
name="Custom View Three",
31+
organization=self.organization,
32+
user_id=user_1.id,
33+
query="is:ignored",
34+
query_sort="freq",
35+
position=2,
36+
)
37+
38+
second_custom_view_user_one = GroupSearchView.objects.create(
39+
name="Custom View Two",
40+
organization=self.organization,
41+
user_id=user_1.id,
42+
query="is:resolved",
43+
query_sort="new",
44+
position=1,
45+
)
46+
47+
first_custom_view_user_two = GroupSearchView.objects.create(
48+
name="Custom View One",
49+
organization=self.organization,
50+
user_id=self.user_2.id,
51+
query="is:unresolved",
52+
query_sort="date",
53+
position=0,
54+
)
55+
56+
second_custom_view_user_two = GroupSearchView.objects.create(
57+
name="Custom View Two",
58+
organization=self.organization,
59+
user_id=self.user_2.id,
60+
query="is:resolved",
61+
query_sort="new",
62+
position=1,
63+
)
64+
65+
return {
66+
"user_one_views": [
67+
first_custom_view_user_one,
68+
second_custom_view_user_one,
69+
third_custom_view_user_one,
70+
],
71+
"user_two_views": [first_custom_view_user_two, second_custom_view_user_two],
72+
}
73+
74+
@with_feature({"organizations:issue-stream-custom-views": True})
75+
def test_get_user_one_custom_views(self):
76+
objs = self.create_base_data()
77+
78+
self.login_as(user=self.user)
79+
response = self.get_success_response(self.organization.slug)
80+
81+
assert response.data == serialize(objs["user_one_views"])
82+
83+
@with_feature({"organizations:issue-stream-custom-views": True})
84+
def test_get_user_two_custom_views(self):
85+
objs = self.create_base_data()
86+
87+
self.login_as(user=self.user_2)
88+
response = self.get_success_response(self.organization.slug)
89+
90+
assert response.data == serialize(objs["user_two_views"])
91+
92+
@with_feature({"organizations:issue-stream-custom-views": True})
93+
def test_get_default_views(self):
94+
self.create_base_data()
95+
96+
self.login_as(user=self.user_3)
97+
response = self.get_success_response(self.organization.slug)
98+
assert len(response.data) == 1
99+
100+
view = response.data[0]
101+
102+
assert view["name"] == "Prioritized"
103+
assert view["query"] == "is:unresolved issue.priority:[high, medium]"
104+
assert view["querySort"] == "date"
105+
assert view["position"] == 0

0 commit comments

Comments
 (0)