Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(custom-views): add custom views get endpoint #71942

Merged
merged 10 commits into from
Jun 14, 2024
29 changes: 29 additions & 0 deletions src/sentry/api/serializers/models/groupsearchview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import TypedDict

from sentry.api.serializers import Serializer, register
from sentry.models.groupsearchview import GroupSearchView
from sentry.models.savedsearch import SORT_LITERALS


class GroupSearchViewSerializerResponse(TypedDict):
id: str
name: str
query: str
querySort: SORT_LITERALS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this not be typed as SortOptions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried for like an hour to get this to work before just giving up. For some reason, mypy refuses to enforce the type if I set it to SortOptions - I setquerySort of a view instance to "awioef" and mypy thought it was fine.

position: int
dateCreated: str | None
dateUpdated: str | None


@register(GroupSearchView)
class GroupSearchViewSerializer(Serializer):
def serialize(self, obj, attrs, user, **kwargs) -> GroupSearchViewSerializerResponse:
return {
"id": str(obj.id),
"name": obj.name,
"query": obj.query,
"querySort": obj.query_sort,
"position": obj.position,
"dateCreated": obj.date_added,
"dateUpdated": obj.date_updated,
}
6 changes: 6 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
GroupEventsEndpoint,
OrganizationActivityEndpoint,
OrganizationGroupIndexEndpoint,
OrganizationGroupSearchViewsEndpoint,
OrganizationReleasePreviousCommitsEndpoint,
OrganizationSearchesEndpoint,
ProjectStacktraceLinkEndpoint,
Expand Down Expand Up @@ -1703,6 +1704,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
),
name="sentry-api-0-organization-monitor-check-in-attachment",
),
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/group-search-views/$",
OrganizationGroupSearchViewsEndpoint.as_view(),
name="sentry-api-0-organization-group-search-views",
),
# Pinned and saved search
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/pinned-searches/$",
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/issues/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .group_events import GroupEventsEndpoint
from .organization_activity import OrganizationActivityEndpoint
from .organization_group_index import OrganizationGroupIndexEndpoint
from .organization_group_search_views import OrganizationGroupSearchViewsEndpoint
from .organization_release_previous_commits import OrganizationReleasePreviousCommitsEndpoint
from .organization_searches import OrganizationSearchesEndpoint
from .project_stacktrace_link import ProjectStacktraceLinkEndpoint
Expand All @@ -12,6 +13,7 @@
"GroupEventsEndpoint",
"OrganizationActivityEndpoint",
"OrganizationGroupIndexEndpoint",
"OrganizationGroupSearchViewsEndpoint",
"OrganizationReleasePreviousCommitsEndpoint",
"OrganizationSearchesEndpoint",
"ProjectStacktraceLinkEndpoint",
Expand Down
74 changes: 74 additions & 0 deletions src/sentry/issues/endpoints/organization_group_search_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.api.paginator import SequencePaginator
from sentry.api.serializers import serialize
from sentry.api.serializers.models.groupsearchview import (
GroupSearchViewSerializer,
GroupSearchViewSerializerResponse,
)
from sentry.models.groupsearchview import GroupSearchView
from sentry.models.organization import Organization
from sentry.models.savedsearch import SortOptions

DEFAULT_VIEWS: list[GroupSearchViewSerializerResponse] = [
{
"id": "",
"name": "Prioritized",
"query": "is:unresolved issue.priority:[high, medium]",
"querySort": SortOptions.DATE.value,
"position": 0,
"dateCreated": None,
"dateUpdated": None,
}
]


class MemberPermission(OrganizationPermission):
scope_map = {
"GET": ["member:read", "member:write"],
}


@region_silo_endpoint
class OrganizationGroupSearchViewsEndpoint(OrganizationEndpoint):
publish_status = {
"GET": ApiPublishStatus.EXPERIMENTAL,
}
owner = ApiOwner.ISSUES
permission_classes = (MemberPermission,)

def get(self, request: Request, organization: Organization) -> Response:
"""
List the current organization member's custom views
`````````````````````````````````````````

Retrieve a list of custom views for the current organization member.
"""
if not features.has("organizations:issue-stream-custom-views", organization):
return Response(status=status.HTTP_404_NOT_FOUND)

