Browse Source

chore(sentry-apps): Add avatars to the SentryAppsStats endpoint (#30799)

See API-2332

Jokes on me for thinking this would only be 2 PRs. In order to add the avatars in _admin tools, we'll need access to the avatars from this endpoint. It is already restricted to SuperUsers only so there isn't functionality change besides extra fields and we're not gonna be leaking anything. Tests have also been updated to reflect the change.
Leander Rodrigues 3 years ago
parent
commit
4e105da38c

+ 11 - 2
src/sentry/api/endpoints/sentry_apps_stats.py

@@ -4,7 +4,8 @@ from rest_framework.response import Response
 
 from sentry.api.bases import SentryAppsBaseEndpoint
 from sentry.api.permissions import SuperuserPermission
-from sentry.models import SentryApp
+from sentry.api.serializers import serialize
+from sentry.models import SentryApp, SentryAppAvatar
 
 
 class SentryAppsStatsEndpoint(SentryAppsBaseEndpoint):
@@ -20,8 +21,16 @@ class SentryAppsStatsEndpoint(SentryAppsBaseEndpoint):
         if "per_page" in request.query_params:
             sentry_apps = sentry_apps[: int(request.query_params["per_page"])]
 
+        avatars_to_app_map = SentryAppAvatar.objects.get_by_apps_as_dict(sentry_apps=sentry_apps)
         apps = [
-            {"id": app.id, "slug": app.slug, "name": app.name, "installs": app.installations__count}
+            {
+                "id": app.id,
+                "uuid": app.uuid,
+                "slug": app.slug,
+                "name": app.name,
+                "installs": app.installations__count,
+                "avatars": serialize(avatars_to_app_map[app.id], request.user),
+            }
             for app in sentry_apps
         ]
 

+ 13 - 3
src/sentry/api/serializers/models/sentry_app.py

@@ -7,6 +7,7 @@ from sentry.constants import SentryAppStatus
 from sentry.models import IntegrationFeature, SentryApp
 from sentry.models.integrationfeature import IntegrationTypes
 from sentry.models.sentryapp import MASKED_VALUE
+from sentry.models.sentryappavatar import SentryAppAvatar
 from sentry.models.user import User
 from sentry.utils.compat import map
 
@@ -14,11 +15,20 @@ from sentry.utils.compat import map
 @register(SentryApp)
 class SentryAppSerializer(Serializer):
     def get_attrs(self, item_list: List[SentryApp], user: User, **kwargs: Any):
-        features_by_sentry_app_id = IntegrationFeature.objects.get_by_targets_as_dict(
+        # Get associated IntegrationFeatures
+        app_feature_attrs = IntegrationFeature.objects.get_by_targets_as_dict(
             targets=item_list, target_type=IntegrationTypes.SENTRY_APP
         )
+
+        # Get associated SentryAppAvatars
+        app_avatar_attrs = SentryAppAvatar.objects.get_by_apps_as_dict(sentry_apps=item_list)
+
         return {
-            item: {"features": features_by_sentry_app_id.get(item.id, set())} for item in item_list
+            item: {
+                "features": app_feature_attrs.get(item.id, set()),
+                "avatars": app_avatar_attrs.get(item.id, set()),
+            }
+            for item in item_list
         }
 
     def serialize(self, obj, attrs, user, access):
@@ -64,6 +74,6 @@ class SentryAppSerializer(Serializer):
                 }
             )
 
-        data.update({"avatars": [serialize(avatar) for avatar in obj.avatar.all()]})
+        data.update({"avatars": serialize(attrs.get("avatars"), user)})
 
         return data

+ 20 - 0
src/sentry/models/sentryappavatar.py

@@ -1,11 +1,17 @@
+from collections import defaultdict
 from enum import Enum
+from typing import TYPE_CHECKING, List
 
 from django.db import models
 
 from sentry.db.models import FlexibleForeignKey
+from sentry.db.models.manager import BaseManager
 
 from . import AvatarBase
 
+if TYPE_CHECKING:
+    from sentry.models.sentryapp import SentryApp
+
 
 class SentryAppAvatarTypes(Enum):
     DEFAULT = 0
@@ -16,12 +22,26 @@ class SentryAppAvatarTypes(Enum):
         return tuple((_.value, _.name.lower()) for _ in SentryAppAvatarTypes)
 
 
+class SentryAppAvatarManager(BaseManager):
+    def get_by_apps_as_dict(self, sentry_apps: List["SentryApp"]):
+        """
+        Returns a dict mapping sentry_app_id (key) to List[SentryAppAvatar] (value)
+        """
+        avatars = SentryAppAvatar.objects.filter(sentry_app__in=sentry_apps)
+        avatar_to_app_map = defaultdict(set)
+        for avatar in avatars:
+            avatar_to_app_map[avatar.sentry_app_id].add(avatar)
+        return avatar_to_app_map
+
+
 class SentryAppAvatar(AvatarBase):
     """
     A SentryAppAvatar associates a SentryApp with a logo photo File
     and specifies which type of logo it is.
     """
 
+    objects = SentryAppAvatarManager()
+
     AVATAR_TYPES = SentryAppAvatarTypes.get_choices()
 
     FILE_TYPE = "avatar.file"

+ 1 - 0
static/app/constants/index.tsx

@@ -187,6 +187,7 @@ export const AVATAR_URL_MAP = {
   user: 'avatar',
   sentryAppColor: 'sentry-app-avatar',
   sentryAppSimple: 'sentry-app-avatar',
+  docIntegration: 'doc-integration-avatar',
 };
 
 export const MENU_CLOSE_DELAY = 200;

+ 1 - 0
static/app/types/integrations.tsx

@@ -254,6 +254,7 @@ export type DocIntegration = {
   url: string;
   popularity: number;
   description: string;
+  isDraft: boolean;
   avatar: Avatar;
   features?: IntegrationFeature[];
   resources?: Array<{title: string; url: string}>;

+ 9 - 0
tests/sentry/api/endpoints/test_sentry_apps_stats.py

@@ -1,5 +1,7 @@
 from django.urls import reverse
 
+from sentry.api.serializers.base import serialize
+from sentry.models.sentryappavatar import SentryAppAvatar
 from sentry.testutils import APITestCase
 from sentry.utils import json
 
@@ -14,6 +16,9 @@ class SentryAppsStatsTest(APITestCase):
         self.app_1 = self.create_sentry_app(
             name="Test", organization=self.super_org, published=True
         )
+        self.app_1_avatar = SentryAppAvatar.objects.create(
+            sentry_app=self.app_1, color=True, avatar_type=0
+        )
 
         self.app_2 = self.create_sentry_app(name="Testin", organization=self.org)
 
@@ -30,16 +35,20 @@ class SentryAppsStatsTest(APITestCase):
         assert response.status_code == 200
         assert {
             "id": self.app_2.id,
+            "uuid": self.app_2.uuid,
             "slug": self.app_2.slug,
             "name": self.app_2.name,
             "installs": 1,
+            "avatars": [],
         } in json.loads(response.content)
 
         assert {
             "id": self.app_1.id,
+            "uuid": self.app_1.uuid,
             "slug": self.app_1.slug,
             "name": self.app_1.name,
             "installs": 1,
+            "avatars": [serialize(self.app_1_avatar)],
         } in json.loads(response.content)
 
     def test_nonsuperusers_have_no_access(self):