Skip to content

Commit

Permalink
feat(dashboards): add new endpoint for favourite dashboards (#81438)
Browse files Browse the repository at this point in the history
1. Add new endpoint for favouriting dashboards (supports `PUT` req)
2. Return `isFavourite` field in `DashboardDetails` endpoint's `GET`
request

#78023

---------

Co-authored-by: harshithadurai <[email protected]>
Co-authored-by: Nar Saynorath <[email protected]>
  • Loading branch information
3 people authored Dec 3, 2024
1 parent b6edfcb commit cbadcc6
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 0 deletions.
39 changes: 39 additions & 0 deletions src/sentry/api/endpoints/organization_dashboard_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,42 @@ def post(self, request: Request, organization, dashboard) -> Response:
dashboard.save(update_fields=["visits", "last_visited"])

return Response(status=204)


@region_silo_endpoint
class OrganizationDashboardFavoriteEndpoint(OrganizationDashboardBase):
"""
Endpoint for managing the favorite status of dashboards for users
"""

publish_status = {
"PUT": ApiPublishStatus.PRIVATE,
}

def put(self, request: Request, organization, dashboard) -> Response:
"""
Toggle favorite status for current user by adding or removing
current user from dashboard favorites
"""
if not features.has("organizations:dashboards-favourite", organization, actor=request.user):
return Response(status=404)

if not features.has(EDIT_FEATURE, organization, actor=request.user):
return Response(status=404)

if isinstance(dashboard, dict):
return Response(status=204)

is_favorited = request.data.get("isFavorited")
current_favorites = set(dashboard.favorited_by)

if is_favorited and request.user.id not in current_favorites:
current_favorites.add(request.user.id)
elif not is_favorited and request.user.id in current_favorites:
current_favorites.remove(request.user.id)
else:
Response(status=204)

dashboard.favorited_by = current_favorites

return Response(status=204)
2 changes: 2 additions & 0 deletions src/sentry/api/serializers/models/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ class DashboardDetailsResponse(DashboardDetailsResponseOptional):
projects: list[int]
filters: DashboardFilters
permissions: DashboardPermissionsResponse | None
isFavorited: bool


@register(Dashboard)
Expand Down Expand Up @@ -316,6 +317,7 @@ def serialize(self, obj, attrs, user, **kwargs) -> DashboardDetailsResponse:
"projects": [project.id for project in obj.projects.all()],
"filters": {},
"permissions": serialize(obj.permissions) if hasattr(obj, "permissions") else None,
"isFavorited": user.id in obj.favorited_by,
}

if obj.filters is not None:
Expand Down
6 changes: 6 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@
from .endpoints.organization_config_repositories import OrganizationConfigRepositoriesEndpoint
from .endpoints.organization_dashboard_details import (
OrganizationDashboardDetailsEndpoint,
OrganizationDashboardFavoriteEndpoint,
OrganizationDashboardVisitEndpoint,
)
from .endpoints.organization_dashboard_widget_details import (
Expand Down Expand Up @@ -1292,6 +1293,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationDashboardVisitEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-visit",
),
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/dashboards/(?P<dashboard_id>[^\/]+)/favorite/$",
OrganizationDashboardFavoriteEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-favorite",
),
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/shortids/(?P<short_id>[^\/]+)/$",
ShortIdLookupEndpoint.as_view(),
Expand Down
1 change: 1 addition & 0 deletions src/sentry/apidocs/examples/dashboard_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"isEditableByEveryone": True,
"teamsWithEditAccess": [],
},
"isFavorited": False,
}

