Browse Source

chore(api): Publish `TeamDetailsEndpoint` (#71184)

This PR removes previous JSON documentation for TeamDetailsEndpoint and
adds new documentation to publish the endpoint.

<img width="1561" alt="image"
src="https://github.com/getsentry/sentry/assets/33237075/86763ea5-8449-4119-bb37-e81158548247">

<img width="1561" alt="image"
src="https://github.com/getsentry/sentry/assets/33237075/35bc1267-e62f-485c-a3a1-e42d896de487">

<img width="1561" alt="image"
src="https://github.com/getsentry/sentry/assets/33237075/3a14dac5-c101-4c57-81df-3995a277c8b7">


For:
https://github.com/getsentry/team-core-product-foundations/issues/325
Raj Joshi 9 months ago
parent
commit
32a650cf2a

+ 0 - 3
api-docs/openapi.json

@@ -87,9 +87,6 @@
     }
   ],
   "paths": {
-    "/api/0/teams/{organization_id_or_slug}/{team_id_or_slug}/": {
-      "$ref": "paths/teams/by-slug.json"
-    },
     "/api/0/teams/{organization_id_or_slug}/{team_id_or_slug}/stats/": {
       "$ref": "paths/teams/stats.json"
     },

+ 0 - 210
api-docs/paths/teams/by-slug.json

@@ -1,210 +0,0 @@
-{
-  "get": {
-    "tags": ["Teams"],
-    "description": "Return details on an individual team.",
-    "operationId": "Retrieve a Team",
-    "parameters": [
-      {
-        "name": "organization_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the organization the team belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "team_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the team to get.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "responses": {
-      "200": {
-        "description": "Success",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/team.json#/TeamWithOrganization"
-            },
-            "example": {
-              "avatar": {
-                "avatarType": "letter_avatar",
-                "avatarUuid": null
-              },
-              "dateCreated": "2018-11-06T21:19:55.114Z",
-              "hasAccess": true,
-              "id": "2",
-              "isMember": true,
-              "isPending": false,
-              "memberCount": 1,
-              "name": "Powerful Abolitionist",
-              "organization": {
-                "avatar": {
-                  "avatarType": "letter_avatar",
-                  "avatarUuid": null
-                },
-                "dateCreated": "2018-11-06T21:19:55.101Z",
-                "id": "2",
-                "isEarlyAdopter": false,
-                "name": "The Interstellar Jurisdiction",
-                "require2FA": false,
-                "slug": "the-interstellar-jurisdiction",
-                "status": {
-                  "id": "active",
-                  "name": "active"
-                }
-              },
-              "slug": "powerful-abolitionist"
-            }
-          }
-        }
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "404": {
-        "description": "Team not found"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["team:read"]
-      }
-    ]
-  },
-  "put": {
-    "tags": ["Teams"],
-    "description": "Update various attributes settings for the given team.",
-    "operationId": "Update a Team",
-    "parameters": [
-      {
-        "name": "organization_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the organization the team belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "team_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the team to get.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "requestBody": {
-      "content": {
-        "application/json": {
-          "schema": {
-            "type": "object",
-            "properties": {
-              "slug": {
-                "type": "string",
-                "description": "Uniquely identifies a team and is used for the interface. Must be available."
-              },
-              "name": {
-                "type": "string",
-                "description": "**`[DEPRECATED]`** The name for the team.",
-                "deprecated": true
-              }
-            }
-          },
-          "example": {
-            "name": "The Inflated Philosophers",
-            "slug": "the-inflated-philosophers"
-          }
-        }
-      },
-      "required": true
-    },
-    "responses": {
-      "200": {
-        "description": "Success",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/team.json#/Team"
-            },
-            "example": {
-              "avatar": {
-                "avatarType": "letter_avatar"
-              },
-              "dateCreated": "2018-11-06T21:20:08.115Z",
-              "hasAccess": true,
-              "id": "3",
-              "isMember": false,
-              "isPending": false,
-              "memberCount": 1,
-              "name": "The Inflated Philosophers",
-              "slug": "the-inflated-philosophers"
-            }
-          }
-        }
-      },
-      "400": {
-        "description": "Bad Input"
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "404": {
-        "description": "Team not found"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["team:write"]
-      }
-    ]
-  },
-  "delete": {
-    "tags": ["Teams"],
-    "description": "Schedules a team for deletion.\n\nNote: Deletion happens asynchronously and therefore is not immediate. However once deletion has begun the state of a project changes and will be hidden from most public views.",
-    "operationId": "Delete a Team",
-    "parameters": [
-      {
-        "name": "organization_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the organization the team belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "team_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the team to get.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "responses": {
-      "204": {
-        "description": "Success"
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "404": {
-        "description": "Team not found"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["team:admin"]
-      }
-    ]
-  }
-}

+ 56 - 35
src/sentry/api/endpoints/team_details.py

@@ -1,6 +1,7 @@
 from uuid import uuid4
 
 from django.db import router, transaction
+from drf_spectacular.utils import extend_schema, extend_schema_serializer
 from rest_framework import serializers, status
 from rest_framework.request import Request
 from rest_framework.response import Response
@@ -12,15 +13,25 @@ from sentry.api.bases.team import TeamEndpoint
 from sentry.api.decorators import sudo_required
 from sentry.api.fields.sentry_slug import SentrySerializerSlugField
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.team import TeamSerializer as ModelTeamSerializer
+from sentry.api.serializers.models.team import TeamSerializer as TeamRequestSerializer
 from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer
+from sentry.apidocs.constants import (
+    RESPONSE_FORBIDDEN,
+    RESPONSE_NO_CONTENT,
+    RESPONSE_NOT_FOUND,
+    RESPONSE_UNAUTHORIZED,
+)
+from sentry.apidocs.examples.team_examples import TeamExamples
+from sentry.apidocs.parameters import GlobalParams, TeamParams
 from sentry.models.scheduledeletion import RegionScheduledDeletion
 from sentry.models.team import Team, TeamStatus
 
 
-class TeamSerializer(CamelSnakeModelSerializer):
+@extend_schema_serializer(exclude_fields=["name"])
+class TeamDetailsSerializer(CamelSnakeModelSerializer):
     slug = SentrySerializerSlugField(
         max_length=50,
+        help_text="Uniquely identifies a team. This is must be available.",
     )
 
     class Meta:
@@ -36,29 +47,34 @@ class TeamSerializer(CamelSnakeModelSerializer):
         return value
 
 
+@extend_schema(tags=["Teams"])
 @region_silo_endpoint
 class TeamDetailsEndpoint(TeamEndpoint):
     publish_status = {
-        "DELETE": ApiPublishStatus.UNKNOWN,
-        "GET": ApiPublishStatus.UNKNOWN,
-        "PUT": ApiPublishStatus.UNKNOWN,
+        "DELETE": ApiPublishStatus.PUBLIC,
+        "GET": ApiPublishStatus.PUBLIC,
+        "PUT": ApiPublishStatus.PUBLIC,
     }
 
+    @extend_schema(
+        operation_id="Retrieve a Team",
+        parameters=[
+            GlobalParams.ORG_ID_OR_SLUG,
+            GlobalParams.TEAM_ID_OR_SLUG,
+            TeamParams.EXPAND,
+            TeamParams.COLLAPSE,
+        ],
+        responses={
+            200: TeamRequestSerializer,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=TeamExamples.RETRIEVE_TEAM_DETAILS,
+    )
     def get(self, request: Request, team) -> Response:
         """
-        Retrieve a Team
-        ```````````````
-
         Return details on an individual team.
-
-        :pparam string organization_id_or_slug: the id or slug of the organization the
-                                          team belongs to.
-        :pparam string team_id_or_slug: the id or slug of the team to get.
-        :qparam list expand: an optional list of strings to opt in to additional
-            data. Supports `projects`, `externalTeams`.
-        :qparam list collapse: an optional list of strings to opt out of certain
-            pieces of data. Supports `organization`.
-        :auth: required
         """
         collapse = request.GET.getlist("collapse", [])
         expand = request.GET.getlist("expand", [])
@@ -70,28 +86,27 @@ class TeamDetailsEndpoint(TeamEndpoint):
             expand.append("organization")
 
         return Response(
-            serialize(team, request.user, ModelTeamSerializer(collapse=collapse, expand=expand))
+            serialize(team, request.user, TeamRequestSerializer(collapse=collapse, expand=expand))
         )
 
+    @extend_schema(
+        operation_id="Update a Team",
+        parameters=[GlobalParams.ORG_ID_OR_SLUG, GlobalParams.TEAM_ID_OR_SLUG],
+        request=TeamDetailsSerializer,
+        responses={
+            200: TeamRequestSerializer,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=TeamExamples.UPDATE_TEAM,
+    )
     def put(self, request: Request, team) -> Response:
         """
-        Update a Team
-        `````````````
-
         Update various attributes and configurable settings for the given
         team.
-
-        :pparam string organization_id_or_slug: the id or slug of the organization the
-                                          team belongs to.
-        :pparam string team_id_or_slug: the id or slug of the team to get.
-        :param string name: the new name for the team.
-        :param string slug: a new slug for the team.  It has to be unique
-                            and available.
-        :param string orgRole: an organization role for the team. Only
-                               owners can set this value.
-        :auth: required
         """
-        serializer = TeamSerializer(team, data=request.data, partial=True)
+        serializer = TeamDetailsSerializer(team, data=request.data, partial=True)
         if serializer.is_valid():
             team = serializer.save()
 
@@ -108,12 +123,18 @@ class TeamDetailsEndpoint(TeamEndpoint):
 
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
+    @extend_schema(
+        operation_id="Delete a Team",
+        parameters=[GlobalParams.ORG_ID_OR_SLUG, GlobalParams.TEAM_ID_OR_SLUG],
+        responses={
+            204: RESPONSE_NO_CONTENT,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+    )
     @sudo_required
     def delete(self, request: Request, team) -> Response:
         """
-        Delete a Team
-        `````````````
-
         Schedules a team for deletion.
 
         **Note:** Deletion happens asynchronously and therefore is not

+ 79 - 0
src/sentry/apidocs/examples/team_examples.py

@@ -389,3 +389,82 @@ class TeamExamples:
             response_only=True,
         )
     ]
+
+    RETRIEVE_TEAM_DETAILS = [
+        OpenApiExample(
+            "Retrieve a Team",
+            value={
+                "avatar": {"avatarType": "letter_avatar", "avatarUuid": None},
+                "dateCreated": "2018-11-06T21:19:55.114Z",
+                "hasAccess": True,
+                "id": "2",
+                "isMember": True,
+                "isPending": False,
+                "memberCount": 1,
+                "name": "Powerful Abolitionist",
+                "organization": {
+                    "avatar": {"avatarType": "letter_avatar", "avatarUuid": None},
+                    "dateCreated": "2018-11-06T21:19:55.101Z",
+                    "id": "2",
+                    "isEarlyAdopter": False,
+                    "name": "The Interstellar Jurisdiction",
+                    "require2FA": False,
+                    "slug": "the-interstellar-jurisdiction",
+                    "status": {"id": "active", "name": "active"},
+                    "requireEmailVerification": False,
+                    "features": ["session-replay-videos"],
+                    "hasAuthProvider": True,
+                    "links": {
+                        "organizationUrl": "https://philosophers.sentry.io",
+                        "regionUrl": "https://us.sentry.io",
+                    },
+                },
+                "slug": "powerful-abolitionist",
+                "access": [
+                    "event:read",
+                    "event:write",
+                    "team:read",
+                    "org:read",
+                    "project:read",
+                    "member:read",
+                    "project:releases",
+                    "alerts:read",
+                ],
+                "flags": {"idp:provisioned": False},
+                "teamRole": "contributor",
+            },
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]
+
+    UPDATE_TEAM = [
+        OpenApiExample(
+            "Update a Team",
+            value={
+                "avatar": {"avatarType": "letter_avatar"},
+                "dateCreated": "2018-11-06T21:20:08.115Z",
+                "hasAccess": True,
+                "id": "3",
+                "isMember": False,
+                "isPending": False,
+                "memberCount": 1,
+                "name": "The Inflated Philosophers",
+                "slug": "the-inflated-philosophers",
+                "access": [
+                    "event:read",
+                    "event:write",
+                    "team:read",
+                    "org:read",
+                    "project:read",
+                    "member:read",
+                    "project:releases",
+                    "alerts:read",
+                ],
+                "flags": {"idp:provisioned": False},
+                "teamRole": "contributor",
+            },
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]

+ 19 - 0
src/sentry/apidocs/parameters.py

@@ -363,6 +363,25 @@ class TeamParams:
         type=str,
         description="""
 Specify `"0"` to return team details that do not include projects.
+""",
+    )
+    COLLAPSE = OpenApiParameter(
+        name="collapse",
+        location="query",
+        required=False,
+        type=str,
+        description="""
+List of strings to opt out of certain pieces of data. Supports `organization`.
+""",
+    )
+
+    EXPAND = OpenApiParameter(
+        name="expand",
+        location="query",
+        required=False,
+        type=str,
+        description="""
+List of strings to opt in to additional data. Supports `projects`, `externalTeams`.
 """,
     )
 

+ 2 - 1
src/sentry/scim/endpoints/teams.py

@@ -15,7 +15,8 @@ from sentry import audit_log
 from sentry.api.api_publish_status import ApiPublishStatus
 from sentry.api.base import region_silo_endpoint
 from sentry.api.endpoints.organization_teams import CONFLICTING_SLUG_ERROR, TeamPostSerializer
-from sentry.api.endpoints.team_details import TeamDetailsEndpoint, TeamSerializer
+from sentry.api.endpoints.team_details import TeamDetailsEndpoint
+from sentry.api.endpoints.team_details import TeamDetailsSerializer as TeamSerializer
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.paginator import GenericOffsetPaginator
 from sentry.api.serializers import serialize