From 5a91031243612f933c7bb6148b84383401ec806f Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:10:22 +0530 Subject: [PATCH] feat: issue drafts (#2161) --- apiserver/plane/api/urls.py | 22 +++ apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/issue.py | 154 ++++++++++++++++++ ..._alter_analyticview_created_by_and_more.py | 19 +++ apiserver/plane/db/models/issue.py | 2 + 5 files changed, 198 insertions(+) create mode 100644 apiserver/plane/db/migrations/0043_issue_is_draft_alter_analyticview_created_by_and_more.py diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 558b7f059..a992b3312 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -91,6 +91,7 @@ from plane.api.views import ( IssueCommentPublicViewSet, IssueReactionViewSet, CommentReactionViewSet, + IssueDraftViewSet, ## End Issues # States StateViewSet, @@ -1010,6 +1011,27 @@ urlpatterns = [ name="project-issue-archive", ), ## End Issue Archives + ## Issue Drafts + path( + "workspaces//projects//issue-drafts/", + IssueDraftViewSet.as_view( + { + "get": "list", + } + ), + name="project-issue-draft", + ), + path( + "workspaces//projects//issue-drafts//", + IssueDraftViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="project-issue-draft", + ), + ## End Issue Drafts ## File Assets path( "workspaces//file-assets/", diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 71647bfea..67691d01e 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -88,6 +88,7 @@ from .issue import ( IssueVotePublicViewSet, IssueRetrievePublicEndpoint, ProjectIssuesPublicEndpoint, + IssueDraftViewSet, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index cf4fa46d4..6f82a368e 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -2240,3 +2240,157 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IssueDraftViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(is_draft=True) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + ) + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + try: + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + self.get_queryset() + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssueLiteSerializer(issue_queryset, many=True).data + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + return Response( + group_results(issues, group_by), status=status.HTTP_200_OK + ) + + return Response(issues, status=status.HTTP_200_OK) + + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + + def retrieve(self, request, slug, project_id, pk=None): + try: + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True + ) + return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + except Issue.DoesNotExist: + return Response( + {"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + diff --git a/apiserver/plane/db/migrations/0043_issue_is_draft_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0043_issue_is_draft_alter_analyticview_created_by_and_more.py new file mode 100644 index 000000000..7589f3d95 --- /dev/null +++ b/apiserver/plane/db/migrations/0043_issue_is_draft_alter_analyticview_created_by_and_more.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.3 on 2023-09-12 12:33 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0042_alter_analyticview_created_by_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='is_draft', + field=models.BooleanField(default=False), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index dd16cd963..e0cc01aab 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -29,6 +29,7 @@ class IssueManager(models.Manager): | models.Q(issue_inbox__isnull=True) ) .exclude(archived_at__isnull=False) + .exclude(is_draft=True) ) @@ -83,6 +84,7 @@ class Issue(ProjectBaseModel): sort_order = models.FloatField(default=65535) completed_at = models.DateTimeField(null=True) archived_at = models.DateField(null=True) + is_draft = models.BooleanField(default=False) objects = models.Manager() issue_objects = IssueManager()