[SILO-1026] feat: add estimates external API endpoints (#8664)
* add project summary endpoint * update response structure * add estimates external API endpoints with migrations * fix invalid project and workspace error
This commit is contained in:
parent
d7c80885fd
commit
9fa707b260
10 changed files with 504 additions and 11 deletions
|
|
@ -53,7 +53,7 @@ from .intake import (
|
||||||
IntakeIssueCreateSerializer,
|
IntakeIssueCreateSerializer,
|
||||||
IntakeIssueUpdateSerializer,
|
IntakeIssueUpdateSerializer,
|
||||||
)
|
)
|
||||||
from .estimate import EstimatePointSerializer
|
from .estimate import EstimateSerializer, EstimatePointSerializer
|
||||||
from .asset import (
|
from .asset import (
|
||||||
UserAssetUploadSerializer,
|
UserAssetUploadSerializer,
|
||||||
AssetUpdateSerializer,
|
AssetUpdateSerializer,
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,36 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
# See the LICENSE file for details.
|
# See the LICENSE file for details.
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import EstimatePoint
|
from plane.db.models import Estimate, EstimatePoint
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class EstimatePointSerializer(BaseSerializer):
|
class EstimateSerializer(BaseSerializer):
|
||||||
"""
|
class Meta:
|
||||||
Serializer for project estimation points and story point values.
|
model = Estimate
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = ["workspace", "project", "deleted_at"]
|
||||||
|
|
||||||
Handles numeric estimation data for work item sizing and sprint planning,
|
def create(self, validated_data):
|
||||||
providing standardized point values for project velocity calculations.
|
validated_data["workspace"] = self.context["workspace"]
|
||||||
"""
|
validated_data["project"] = self.context["project"]
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class EstimatePointSerializer(BaseSerializer):
|
||||||
|
def validate(self, data):
|
||||||
|
if not data:
|
||||||
|
raise serializers.ValidationError("Estimate points are required")
|
||||||
|
value = data.get("value")
|
||||||
|
if value and len(value) > 20:
|
||||||
|
raise serializers.ValidationError("Value can't be more than 20 characters")
|
||||||
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EstimatePoint
|
model = EstimatePoint
|
||||||
fields = ["id", "value"]
|
fields = "__all__"
|
||||||
read_only_fields = fields
|
read_only_fields = ["estimate", "workspace", "project"]
|
||||||
|
|
|
||||||
29
apps/api/plane/api/urls/estimate.py
Normal file
29
apps/api/plane/api/urls/estimate.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# See the LICENSE file for details.
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.api.views.estimate import (
|
||||||
|
ProjectEstimateAPIEndpoint,
|
||||||
|
EstimatePointListCreateAPIEndpoint,
|
||||||
|
EstimatePointDetailAPIEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
|
ProjectEstimateAPIEndpoint.as_view(http_method_names=["get", "post", "patch", "delete"]),
|
||||||
|
name="project-estimate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
|
||||||
|
EstimatePointListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||||
|
name="estimate-point-list-create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<uuid:estimate_point_id>/",
|
||||||
|
EstimatePointDetailAPIEndpoint.as_view(http_method_names=["patch", "delete"]),
|
||||||
|
name="estimate-point-detail",
|
||||||
|
),
|
||||||
|
]
|
||||||
291
apps/api/plane/api/views/estimate.py
Normal file
291
apps/api/plane/api/views/estimate.py
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# See the LICENSE file for details.
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from drf_spectacular.utils import OpenApiRequest, OpenApiResponse
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.permissions.project import ProjectEntityPermission
|
||||||
|
from plane.api.views.base import BaseAPIView
|
||||||
|
from plane.db.models import Estimate, EstimatePoint, Project, Workspace
|
||||||
|
from plane.api.serializers import EstimateSerializer, EstimatePointSerializer
|
||||||
|
from plane.utils.openapi.decorators import estimate_docs, estimate_point_docs
|
||||||
|
from plane.utils.openapi import (
|
||||||
|
ESTIMATE_CREATE_EXAMPLE,
|
||||||
|
ESTIMATE_UPDATE_EXAMPLE,
|
||||||
|
ESTIMATE_POINT_CREATE_EXAMPLE,
|
||||||
|
ESTIMATE_POINT_UPDATE_EXAMPLE,
|
||||||
|
ESTIMATE_EXAMPLE,
|
||||||
|
ESTIMATE_POINT_EXAMPLE,
|
||||||
|
DELETED_RESPONSE,
|
||||||
|
WORKSPACE_SLUG_PARAMETER,
|
||||||
|
PROJECT_ID_PARAMETER,
|
||||||
|
ESTIMATE_ID_PARAMETER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectEstimateAPIEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [ProjectEntityPermission]
|
||||||
|
model = Estimate
|
||||||
|
serializer_class = EstimateSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(workspace__slug=self.workspace_slug, project_id=self.project_id)
|
||||||
|
|
||||||
|
@estimate_docs(
|
||||||
|
operation_id="create_estimate",
|
||||||
|
summary="Create an estimate",
|
||||||
|
description="Create an estimate for a project",
|
||||||
|
request=OpenApiRequest(
|
||||||
|
request=EstimateSerializer,
|
||||||
|
examples=[ESTIMATE_CREATE_EXAMPLE],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def post(self, request, slug, project_id):
|
||||||
|
project = Project.objects.filter(id=project_id, workspace__slug=slug).first()
|
||||||
|
if not project:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})
|
||||||
|
|
||||||
|
workspace = Workspace.objects.filter(slug=slug).first()
|
||||||
|
if not workspace:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})
|
||||||
|
|
||||||
|
project_estimate = self.get_queryset().first()
|
||||||
|
if project_estimate:
|
||||||
|
# return 409 if the project estimate already exists
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_409_CONFLICT,
|
||||||
|
data={"error": "An estimate already exists for this project", "id": str(project_estimate.id)},
|
||||||
|
)
|
||||||
|
# create the project estimate
|
||||||
|
serializer = self.serializer_class(data=request.data, context={"workspace": workspace, "project": project})
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@estimate_docs(
|
||||||
|
operation_id="get_estimate",
|
||||||
|
summary="Get an estimate",
|
||||||
|
description="Get an estimate for a project",
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
description="Estimate",
|
||||||
|
response=EstimateSerializer,
|
||||||
|
examples=[ESTIMATE_EXAMPLE],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
estimate = self.get_queryset().first()
|
||||||
|
if not estimate:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||||
|
serializer = self.serializer_class(estimate)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@estimate_docs(
|
||||||
|
operation_id="update_estimate",
|
||||||
|
summary="Update an estimate",
|
||||||
|
description="Update an estimate for a project",
|
||||||
|
request=OpenApiRequest(
|
||||||
|
request=EstimateSerializer,
|
||||||
|
examples=[ESTIMATE_UPDATE_EXAMPLE],
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
description="Estimate",
|
||||||
|
response=EstimateSerializer,
|
||||||
|
examples=[ESTIMATE_EXAMPLE],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def patch(self, request, slug, project_id):
|
||||||
|
ALLOWED_FIELDS = ["name", "description"]
|
||||||
|
estimate = self.get_queryset().first()
|
||||||
|
if not estimate:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||||
|
filtered_data = {k: v for k, v in request.data.items() if k in ALLOWED_FIELDS}
|
||||||
|
if not filtered_data:
|
||||||
|
serializer = self.serializer_class(estimate)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
serializer = self.serializer_class(estimate, data=filtered_data, partial=True)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@estimate_docs(
|
||||||
|
operation_id="delete_estimate",
|
||||||
|
summary="Delete an estimate",
|
||||||
|
description="Delete an estimate for a project",
|
||||||
|
responses={
|
||||||
|
204: DELETED_RESPONSE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def delete(self, request, slug, project_id):
|
||||||
|
estimate = self.get_queryset().first()
|
||||||
|
if not estimate:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||||
|
estimate.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class EstimatePointListCreateAPIEndpoint(BaseAPIView):
|
||||||
|
"""List and bulk create estimate points for an estimate."""
|
||||||
|
|
||||||
|
permission_classes = [ProjectEntityPermission]
|
||||||
|
model = EstimatePoint
|
||||||
|
serializer_class = EstimatePointSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(
|
||||||
|
estimate_id=self.kwargs["estimate_id"],
|
||||||
|
workspace__slug=self.kwargs["slug"],
|
||||||
|
project_id=self.kwargs["project_id"],
|
||||||
|
).select_related("estimate", "workspace", "project")
|
||||||
|
|
||||||
|
@estimate_point_docs(
|
||||||
|
operation_id="get_estimate_points",
|
||||||
|
summary="Get estimate points",
|
||||||
|
description="Get estimate points for an estimate",
|
||||||
|
parameters=[
|
||||||
|
WORKSPACE_SLUG_PARAMETER,
|
||||||
|
PROJECT_ID_PARAMETER,
|
||||||
|
ESTIMATE_ID_PARAMETER,
|
||||||
|
],
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
description="Estimate points",
|
||||||
|
response=EstimatePointSerializer(many=True),
|
||||||
|
examples=[ESTIMATE_POINT_EXAMPLE],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request, slug, project_id, estimate_id):
|
||||||
|
estimate = Estimate.objects.filter(
|
||||||
|
id=estimate_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
).first()
|
||||||
|
if not estimate:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||||
|
estimate_points = self.get_queryset()
|
||||||
|
serializer = self.serializer_class(estimate_points, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@estimate_point_docs(
|
||||||
|
operation_id="create_estimate_points",
|
||||||
|
summary="Create estimate points",
|
||||||
|
description="Create estimate points for an estimate",
|
||||||
|
request=OpenApiRequest(
|
||||||
|
request=EstimatePointSerializer,
|
||||||
|
examples=[ESTIMATE_POINT_CREATE_EXAMPLE],
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
201: OpenApiResponse(
|
||||||
|
description="Estimate points",
|
||||||
|
response=EstimatePointSerializer(many=True),
|
||||||
|
examples=[ESTIMATE_POINT_EXAMPLE],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def post(self, request, slug, project_id, estimate_id):
|
||||||
|
estimate = Estimate.objects.filter(
|
||||||
|
id=estimate_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
).first()
|
||||||
|
if not estimate:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate not found"})
|
||||||
|
|
||||||
|
estimate_points_data = (
|
||||||
|
request.data if isinstance(request.data, list) else request.data.get("estimate_points", [])
|
||||||
|
)
|
||||||
|
if not estimate_points_data:
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
data={"error": "Estimate points are required"},
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = self.serializer_class(data=estimate_points_data, many=True)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
estimate_points = [
|
||||||
|
EstimatePoint(
|
||||||
|
estimate=estimate,
|
||||||
|
workspace=estimate.workspace,
|
||||||
|
project=estimate.project,
|
||||||
|
**item,
|
||||||
|
)
|
||||||
|
for item in serializer.validated_data
|
||||||
|
]
|
||||||
|
created = EstimatePoint.objects.bulk_create(estimate_points)
|
||||||
|
return Response(
|
||||||
|
self.serializer_class(created, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EstimatePointDetailAPIEndpoint(BaseAPIView):
|
||||||
|
"""Update and delete a single estimate point."""
|
||||||
|
|
||||||
|
permission_classes = [ProjectEntityPermission]
|
||||||
|
model = EstimatePoint
|
||||||
|
serializer_class = EstimatePointSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(
|
||||||
|
estimate_id=self.kwargs["estimate_id"],
|
||||||
|
workspace__slug=self.kwargs["slug"],
|
||||||
|
project_id=self.kwargs["project_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@estimate_point_docs(
|
||||||
|
operation_id="update_estimate_point",
|
||||||
|
summary="Update an estimate point",
|
||||||
|
description="Update an estimate point for an estimate",
|
||||||
|
request=OpenApiRequest(
|
||||||
|
request=EstimatePointSerializer,
|
||||||
|
examples=[ESTIMATE_POINT_UPDATE_EXAMPLE],
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(
|
||||||
|
description="Estimate point",
|
||||||
|
response=EstimatePointSerializer,
|
||||||
|
examples=[ESTIMATE_POINT_EXAMPLE],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def patch(self, request, slug, project_id, estimate_id, estimate_point_id):
|
||||||
|
estimate_point = self.get_queryset().filter(id=estimate_point_id).first()
|
||||||
|
if not estimate_point:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate point not found"})
|
||||||
|
ALLOWED_FIELDS = ["key", "value", "description"]
|
||||||
|
filtered_data = {k: v for k, v in request.data.items() if k in ALLOWED_FIELDS}
|
||||||
|
if not filtered_data:
|
||||||
|
return Response(self.serializer_class(estimate_point).data, status=status.HTTP_200_OK)
|
||||||
|
serializer = self.serializer_class(estimate_point, data=filtered_data, partial=True)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@estimate_point_docs(
|
||||||
|
operation_id="delete_estimate_point",
|
||||||
|
summary="Delete an estimate point",
|
||||||
|
description="Delete an estimate point for an estimate",
|
||||||
|
responses={
|
||||||
|
204: DELETED_RESPONSE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def delete(self, request, slug, project_id, estimate_id, estimate_point_id):
|
||||||
|
estimate_point = self.get_queryset().filter(id=estimate_point_id).first()
|
||||||
|
if not estimate_point:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Estimate point not found"})
|
||||||
|
estimate_point.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
18
apps/api/plane/db/migrations/0121_alter_estimate_type.py
Normal file
18
apps/api/plane/db/migrations/0121_alter_estimate_type.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.28 on 2026-02-26 14:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0120_issueview_archived_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='estimate',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('categories', 'Categories'), ('points', 'Points')], default='categories', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -10,11 +10,15 @@ from django.db.models import Q
|
||||||
# Module imports
|
# Module imports
|
||||||
from .project import ProjectBaseModel
|
from .project import ProjectBaseModel
|
||||||
|
|
||||||
|
class EstimateType(models.TextChoices):
|
||||||
|
CATEGORIES = "categories", "Categories"
|
||||||
|
POINTS = "points", "Points"
|
||||||
|
|
||||||
|
|
||||||
class Estimate(ProjectBaseModel):
|
class Estimate(ProjectBaseModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(verbose_name="Estimate Description", blank=True)
|
description = models.TextField(verbose_name="Estimate Description", blank=True)
|
||||||
type = models.CharField(max_length=255, default="categories")
|
type = models.CharField(max_length=255, choices=EstimateType.choices, default=EstimateType.CATEGORIES)
|
||||||
last_used = models.BooleanField(default=False)
|
last_used = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ from .parameters import (
|
||||||
CYCLE_VIEW_PARAMETER,
|
CYCLE_VIEW_PARAMETER,
|
||||||
FIELDS_PARAMETER,
|
FIELDS_PARAMETER,
|
||||||
EXPAND_PARAMETER,
|
EXPAND_PARAMETER,
|
||||||
|
ESTIMATE_ID_PARAMETER,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Responses
|
# Responses
|
||||||
|
|
@ -126,6 +127,10 @@ from .examples import (
|
||||||
STATE_UPDATE_EXAMPLE,
|
STATE_UPDATE_EXAMPLE,
|
||||||
INTAKE_ISSUE_CREATE_EXAMPLE,
|
INTAKE_ISSUE_CREATE_EXAMPLE,
|
||||||
INTAKE_ISSUE_UPDATE_EXAMPLE,
|
INTAKE_ISSUE_UPDATE_EXAMPLE,
|
||||||
|
ESTIMATE_CREATE_EXAMPLE,
|
||||||
|
ESTIMATE_UPDATE_EXAMPLE,
|
||||||
|
ESTIMATE_POINT_CREATE_EXAMPLE,
|
||||||
|
ESTIMATE_POINT_UPDATE_EXAMPLE,
|
||||||
# Response Examples
|
# Response Examples
|
||||||
CYCLE_EXAMPLE,
|
CYCLE_EXAMPLE,
|
||||||
TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE,
|
TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE,
|
||||||
|
|
@ -145,6 +150,8 @@ from .examples import (
|
||||||
PROJECT_MEMBER_EXAMPLE,
|
PROJECT_MEMBER_EXAMPLE,
|
||||||
CYCLE_ISSUE_EXAMPLE,
|
CYCLE_ISSUE_EXAMPLE,
|
||||||
STICKY_EXAMPLE,
|
STICKY_EXAMPLE,
|
||||||
|
ESTIMATE_EXAMPLE,
|
||||||
|
ESTIMATE_POINT_EXAMPLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Helper decorators
|
# Helper decorators
|
||||||
|
|
@ -166,6 +173,8 @@ from .decorators import (
|
||||||
module_docs,
|
module_docs,
|
||||||
module_issue_docs,
|
module_issue_docs,
|
||||||
state_docs,
|
state_docs,
|
||||||
|
estimate_docs,
|
||||||
|
estimate_point_docs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Schema processing hooks
|
# Schema processing hooks
|
||||||
|
|
@ -207,6 +216,7 @@ __all__ = [
|
||||||
"CYCLE_VIEW_PARAMETER",
|
"CYCLE_VIEW_PARAMETER",
|
||||||
"FIELDS_PARAMETER",
|
"FIELDS_PARAMETER",
|
||||||
"EXPAND_PARAMETER",
|
"EXPAND_PARAMETER",
|
||||||
|
"ESTIMATE_ID_PARAMETER",
|
||||||
# Responses
|
# Responses
|
||||||
"UNAUTHORIZED_RESPONSE",
|
"UNAUTHORIZED_RESPONSE",
|
||||||
"FORBIDDEN_RESPONSE",
|
"FORBIDDEN_RESPONSE",
|
||||||
|
|
@ -280,6 +290,10 @@ __all__ = [
|
||||||
"STATE_UPDATE_EXAMPLE",
|
"STATE_UPDATE_EXAMPLE",
|
||||||
"INTAKE_ISSUE_CREATE_EXAMPLE",
|
"INTAKE_ISSUE_CREATE_EXAMPLE",
|
||||||
"INTAKE_ISSUE_UPDATE_EXAMPLE",
|
"INTAKE_ISSUE_UPDATE_EXAMPLE",
|
||||||
|
"ESTIMATE_CREATE_EXAMPLE",
|
||||||
|
"ESTIMATE_UPDATE_EXAMPLE",
|
||||||
|
"ESTIMATE_POINT_CREATE_EXAMPLE",
|
||||||
|
"ESTIMATE_POINT_UPDATE_EXAMPLE",
|
||||||
# Response Examples
|
# Response Examples
|
||||||
"CYCLE_EXAMPLE",
|
"CYCLE_EXAMPLE",
|
||||||
"TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE",
|
"TRANSFER_CYCLE_ISSUE_SUCCESS_EXAMPLE",
|
||||||
|
|
@ -299,6 +313,8 @@ __all__ = [
|
||||||
"PROJECT_MEMBER_EXAMPLE",
|
"PROJECT_MEMBER_EXAMPLE",
|
||||||
"CYCLE_ISSUE_EXAMPLE",
|
"CYCLE_ISSUE_EXAMPLE",
|
||||||
"STICKY_EXAMPLE",
|
"STICKY_EXAMPLE",
|
||||||
|
"ESTIMATE_EXAMPLE",
|
||||||
|
"ESTIMATE_POINT_EXAMPLE",
|
||||||
# Decorators
|
# Decorators
|
||||||
"workspace_docs",
|
"workspace_docs",
|
||||||
"project_docs",
|
"project_docs",
|
||||||
|
|
@ -317,6 +333,8 @@ __all__ = [
|
||||||
"module_docs",
|
"module_docs",
|
||||||
"module_issue_docs",
|
"module_issue_docs",
|
||||||
"state_docs",
|
"state_docs",
|
||||||
|
"estimate_docs",
|
||||||
|
"estimate_point_docs",
|
||||||
# Hooks
|
# Hooks
|
||||||
"preprocess_filter_api_v1_paths",
|
"preprocess_filter_api_v1_paths",
|
||||||
"generate_operation_summary",
|
"generate_operation_summary",
|
||||||
|
|
|
||||||
|
|
@ -297,3 +297,29 @@ def sticky_docs(**kwargs):
|
||||||
}
|
}
|
||||||
|
|
||||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||||
|
|
||||||
|
def estimate_docs(**kwargs):
|
||||||
|
"""Decorator for estimate-related endpoints"""
|
||||||
|
defaults = {
|
||||||
|
"tags": ["Estimates"],
|
||||||
|
"parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||||
|
"responses": {
|
||||||
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
|
403: FORBIDDEN_RESPONSE,
|
||||||
|
404: NOT_FOUND_RESPONSE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||||
|
|
||||||
|
def estimate_point_docs(**kwargs):
|
||||||
|
"""Decorator for estimate point-related endpoints"""
|
||||||
|
defaults = {
|
||||||
|
"tags": ["Estimate Points"],
|
||||||
|
"parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||||
|
"responses": {
|
||||||
|
401: UNAUTHORIZED_RESPONSE,
|
||||||
|
403: FORBIDDEN_RESPONSE,
|
||||||
|
404: NOT_FOUND_RESPONSE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||||
|
|
@ -686,6 +686,69 @@ STICKY_EXAMPLE = OpenApiExample(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Estimate Examples
|
||||||
|
ESTIMATE_EXAMPLE = OpenApiExample(
|
||||||
|
name="Estimate",
|
||||||
|
value={
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"name": "Estimate 1",
|
||||||
|
"description": "Estimate 1 description",
|
||||||
|
},
|
||||||
|
description="Example response for an estimate",
|
||||||
|
)
|
||||||
|
|
||||||
|
ESTIMATE_POINT_EXAMPLE = OpenApiExample(
|
||||||
|
name="EstimatePoint",
|
||||||
|
value={
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"estimate": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"key": 1,
|
||||||
|
"value": "1",
|
||||||
|
},
|
||||||
|
description="Example response for an estimate point",
|
||||||
|
)
|
||||||
|
ESTIMATE_CREATE_EXAMPLE = OpenApiExample(
|
||||||
|
name="EstimateCreateSerializer",
|
||||||
|
value={
|
||||||
|
"name": "Estimate 1",
|
||||||
|
"description": "Estimate 1 description",
|
||||||
|
},
|
||||||
|
description="Example request for creating an estimate",
|
||||||
|
)
|
||||||
|
ESTIMATE_UPDATE_EXAMPLE = OpenApiExample(
|
||||||
|
name="EstimateUpdateSerializer",
|
||||||
|
value={
|
||||||
|
"name": "Estimate 1",
|
||||||
|
"description": "Estimate 1 description",
|
||||||
|
},
|
||||||
|
description="Example request for updating an estimate",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Estimate Point Examples
|
||||||
|
ESTIMATE_POINT_CREATE_EXAMPLE = OpenApiExample(
|
||||||
|
name="EstimatePointCreateSerializer",
|
||||||
|
value=[
|
||||||
|
{
|
||||||
|
"value": "1",
|
||||||
|
"description": "Estimate Point 1 description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "2",
|
||||||
|
"description": "Estimate Point 2 description",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
description="Example request for creating an estimate point",
|
||||||
|
)
|
||||||
|
ESTIMATE_POINT_UPDATE_EXAMPLE = OpenApiExample(
|
||||||
|
name="EstimatePointUpdateSerializer",
|
||||||
|
value={
|
||||||
|
"value": "1",
|
||||||
|
"description": "Estimate Point 1 description",
|
||||||
|
},
|
||||||
|
description="Example request for updating an estimate point",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Sample data for different entity types
|
# Sample data for different entity types
|
||||||
SAMPLE_ISSUE = {
|
SAMPLE_ISSUE = {
|
||||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
|
@ -801,6 +864,24 @@ SAMPLE_STICKY = {
|
||||||
"created_at": "2024-01-01T10:30:00Z",
|
"created_at": "2024-01-01T10:30:00Z",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SAMPLE_ESTIMATE = {
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"name": "Estimate 1",
|
||||||
|
"description": "Estimate 1 description",
|
||||||
|
"type": "categories",
|
||||||
|
"last_used": False,
|
||||||
|
"created_at": "2024-01-01T10:30:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
SAMPLE_ESTIMATE_POINT = {
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"estimate": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"key": 1,
|
||||||
|
"value": "1",
|
||||||
|
"description": "Estimate Point 1 description",
|
||||||
|
"created_at": "2024-01-01T10:30:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
# Mapping of schema types to sample data
|
# Mapping of schema types to sample data
|
||||||
SCHEMA_EXAMPLES = {
|
SCHEMA_EXAMPLES = {
|
||||||
"Issue": SAMPLE_ISSUE,
|
"Issue": SAMPLE_ISSUE,
|
||||||
|
|
@ -816,6 +897,8 @@ SCHEMA_EXAMPLES = {
|
||||||
"Intake": SAMPLE_INTAKE,
|
"Intake": SAMPLE_INTAKE,
|
||||||
"CycleIssue": SAMPLE_CYCLE_ISSUE,
|
"CycleIssue": SAMPLE_CYCLE_ISSUE,
|
||||||
"Sticky": SAMPLE_STICKY,
|
"Sticky": SAMPLE_STICKY,
|
||||||
|
"Estimate": SAMPLE_ESTIMATE,
|
||||||
|
"EstimatePoint": SAMPLE_ESTIMATE_POINT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -495,3 +495,11 @@ EXPAND_PARAMETER = OpenApiParameter(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ESTIMATE_ID_PARAMETER = OpenApiParameter(
|
||||||
|
name="estimate_id",
|
||||||
|
description="Estimate ID",
|
||||||
|
required=True,
|
||||||
|
type=OpenApiTypes.UUID,
|
||||||
|
location=OpenApiParameter.PATH,
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue