Skip to content

Commit

Permalink
feat(custom-views): add custom views get endpoint (#71942)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
MichaelSun48 authored Jun 14, 2024
1 parent 9051647 commit f98a882
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 2 deletions.
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
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 @@ -1700,6 +1701,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)

# Return only the prioritized view 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)]
),
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
105 changes: 105 additions & 0 deletions tests/sentry/issues/endpoints/test_organization_group_search_views.py
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

0 comments on commit f98a882

Please sign in to comment.