DASHBOARDS_OBJECT = [
Expand Down
126 changes: 126 additions & 0 deletions tests/sentry/api/endpoints/test_organization_dashboard_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,63 @@ def test_dashboard_details_data_returns_permissions_with_teams(self):
assert "teamsWithEditAccess" in response.data["permissions"]
assert response.data["permissions"]["teamsWithEditAccess"] == [team1.id, team2.id]

def test_get_favorited_user_status(self):
self.user_1 = self.create_user(email="[email protected]")
self.user_2 = self.create_user(email="[email protected]")
self.create_member(user=self.user_1, organization=self.organization)
self.create_member(user=self.user_2, organization=self.organization)

self.dashboard.favorited_by = [self.user_1.id, self.user_2.id]

self.login_as(user=self.user_1)
with self.feature({"organizations:dashboards-favourite": True}):
response = self.do_request("get", self.url(self.dashboard.id))
assert response.status_code == 200
assert response.data["isFavorited"] is True

def test_get_not_favorited_user_status(self):
self.user_1 = self.create_user(email="[email protected]")
self.create_member(user=self.user_1, organization=self.organization)
self.dashboard.favorited_by = [self.user_1.id, self.user.id]

user_3 = self.create_user()
self.create_member(user=user_3, organization=self.organization)
self.login_as(user=user_3)
with self.feature({"organizations:dashboards-favourite": True}):
response = self.do_request("get", self.url(self.dashboard.id))
assert response.status_code == 200
assert response.data["isFavorited"] is False

def test_get_favorite_status_no_dashboard_edit_access(self):
self.user_1 = self.create_user(email="[email protected]")
self.user_2 = self.create_user(email="[email protected]")
self.create_member(user=self.user_1, organization=self.organization)
self.create_member(user=self.user_2, organization=self.organization)

self.dashboard.favorited_by = [self.user_1.id, self.user_2.id, self.user.id]

DashboardPermissions.objects.create(is_editable_by_everyone=False, dashboard=self.dashboard)
self.login_as(user=self.user_2)
dashboard_detail_put_url = reverse(
"sentry-api-0-organization-dashboard-details",
kwargs={
"organization_id_or_slug": self.organization.slug,
"dashboard_id": self.dashboard.id,
},
)
with self.feature({"organizations:dashboards-edit-access": True}):
response = self.do_request(
"put", dashboard_detail_put_url, data={"title": "New Dashboard 9"}
)
# assert user cannot edit dashboard
assert response.status_code == 403

# assert user can see if they favorited the dashboard
with self.feature({"organizations:dashboards-favourite": True}):
response = self.do_request("get", self.url(self.dashboard.id))
assert response.status_code == 200
assert response.data["isFavorited"] is True


class OrganizationDashboardDetailsDeleteTest(OrganizationDashboardDetailsTestCase):
def test_delete(self):
Expand Down Expand Up @@ -3220,3 +3277,72 @@ def test_visit_dashboard_no_access(self):
dashboard = Dashboard.objects.get(id=self.dashboard.id)
assert dashboard.visits == 1
assert dashboard.last_visited == last_visited


class OrganizationDashboardFavoriteTest(OrganizationDashboardDetailsTestCase):
def setUp(self):
super().setUp()
# Create two additional users
self.user_1 = self.create_user(email="[email protected]")
self.user_2 = self.create_user(email="[email protected]")
self.create_member(user=self.user_1, organization=self.organization)
self.create_member(user=self.user_2, organization=self.organization)

# Both users have favorited the dashboard
self.dashboard.favorited_by = [self.user_1.id, self.user_2.id]

def url(self, dashboard_id):
return reverse(
"sentry-api-0-organization-dashboard-favorite",
kwargs={
"organization_id_or_slug": self.organization.slug,
"dashboard_id": dashboard_id,
},
)

# PUT tests
def test_favorite_dashboard(self):
assert self.user.id not in self.dashboard.favorited_by
self.login_as(user=self.user)
with self.feature({"organizations:dashboards-favourite": True}):
response = self.do_request(
"put", self.url(self.dashboard.id), data={"isFavorited": "true"}
)
assert response.status_code == 204
assert self.user.id in self.dashboard.favorited_by

def test_unfavorite_dashboard(self):
assert self.user_1.id in self.dashboard.favorited_by
self.login_as(user=self.user_1)
with self.feature({"organizations:dashboards-favourite": True}):
response = self.do_request(
"put", self.url(self.dashboard.id), data={"isFavorited": False}
)
assert response.status_code == 204
assert self.user_1.id not in self.dashboard.favorited_by

def test_favorite_dashboard_no_dashboard_edit_access(self):
DashboardPermissions.objects.create(is_editable_by_everyone=False, dashboard=self.dashboard)
self.login_as(user=self.user_2)
dashboard_detail_url = reverse(
"sentry-api-0-organization-dashboard-details",
kwargs={
"organization_id_or_slug": self.organization.slug,
"dashboard_id": self.dashboard.id,
},
)
with self.feature({"organizations:dashboards-edit-access": True}):
response = self.do_request(
"put", dashboard_detail_url, data={"title": "New Dashboard 9"}
)
# assert user cannot edit dashboard
assert response.status_code == 403

# assert if user can edit the favorite status of the dashboard
assert self.user_2.id in self.dashboard.favorited_by
with self.feature({"organizations:dashboards-favourite": True}):
response = self.do_request(
"put", self.url(self.dashboard.id), data={"isFavorited": False}
)
assert response.status_code == 204
assert self.user_2.id not in self.dashboard.favorited_by

0 comments on commit cbadcc6

Please sign in to comment.