diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py index d8571ff0c..66011296e 100644 --- a/apiserver/plane/app/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -1,13 +1,34 @@ from django.urls import path - from plane.app.views import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + WorkspaceEstimateEndpoint, ) urlpatterns = [ + path( + "workspaces//estimates/", + WorkspaceEstimateEndpoint.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace-estimate-points", + ), + path( + "workspaces//estimates//", + WorkspaceEstimateEndpoint.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="workspace-estimate-points", + ), path( "workspaces//projects//project-estimates/", ProjectEstimatePointEndpoint.as_view(), diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index c122dce9f..a90669e6e 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -141,6 +141,7 @@ from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndp from .estimate import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + WorkspaceEstimateEndpoint, ) from .inbox import InboxViewSet, InboxIssueViewSet diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate.py index ec9393f5b..864351218 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate.py @@ -4,13 +4,158 @@ from rest_framework import status # Module imports from .base import BaseViewSet, BaseAPIView -from plane.app.permissions import ProjectEntityPermission -from plane.db.models import Project, Estimate, EstimatePoint +from plane.app.permissions import ProjectEntityPermission, WorkspaceEntityPermission +from plane.db.models import Project, Estimate, EstimatePoint, Workspace from plane.app.serializers import ( EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer, ) +from django.db.models import Q + +class WorkspaceEstimateEndpoint(BaseViewSet): + permission_classes = [ + WorkspaceEntityPermission, + ] + model = Estimate + serializer_class = EstimateSerializer + + def list(self, request, slug): + estimates = Estimate.objects.filter( + workspace__slug=slug, project_id__isnull=True + ).prefetch_related("points").select_related("workspace") + serializer = EstimateReadSerializer(estimates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def create(self, request, slug): + if not request.data.get("estimate", False): + return Response( + {"error": "Estimate is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + estimate_points = request.data.get("estimate_points", []) + + if not len(estimate_points) or len(estimate_points) > 8: + return Response( + {"error": "Estimate points are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + estimate_serializer = EstimateSerializer(data=request.data.get("estimate")) + if not estimate_serializer.is_valid(): + return Response( + estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + workspace = Workspace.objects.get(slug=slug) + estimate = estimate_serializer.save(workspace_id=workspace.id) + estimate_points = EstimatePoint.objects.bulk_create( + [ + EstimatePoint( + estimate=estimate, + key=estimate_point.get("key", 0), + value=estimate_point.get("value", ""), + description=estimate_point.get("description", ""), + workspace_id=estimate.workspace_id, + created_by=request.user, + updated_by=request.user, + ) + for estimate_point in estimate_points + ], + batch_size=10, + ignore_conflicts=True, + ) + + estimate_point_serializer = EstimatePointSerializer( + estimate_points, many=True + ) + + return Response( + { + "estimate": estimate_serializer.data, + "estimate_points": estimate_point_serializer.data, + }, + status=status.HTTP_200_OK, + ) + + def retrieve(self, request, slug, estimate_id): + estimate = Estimate.objects.get( + pk=estimate_id, workspace__slug=slug + ) + serializer = EstimateReadSerializer(estimate) + return Response( + serializer.data, + status=status.HTTP_200_OK, + ) + + def partial_update(self, request, slug, estimate_id): + if not request.data.get("estimate", False): + return Response( + {"error": "Estimate is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if not len(request.data.get("estimate_points", [])): + return Response( + {"error": "Estimate points are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + estimate = Estimate.objects.get(pk=estimate_id) + + estimate_serializer = EstimateSerializer( + estimate, data=request.data.get("estimate"), partial=True + ) + if not estimate_serializer.is_valid(): + return Response( + estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + estimate = estimate_serializer.save() + + estimate_points_data = request.data.get("estimate_points", []) + + estimate_points = EstimatePoint.objects.filter( + pk__in=[ + estimate_point.get("id") for estimate_point in estimate_points_data + ], + workspace__slug=slug, + estimate_id=estimate_id, + ) + + updated_estimate_points = [] + for estimate_point in estimate_points: + # Find the data for that estimate point + estimate_point_data = [ + point + for point in estimate_points_data + if point.get("id") == str(estimate_point.id) + ] + if len(estimate_point_data): + estimate_point.value = estimate_point_data[0].get( + "value", estimate_point.value + ) + updated_estimate_points.append(estimate_point) + + EstimatePoint.objects.bulk_update( + updated_estimate_points, ["value"], batch_size=10, + ) + + estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True) + return Response( + { + "estimate": estimate_serializer.data, + "estimate_points": estimate_point_serializer.data, + }, + status=status.HTTP_200_OK, + ) + + def destroy(self, request, slug, estimate_id): + estimate = Estimate.objects.get( + pk=estimate_id, workspace__slug=slug + ) + estimate.delete() + return Response(status=status.HTTP_204_NO_CONTENT) class ProjectEstimatePointEndpoint(BaseAPIView): @@ -39,6 +184,14 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer_class = EstimateSerializer def list(self, request, slug, project_id): + workspace = request.GET.get("workspace", False) + if workspace: + estimates = Estimate.objects.filter( + Q(project_id=project_id) | Q(project_id__isnull=True), workspace__slug=slug, + ).prefetch_related("points").select_related("workspace") + serializer = EstimateReadSerializer(estimates, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + estimates = Estimate.objects.filter( workspace__slug=slug, project_id=project_id ).prefetch_related("points").select_related("workspace", "project") diff --git a/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py b/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py index e51ca1230..d64b986ad 100644 --- a/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py +++ b/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py @@ -1,7 +1,7 @@ # Generated by Django 4.2.7 on 2023-12-20 14:33 from django.db import migrations, models - +import django.db.models.deletion class Migration(migrations.Migration): @@ -10,69 +10,14 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name='cycle', - name='external_id', - field=models.CharField(blank=True, max_length=255, null=True), + migrations.AlterField( + model_name='estimate', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), ), - migrations.AddField( - model_name='cycle', - name='external_source', - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name='importer', - name='reason', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='issue', - name='external_id', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='issue', - name='external_source', - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name='issuecomment', - name='external_id', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='issuecomment', - name='external_source', - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name='label', - name='external_id', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='label', - name='external_source', - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name='module', - name='external_id', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='module', - name='external_source', - field=models.CharField(blank=True, null=True), - ), - migrations.AddField( - model_name='state', - name='external_id', - field=models.CharField(blank=True, max_length=255, null=True), - ), - migrations.AddField( - model_name='state', - name='external_source', - field=models.CharField(blank=True, null=True), + migrations.AlterField( + model_name='estimatepoint', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), ), ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index c76df6e5b..671874e87 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -9,6 +9,7 @@ from .workspace import ( WorkspaceMemberInvite, TeamMember, WorkspaceTheme, + WorkspaceBaseModel, ) from .project import ( diff --git a/apiserver/plane/db/models/estimate.py b/apiserver/plane/db/models/estimate.py index d95a86316..f159aa14e 100644 --- a/apiserver/plane/db/models/estimate.py +++ b/apiserver/plane/db/models/estimate.py @@ -3,10 +3,10 @@ from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator # Module imports -from . import ProjectBaseModel +from . import WorkspaceBaseModel -class Estimate(ProjectBaseModel): +class Estimate(WorkspaceBaseModel): name = models.CharField(max_length=255) description = models.TextField(verbose_name="Estimate Description", blank=True) @@ -22,7 +22,7 @@ class Estimate(ProjectBaseModel): ordering = ("name",) -class EstimatePoint(ProjectBaseModel): +class EstimatePoint(WorkspaceBaseModel): estimate = models.ForeignKey( "db.Estimate", on_delete=models.CASCADE, diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 505bfbcfa..0aaa1155b 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -103,6 +103,23 @@ class Workspace(BaseModel): ordering = ("-created_at",) +class WorkspaceBaseModel(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", models.CASCADE, related_name="workspace_%(class)s" + ) + project = models.ForeignKey( + "db.Project", models.CASCADE, related_name="project_%(class)s", null=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if self.project: + self.workspace = self.project.workspace + super(WorkspaceBaseModel, self).save(*args, **kwargs) + + class WorkspaceMember(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"