query = GroupSearchView.objects.filter(organization=organization, user_id=request.user.id)
MichaelSun48 marked this conversation as resolved.
Show resolved Hide resolved

# Return only the prioritized view if user has no custom views yet
MichaelSun48 marked this conversation as resolved.
Show resolved Hide resolved
if not query.exists():
return self.paginate(
request=request,
paginator=SequencePaginator(
[(idx, view) for idx, view in enumerate(DEFAULT_VIEWS)]
),
on_results=lambda results: serialize(results, request.user),
)

return self.paginate(
request=request,
queryset=query,
order_by="position",
on_results=lambda x: serialize(x, request.user, serializer=GroupSearchViewSerializer()),
)
8 changes: 6 additions & 2 deletions src/sentry/models/savedsearch.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any
from enum import StrEnum
from typing import Any, Literal

from django.db import models
from django.db.models import Q, UniqueConstraint
Expand All @@ -14,7 +15,7 @@
from sentry.models.search_common import SearchType


class SortOptions:
class SortOptions(StrEnum):
DATE = "date"
NEW = "new"
TRENDS = "trends"
Expand All @@ -34,6 +35,9 @@ def as_choices(cls):
)


SORT_LITERALS = Literal["date", "new", "trends", "freq", "user", "inbox"]


class Visibility:
ORGANIZATION = "organization"
OWNER = "owner"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from sentry.api.serializers.base import serialize
from sentry.models.groupsearchview import GroupSearchView
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.features import with_feature


class OrganizationGroupSearchViewsTest(APITestCase):
endpoint = "sentry-api-0-organization-group-search-views"
method = "get"

def create_base_data(self):
user_1 = self.user
self.user_2 = self.create_user()
self.user_3 = self.create_user()

self.create_member(organization=self.organization, user=self.user_2)
self.create_member(organization=self.organization, user=self.user_3)

first_custom_view_user_one = GroupSearchView.objects.create(
name="Custom View One",
organization=self.organization,
user_id=user_1.id,
query="is:unresolved",
query_sort="date",
position=0,
)

# This is out of order to test that the endpoint returns the views in the correct order
third_custom_view_user_one = GroupSearchView.objects.create(
name="Custom View Three",
organization=self.organization,
user_id=user_1.id,
query="is:ignored",
query_sort="freq",
position=2,
)

second_custom_view_user_one = GroupSearchView.objects.create(
name="Custom View Two",
organization=self.organization,
user_id=user_1.id,
query="is:resolved",
query_sort="new",
position=1,
)

first_custom_view_user_two = GroupSearchView.objects.create(
name="Custom View One",
organization=self.organization,
user_id=self.user_2.id,
query="is:unresolved",
query_sort="date",
position=0,
)

second_custom_view_user_two = GroupSearchView.objects.create(
name="Custom View Two",
organization=self.organization,
user_id=self.user_2.id,
query="is:resolved",
query_sort="new",
position=1,
)

return {
"user_one_views": [
first_custom_view_user_one,
second_custom_view_user_one,
third_custom_view_user_one,
],
"user_two_views": [first_custom_view_user_two, second_custom_view_user_two],
}

@with_feature({"organizations:issue-stream-custom-views": True})
def test_get_user_one_custom_views(self):
objs = self.create_base_data()

self.login_as(user=self.user)
response = self.get_success_response(self.organization.slug)

assert response.data == serialize(objs["user_one_views"])

@with_feature({"organizations:issue-stream-custom-views": True})
def test_get_user_two_custom_views(self):
objs = self.create_base_data()

self.login_as(user=self.user_2)
response = self.get_success_response(self.organization.slug)

assert response.data == serialize(objs["user_two_views"])

@with_feature({"organizations:issue-stream-custom-views": True})
def test_get_default_views(self):
self.create_base_data()

self.login_as(user=self.user_3)
response = self.get_success_response(self.organization.slug)
assert len(response.data) == 1

view = response.data[0]

assert view["name"] == "Prioritized"
assert view["query"] == "is:unresolved issue.priority:[high, medium]"
assert view["querySort"] == "date"
assert view["position"] == 0
Loading