diff --git a/src/sentry/discover/endpoints/discover_saved_query_starred.py b/src/sentry/discover/endpoints/discover_saved_query_starred.py new file mode 100644 index 000000000000..107b600cc404 --- /dev/null +++ b/src/sentry/discover/endpoints/discover_saved_query_starred.py @@ -0,0 +1,84 @@ +from django.db.models import Q +from rest_framework import serializers, 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 cell_silo_endpoint +from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission +from sentry.discover.models import DiscoverSavedQuery, DiscoverSavedQueryStarred +from sentry.models.organization import Organization + + +class StarQuerySerializer(serializers.Serializer): + starred = serializers.BooleanField(required=True) + position = serializers.IntegerField(required=False) + + def validate(self, data): + if not data["starred"] and "position" in data: + raise serializers.ValidationError("Position is only allowed when starring a query.") + return data + + +class MemberPermission(OrganizationPermission): + scope_map = { + "POST": ["member:read", "member:write"], + } + + +@cell_silo_endpoint +class DiscoverSavedQueryStarredEndpoint(OrganizationEndpoint): + """ + Star or unstar a single saved Discover query. + """ + + publish_status = { + "POST": ApiPublishStatus.EXPERIMENTAL, + } + owner = ApiOwner.DATA_BROWSING + permission_classes = (MemberPermission,) + + def has_feature(self, organization, request): + return features.has( + "organizations:visibility-explore-view", organization, actor=request.user + ) and features.has( + "organizations:discover-queries-in-all-queries", organization, actor=request.user + ) + + def post(self, request: Request, organization: Organization, id: int) -> Response: + """ + Update the starred status of a saved Discover query for the current organization member. + """ + if not request.user.is_authenticated: + return Response(status=status.HTTP_400_BAD_REQUEST) + + if not self.has_feature(organization, request): + return self.respond(status=404) + + serializer = StarQuerySerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + is_starred = serializer.validated_data["starred"] + + try: + query = DiscoverSavedQuery.objects.get( + Q(is_homepage=False) | Q(is_homepage__isnull=True), id=id, organization=organization + ) + except DiscoverSavedQuery.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + if is_starred: + if DiscoverSavedQueryStarred.objects.insert_starred_query( + organization, request.user.id, query + ): + return Response(status=status.HTTP_200_OK) + else: + if DiscoverSavedQueryStarred.objects.delete_starred_query( + organization, request.user.id, query + ): + return Response(status=status.HTTP_200_OK) + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/tests/sentry/discover/endpoints/test_discover_saved_query_starred.py b/tests/sentry/discover/endpoints/test_discover_saved_query_starred.py new file mode 100644 index 000000000000..809c561931ee --- /dev/null +++ b/tests/sentry/discover/endpoints/test_discover_saved_query_starred.py @@ -0,0 +1,57 @@ +import pytest +from django.urls import reverse + +from sentry.discover.models import DiscoverSavedQuery, DiscoverSavedQueryStarred +from sentry.testutils.cases import APITestCase + + +@pytest.mark.skip(reason="API not public yet, this line will be removed in future") +class DiscoverSavedQueryStarredTest(APITestCase): + feature_flags = { + "organizations:visibility-explore-view": True, + "organizations:discover-queries-in-all-queries": True, + } + + def setUp(self) -> None: + super().setUp() + self.login_as(user=self.user) + self.org = self.create_organization(owner=self.user) + self.project_ids = [ + self.create_project(organization=self.org).id, + self.create_project(organization=self.org).id, + ] + query = {"fields": ["title"], "conditions": "", "limit": 10} + + model = DiscoverSavedQuery.objects.create( + organization=self.org, created_by_id=self.user.id, name="Test query", query=query + ) + + model.set_projects(self.project_ids) + + self.query_id = model.id + + self.url = reverse( + "sentry-api-0-discover-saved-query-starred", args=[self.org.slug, self.query_id] + ) + + def test_post(self) -> None: + with self.feature(self.feature_flags): + assert not DiscoverSavedQuery.objects.filter( + id__in=DiscoverSavedQueryStarred.objects.filter( + organization=self.org, user_id=self.user.id + ).values_list("discover_saved_query_id", flat=True) + ).exists() + response = self.client.post(self.url, data={"starred": "1"}) + assert response.status_code == 200, response.content + assert DiscoverSavedQuery.objects.filter( + id__in=DiscoverSavedQueryStarred.objects.filter( + organization=self.org, user_id=self.user.id + ).values_list("discover_saved_query_id", flat=True) + ).exists() + response = self.client.post(self.url, data={"starred": "0"}) + assert response.status_code == 200, response.content + assert not DiscoverSavedQuery.objects.filter( + id__in=DiscoverSavedQueryStarred.objects.filter( + organization=self.org, user_id=self.user.id + ).values_list("discover_saved_query_id", flat=True) + ).exists()