Skip to content

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

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

Merged
merged 18 commits into from
Jun 9, 2023
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
109 changes: 0 additions & 109 deletions api-docs/paths/teams/projects.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
}
}
2 changes: 1 addition & 1 deletion api-docs/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The make watch-api-docs command was broken before so this fixes it


buildCommand.stdout.on('data', function (data) {
stdout.write(data.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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
Expand Down Expand Up @@ -73,7 +73,7 @@ def post(self, request: Request, organization: Organization) -> Response:
: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)
Expand Down
49 changes: 34 additions & 15 deletions src/sentry/api/endpoints/team_projects.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,6 +9,12 @@
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
Expand All @@ -16,7 +23,7 @@
ERR_INVALID_STATS_PERIOD = "Invalid stats_period. Valid choices are '', '24h', '14d', and '30d'"


class ProjectSerializer(serializers.Serializer):
class ProjectPostSerializer(serializers.Serializer):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're required to change the name of this serializer b/c there is a duplicate ProjectSerializer in the projects model and drf-spectacular/openapi yells at me

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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -93,24 +101,35 @@ def get(self, request: Request, team) -> Response:
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)
Expand Down
Empty file.
63 changes: 63 additions & 0 deletions src/sentry/apidocs/examples/project_examples.py
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions src/sentry/apidocs/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ class GLOBAL_PARAMS:
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(
Expand Down Expand Up @@ -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,
)
7 changes: 0 additions & 7 deletions tests/apidocs/endpoints/teams/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,3 @@ def test_get(self):
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)
Comment on lines -24 to -30
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test can be safely deleted b/c typing is verified through drf/open-api