Browse Source

docs(api): Update create project api doc (#50454)

The slug and name query parameter descriptions will be changed later.

Before:
![Screenshot 2023-06-08 at 2 34 34
PM](https://github.com/getsentry/sentry/assets/67301797/62388057-e671-46a9-9a76-4a84308515fc)

After: (the full response example can be viewed [here in a
gist.](https://gist.github.com/schew2381/944ebe1cd385d3e2dec3c1f7b8d9d7bb))
![Screenshot 2023-06-08 at 2 34 06
PM](https://github.com/getsentry/sentry/assets/67301797/5882fd9d-67ae-4de2-9553-7b7c347b4635)
Seiji Chew 1 year ago
parent
commit
1a3443e1f7

+ 0 - 109
api-docs/paths/teams/projects.json

@@ -89,114 +89,5 @@
         "auth_token": ["project:read"]
       }
     ]
-  },
-  "post": {
-    "tags": ["Teams"],
-    "description": "Create a new project bound to a team.",
-    "operationId": "Create a New Project",
-    "parameters": [
-      {
-        "name": "organization_slug",
-        "in": "path",
-        "description": "The slug of the organization the team belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "team_slug",
-        "in": "path",
-        "description": "The slug of the team to create a new project for.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "requestBody": {
-      "content": {
-        "application/json": {
-          "schema": {
-            "required": ["name"],
-            "type": "object",
-            "properties": {
-              "name": {
-                "type": "string",
-                "description": "The name for the new project."
-              },
-              "slug": {
-                "type": "string",
-                "description": "Optional slug for the new project. If not provided a slug is generated from the name."
-              }
-            }
-          },
-          "example": {
-            "name": "The Spoiled Yoghurt",
-            "slug": "the-spoiled-yoghurt"
-          }
-        }
-      },
-      "required": true
-    },
-    "responses": {
-      "201": {
-        "description": "Created",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/project.json#/Project"
-            },
-            "example": {
-              "status": "active",
-              "name": "The Spoiled Yoghurt",
-              "color": "#bf6e3f",
-              "isInternal": false,
-              "isPublic": false,
-              "slug": "the-spoiled-yoghurt",
-              "platform": null,
-              "hasAccess": true,
-              "firstEvent": null,
-              "avatar": {
-                "avatarUuid": null,
-                "avatarType": "letter_avatar"
-              },
-              "isMember": false,
-              "dateCreated": "2020-08-20T14:36:34.171255Z",
-              "isBookmarked": false,
-              "id": "5398494",
-              "features": [
-                "custom-inbound-filters",
-                "discard-groups",
-                "rate-limits",
-                "data-forwarding",
-                "similarity-view",
-                "issue-alerts-targeting",
-                "servicehooks",
-                "minidump",
-                "similarity-indexing"
-              ]
-            }
-          }
-        }
-      },
-      "400": {
-        "description": "Bad input"
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "404": {
-        "description": "Team not found"
-      },
-      "409": {
-        "description": "A project with the given slug already exists"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["project:write"]
-      }
-    ]
   }
 }

+ 1 - 1
api-docs/watch.ts

@@ -20,7 +20,7 @@ const makeApiDocsCommand = function () {
   }
   console.log('rebuilding OpenAPI schema...');
   isCurrentlyRunning = true;
