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(shared-views): Add groupsearchview starred endpoint for reordering #87347

Merged
merged 4 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@
SourceMapDebugEndpoint,
TeamGroupsOldEndpoint,
)
from sentry.issues.endpoints.organization_group_search_view_starred_order import (
OrganizationGroupSearchViewStarredOrderEndpoint,
)
from sentry.issues.endpoints.organization_issue_metrics import OrganizationIssueMetricsEndpoint
from sentry.monitors.endpoints.organization_monitor_checkin_index import (
OrganizationMonitorCheckInIndexEndpoint,
Expand Down Expand Up @@ -1787,6 +1790,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationGroupSearchViewVisitEndpoint.as_view(),
name="sentry-api-0-organization-group-search-view-visit",
),
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/group-search-views-starred-order/$",
OrganizationGroupSearchViewStarredOrderEndpoint.as_view(),
name="sentry-api-0-organization-group-search-view-starred-order",
),
# Pinned and saved search
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/pinned-searches/$",
Expand Down
1 change: 1 addition & 0 deletions src/sentry/issues/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"OrganizationGroupSearchViewsEndpoint",
"OrganizationGroupSearchViewDetailsEndpoint",
"OrganizationGroupSearchViewVisitEndpoint",
"OrganizationGroupSearchViewStarredEndpoint",
"OrganizationIssuesCountEndpoint",
"OrganizationReleasePreviousCommitsEndpoint",
"OrganizationSearchesEndpoint",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.db import IntegrityError, router, transaction
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 region_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewVisibility
from sentry.models.groupsearchviewstarred import GroupSearchViewStarred
from sentry.models.organization import Organization


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


class GroupSearchViewStarredOrderSerializer(serializers.Serializer):
view_ids = serializers.ListField(child=serializers.IntegerField(), required=True, min_length=0)

def validate_view_ids(self, view_ids):
if len(view_ids) != len(set(view_ids)):
raise serializers.ValidationError("Single view cannot take up multiple positions")

gsvs = GroupSearchView.objects.filter(
organization=self.context["organization"], id__in=view_ids
)
# This should never happen, but we can check just in case
if any(
gsv.user_id != self.context["user"].id
and gsv.visibility != GroupSearchViewVisibility.ORGANIZATION
for gsv in gsvs
):
raise serializers.ValidationError("You do not have access to one or more views")

return view_ids


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

def put(self, request: Request, organization: Organization) -> Response:
if not features.has("organizations:issue-view-sharing", organization, actor=request.user):
return Response(status=status.HTTP_400_BAD_REQUEST)

serializer = GroupSearchViewStarredOrderSerializer(
data=request.data, context={"organization": organization, "user": request.user}
)

if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

view_ids = serializer.validated_data["view_ids"]

try:
with transaction.atomic(using=router.db_for_write(GroupSearchViewStarred)):
Copy link
Member

Choose a reason for hiding this comment

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

Hmm I wonder if this transaction is necessary anymore since we are just doing a single bulk update

GroupSearchViewStarred.objects.reorder_starred_views(
organization=organization,
user_id=request.user.id,
new_view_positions=view_ids,
)
except IntegrityError as e:
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": e.args[0]})
except ValueError as e:
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": e.args[0]})

return Response(status=status.HTTP_204_NO_CONTENT)
44 changes: 44 additions & 0 deletions src/sentry/models/groupsearchviewstarred.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
from typing import ClassVar

from django.db import models
from django.db.models import UniqueConstraint

from sentry.backup.scopes import RelocationScope
from sentry.db.models import FlexibleForeignKey, region_silo_model
from sentry.db.models.base import DefaultFieldsModel
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
from sentry.db.models.manager.base import BaseManager
from sentry.models.organization import Organization


class GroupSearchViewStarredManager(BaseManager["GroupSearchViewStarred"]):
def reorder_starred_views(
self, organization: Organization, user_id: int, new_view_positions: list[int]
):
"""
Reorders the positions of starred views for a user in an organization.
Copy link
Member

Choose a reason for hiding this comment

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

Very nice explanatory comment!

Does NOT add or remove starred views.

Args:
organization: The organization the views belong to
user_id: The ID of the user whose starred views are being reordered
new_view_positions: List of view IDs in their new order

Raises:
ValueError: If there's a mismatch between existing starred views and the provided list
"""
existing_starred_views = self.filter(
organization=organization,
user_id=user_id,
)

existing_view_ids = {view.group_search_view_id for view in existing_starred_views}
new_view_ids = set(new_view_positions)

if existing_view_ids != new_view_ids:
raise ValueError("Mismatch between existing and provided starred views.")

position_map = {view_id: idx for idx, view_id in enumerate(new_view_positions)}

views_to_update = list(existing_starred_views)

for view in views_to_update:
view.position = position_map[view.group_search_view_id]

if views_to_update:
self.bulk_update(views_to_update, ["position"])


@region_silo_model
Expand All @@ -17,6 +59,8 @@ class GroupSearchViewStarred(DefaultFieldsModel):

position = models.PositiveSmallIntegerField()

objects: ClassVar[GroupSearchViewStarredManager] = GroupSearchViewStarredManager()

class Meta:
app_label = "sentry"
db_table = "sentry_groupsearchviewstarred"
Expand Down
Loading
Loading