Files
upstream-plane/apps/api/plane/api/views/estimate.py
upstream-mirror 23059a1de3
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
2026-09-15 15:14:58 +05:00

292 lines
11 KiB
Python

# 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)