-  const buildCommand = spawn('make', ['build-api-docs']);
+  const buildCommand = spawn('make', ['-C', '../', 'build-api-docs']);
 
   buildCommand.stdout.on('data', function (data) {
     stdout.write(data.toString());

+ 2 - 2
src/sentry/api/endpoints/organization_projects_experiment.py

@@ -13,7 +13,7 @@ from rest_framework.serializers import ValidationError
 from sentry import audit_log, features
 from sentry.api.base import region_silo_endpoint
 from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
-from sentry.api.endpoints.team_projects import ProjectSerializer
+from sentry.api.endpoints.team_projects import ProjectPostSerializer
 from sentry.api.exceptions import ConflictError, ResourceDoesNotExist
 from sentry.api.serializers import serialize
 from sentry.experiments import manager as expt_manager
@@ -73,7 +73,7 @@ class OrganizationProjectsExperimentEndpoint(OrganizationEndpoint):
         :param bool default_rules: create default rules (defaults to True)
         :auth: required
         """
-        serializer = ProjectSerializer(data=request.data)
+        serializer = ProjectPostSerializer(data=request.data)
 
         if not serializer.is_valid():
             raise ValidationError(serializer.errors)

+ 34 - 15
src/sentry/api/endpoints/team_projects.py

@@ -1,4 +1,5 @@
 from django.db import IntegrityError, transaction
+from drf_spectacular.utils import OpenApiResponse, extend_schema
 from rest_framework import serializers, status
 from rest_framework.request import Request
 from rest_framework.response import Response
@@ -8,6 +9,12 @@ from sentry.api.base import EnvironmentMixin, region_silo_endpoint
 from sentry.api.bases.team import TeamEndpoint, TeamPermission
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.serializers import ProjectSummarySerializer, serialize
+from sentry.api.serializers.models.project import (
+    ProjectSerializer as SentryProjectResponseSerializer,
+)
+from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN
+from sentry.apidocs.examples.project_examples import ProjectExamples
+from sentry.apidocs.parameters import GLOBAL_PARAMS, PROJECT_PARAMS
 from sentry.constants import ObjectStatus
 from sentry.models import Project
 from sentry.signals import project_created
@@ -16,7 +23,7 @@ from sentry.utils.snowflake import MaxSnowflakeRetryError
 ERR_INVALID_STATS_PERIOD = "Invalid stats_period. Valid choices are '', '24h', '14d', and '30d'"
 
 
-class ProjectSerializer(serializers.Serializer):
+class ProjectPostSerializer(serializers.Serializer):
     name = serializers.CharField(max_length=50, required=True)
     slug = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50, required=False, allow_null=True)
     platform = serializers.CharField(required=False, allow_blank=True, allow_null=True)
@@ -47,6 +54,7 @@ class TeamProjectPermission(TeamPermission):
 
 @region_silo_endpoint
 class TeamProjectsEndpoint(TeamEndpoint, EnvironmentMixin):
+    public = {"POST"}
     permission_classes = (TeamProjectPermission,)
 
     def get(self, request: Request, team) -> Response:
@@ -93,24 +101,35 @@ class TeamProjectsEndpoint(TeamEndpoint, EnvironmentMixin):
             paginator_cls=OffsetPaginator,
         )
 
+    @extend_schema(
+        # Ensure POST is in the projects tab
+        tags=["Projects"],
+        operation_id="Create a New Project",
+        parameters=[
+            GLOBAL_PARAMS.ORG_SLUG,
+            GLOBAL_PARAMS.TEAM_SLUG,
+            GLOBAL_PARAMS.name("The name of the project.", required=True),
+            GLOBAL_PARAMS.slug(
+                "Optional slug for the project. If not provided a slug is generated from the name."
+            ),
+            PROJECT_PARAMS.platform("The platform for the project."),
+            PROJECT_PARAMS.DEFAULT_RULES,
+        ],
+        request=ProjectPostSerializer,
+        responses={
+            201: SentryProjectResponseSerializer,
+            400: RESPONSE_BAD_REQUEST,
+            403: RESPONSE_FORBIDDEN,
+            404: OpenApiResponse(description="Team not found."),
+            409: OpenApiResponse(description="A project with this slug already exists."),
+        },
+        examples=ProjectExamples.CREATE_PROJECT,
+    )
     def post(self, request: Request, team) -> Response:
         """
-        Create a New Project
-        ````````````````````
-
         Create a new project bound to a team.
-
-        :pparam string organization_slug: the slug of the organization the
-                                          team belongs to.
-        :pparam string team_slug: the slug of the team to create a new project
-                                  for.
-        :param string name: the name for the new project.
-        :param string slug: optionally a slug for the new project.  If it's
-                            not provided a slug is generated from the name.
-        :param bool default_rules: create default rules (defaults to True)
-        :auth: required
         """
-        serializer = ProjectSerializer(data=request.data)
+        serializer = ProjectPostSerializer(data=request.data)
 
         if not serializer.is_valid():
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

+ 0 - 0
src/sentry/apidocs/examples/__init__.py


+ 63 - 0
src/sentry/apidocs/examples/project_examples.py

@@ -0,0 +1,63 @@
+from drf_spectacular.utils import OpenApiExample
+
+
+class ProjectExamples:
+    CREATE_PROJECT = [
+        OpenApiExample(
+            "Project successfully created",
+            {
+                "id": "4505321021243392",
+                "slug": "the-spoiled-yoghurt",
+                "name": "The Spoiled Yoghurt",
+                "platform": "python",
+                "dateCreated": "2023-06-08T00:13:06.004534Z",
+                "isBookmarked": False,
+                "isMember": True,
+                "features": [
+                    "alert-filters",
+                    "custom-inbound-filters",
+                    "data-forwarding",
+                    "discard-groups",
+                    "minidump",
+                    "race-free-group-creation",
+                    "rate-limits",
+                    "servicehooks",
+                    "similarity-indexing",
+                    "similarity-indexing-v2",
+                    "similarity-view",
+                    "similarity-view-v2",
+                ],
+                "firstEvent": None,
+                "firstTransactionEvent": False,
+                "access": [
+                    "member:read",
+                    "event:read",
+                    "project:admin",
+                    "team:write",
+                    "project:write",
+                    "team:admin",
+                    "project:read",
+                    "org:integrations",
+                    "org:read",
+                    "project:releases",
+                    "team:read",
+                    "alerts:write",
+                    "event:admin",
+                    "event:write",
+                    "alerts:read",
+                ],
+                "hasAccess": True,
+                "hasMinifiedStackTrace": False,
+                "hasMonitors": False,
+                "hasProfiles": False,
+                "hasReplays": False,
+                "hasSessions": False,
+                "isInternal": False,
+                "isPublic": False,
+                "avatar": {"avatarType": "letter_avatar", "avatarUuid": None},
+                "color": "#3f70bf",
+                "status": "active",
+            },
+            status_codes=["201"],
+        ),
+    ]

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

@@ -70,6 +70,26 @@ For example `24h`, to mean query data starting from 24 hours ago to now.""",
         description="The name of environments to filter by.",
     )
 
+    @staticmethod
+    def name(description: str, required: bool = False) -> OpenApiParameter:
+        return OpenApiParameter(
+            name="name",
+            location="query",
+            required=required,
+            type=str,
+            description=description,
+        )
+
+    @staticmethod
+    def slug(description: str, required: bool = False) -> OpenApiParameter:
+        return OpenApiParameter(
+            name="slug",
+            location="query",
+            required=required,
+            type=str,
+            description=description,
+        )
+
 
 class SCIM_PARAMS:
     MEMBER_ID = OpenApiParameter(
@@ -191,3 +211,23 @@ class EVENT_PARAMS:
         type=int,
         description="Index of the exception that should be used for source map resolution.",
     )
+
+
+class PROJECT_PARAMS:
+    DEFAULT_RULES = OpenApiParameter(
+        name="default_rules",
+        location="query",
+        required=False,
+        type=bool,
+        description="Defaults to true where the behavior is to alert the user on every new issue. Setting this to false will turn this off and the user must create their own alerts to be notified of new issues.",
+    )
+
+    @staticmethod
+    def platform(description: str) -> OpenApiParameter:
+        return OpenApiParameter(
+            name="platform",
+            location="query",
+            required=False,
+            type=str,
+            description=description,
+        )

+ 0 - 7
tests/apidocs/endpoints/teams/test_projects.py

@@ -21,10 +21,3 @@ class TeamsProjectsDocs(APIDocsTestCase):
         request = RequestFactory().get(self.url)
 
         self.validate_schema(request, response)
-
-    def test_post(self):
-        data = {"name": "foo"}
-        response = self.client.post(self.url, data)
-        request = RequestFactory().post(self.url, data)
-
-        self.validate_schema(request, response)