From 9d60aaddd787c3b4c468da7e9bf20a7de8b876e6 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 7 Mar 2024 15:11:44 +0530 Subject: [PATCH] dev: fix grouping on taget date and project_id --- apiserver/plane/app/views/cycle/base.py | 15 +- apiserver/plane/app/views/issue/archive.py | 252 ++++------ apiserver/plane/app/views/issue/base.py | 461 ++++++------------ apiserver/plane/app/views/issue/draft.py | 262 ++++------ apiserver/plane/app/views/module/base.py | 12 +- apiserver/plane/app/views/view/base.py | 1 + .../plane/bgtasks/project_invitation_task.py | 9 +- apiserver/plane/space/views/issue.py | 96 ++-- apiserver/plane/utils/cache.py | 15 +- apiserver/plane/utils/grouper.py | 40 +- apiserver/plane/utils/paginator.py | 9 +- 11 files changed, 457 insertions(+), 715 deletions(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index fa4bb0e87..1461ffb93 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -54,7 +54,11 @@ from plane.db.models import ( User, ) from plane.utils.analytics_plot import burndown_plot -from plane.utils.grouper import issue_on_results, issue_queryset_grouper +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator @@ -759,7 +763,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): # Group by group_by = request.GET.get("group_by", False) issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, field=group_by + queryset=issue_queryset, + field=group_by, ) # List Paginate @@ -781,6 +786,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ), paginator_cls=GroupedOffsetPaginator, group_by_field_name=group_by, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), count_filter=Q( Q(issue_inbox__status=1) | Q(issue_inbox__status=-1) diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index 540715a24..a6ae04f31 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -1,52 +1,61 @@ # Python imports import json -# Django imports -from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, - UUIDField, -) -from django.core.serializers.json import DjangoJSONEncoder -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField +from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import ( + Case, + CharField, + Exists, + F, + Func, + Max, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, + When, +) from django.db.models.functions import Coalesce +# Django imports +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from rest_framework import status + # Third Party imports from rest_framework.response import Response -from rest_framework import status -# Module imports -from .. import BaseViewSet -from plane.app.serializers import ( - IssueSerializer, - IssueFlatSerializer, - IssueDetailSerializer, -) from plane.app.permissions import ( ProjectEntityPermission, ) -from plane.db.models import ( - Issue, - IssueLink, - IssueAttachment, - IssueSubscriber, - IssueReaction, +from plane.app.serializers import ( + IssueDetailSerializer, + IssueFlatSerializer, + IssueSerializer, ) from plane.bgtasks.issue_activites_task import issue_activity +from plane.db.models import ( + Issue, + IssueAttachment, + IssueLink, + IssueReaction, + IssueSubscriber, +) +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) from plane.utils.issue_filters import issue_filters +from plane.utils.order_queryset import order_issue_queryset +from plane.utils.paginator import GroupedOffsetPaginator + +# Module imports +from .. import BaseViewSet class IssueArchiveViewSet(BaseViewSet): @@ -92,32 +101,6 @@ class IssueArchiveViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) ) @method_decorator(gzip_page) @@ -125,120 +108,63 @@ class IssueArchiveViewSet(BaseViewSet): filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") - # 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) - # 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) - issue_queryset = ( issue_queryset if show_sub_issues == "true" else issue_queryset.filter(parent__isnull=True) ) - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", + # Issue queryset + issue_queryset = order_issue_queryset( + issue_queryset=issue_queryset, + order_by_param=order_by_param, + ) + + # Group by + group_by = request.GET.get("group_by", False) + + issue_queryset = issue_queryset_grouper( + queryset=issue_queryset, + field=group_by, + ) + + # List Paginate + if not group_by: + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), ) - return Response(issues, status=status.HTTP_200_OK) + + # Group paginate + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), + paginator_cls=GroupedOffsetPaginator, + group_by_field_name=group_by, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + count_filter=Q( + Q(issue_inbox__status=1) + | Q(issue_inbox__status=-1) + | Q(issue_inbox__status=2) + | Q(issue_inbox__isnull=True), + archived_at__isnull=False, + is_draft=True, + ), + ) def retrieve(self, request, slug, project_id, pk=None): issue = ( diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 63d4358b0..d108cc551 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -1,84 +1,93 @@ # Python imports import json import random +from collections import defaultdict from itertools import chain +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.core.serializers.json import DjangoJSONEncoder +from django.db import IntegrityError +from django.db.models import ( + Case, + CharField, + Exists, + F, + Func, + Max, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, + When, +) +from django.db.models.functions import Coalesce + # Django imports from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, -) -from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.db import IntegrityError -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField -from django.db.models.functions import Coalesce +from rest_framework import status +from rest_framework.parsers import FormParser, MultiPartParser # Third Party imports from rest_framework.response import Response -from rest_framework import status -from rest_framework.parsers import MultiPartParser, FormParser -# Module imports -from .. import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, + ProjectMemberPermission, + WorkSpaceAdminPermission, +) from plane.app.serializers import ( + CommentReactionSerializer, IssueActivitySerializer, + IssueAttachmentSerializer, IssueCommentSerializer, - IssuePropertySerializer, - IssueSerializer, IssueCreateSerializer, - LabelSerializer, + IssueDetailSerializer, IssueFlatSerializer, IssueLinkSerializer, IssueLiteSerializer, - IssueAttachmentSerializer, - IssueSubscriberSerializer, - ProjectMemberLiteSerializer, + IssuePropertySerializer, IssueReactionSerializer, - CommentReactionSerializer, IssueRelationSerializer, + IssueSerializer, + IssueSubscriberSerializer, + LabelSerializer, + ProjectMemberLiteSerializer, RelatedIssueSerializer, - IssueDetailSerializer, -) -from plane.app.permissions import ( - ProjectEntityPermission, - WorkSpaceAdminPermission, - ProjectMemberPermission, - ProjectLitePermission, -) -from plane.db.models import ( - Project, - Issue, - IssueActivity, - IssueComment, - IssueProperty, - Label, - IssueLink, - IssueAttachment, - IssueSubscriber, - ProjectMember, - IssueReaction, - CommentReaction, - IssueRelation, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.grouper import group_results -from plane.utils.issue_filters import issue_filters -from collections import defaultdict +from plane.db.models import ( + CommentReaction, + Issue, + IssueActivity, + IssueAttachment, + IssueComment, + IssueLink, + IssueProperty, + IssueReaction, + IssueRelation, + IssueSubscriber, + Label, + Project, + ProjectMember, +) from plane.utils.cache import invalidate_cache +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) +from plane.utils.issue_filters import issue_filters +from plane.utils.order_queryset import order_issue_queryset +from plane.utils.paginator import GroupedOffsetPaginator + +# Module imports +from .. import BaseAPIView, BaseViewSet, WebhookMixin + class IssueListEndpoint(BaseAPIView): @@ -129,143 +138,58 @@ class IssueListEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) ).distinct() 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 = queryset.filter(**filters) + # Issue queryset + issue_queryset = order_issue_queryset( + issue_queryset=issue_queryset, + order_by_param=order_by_param, + ) - # 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") + # Group by + group_by = request.GET.get("group_by", False) + issue_queryset = issue_queryset_grouper( + queryset=issue_queryset, field=group_by + ) - # 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] + # List Paginate + if not group_by: + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), ) - 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) - if self.fields or self.expand: - issues = IssueSerializer( - queryset, many=True, fields=self.fields, expand=self.expand - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - return Response(issues, status=status.HTTP_200_OK) + # Group paginate + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), + paginator_cls=GroupedOffsetPaginator, + group_by_field_name=group_by, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + count_filter=Q( + Q(issue_inbox__status=1) + | Q(issue_inbox__status=-1) + | Q(issue_inbox__status=2) + | Q(issue_inbox__isnull=True), + archived_at__isnull=False, + is_draft=True, + ), + ) class IssueViewSet(WebhookMixin, BaseViewSet): @@ -323,32 +247,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) ).distinct() @method_decorator(gzip_page) @@ -358,112 +256,55 @@ class IssueViewSet(WebhookMixin, BaseViewSet): issue_queryset = self.get_queryset().filter(**filters) # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - # 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") + # Issue queryset + issue_queryset = order_issue_queryset( + issue_queryset=issue_queryset, + order_by_param=order_by_param, + ) - # 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) + # Group by + group_by = request.GET.get("group_by", False) - # Only use serializer when expand or fields else return by values - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - expand=self.expand, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", + # issue queryset + issue_queryset = issue_queryset_grouper( + queryset=issue_queryset, field=group_by + ) + + # List Paginate + if not group_by: + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), ) - return Response(issues, status=status.HTTP_200_OK) + + # Group paginate + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), + paginator_cls=GroupedOffsetPaginator, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + group_by_field_name=group_by, + count_filter=Q( + Q(issue_inbox__status=1) + | Q(issue_inbox__status=-1) + | Q(issue_inbox__status=2) + | Q(issue_inbox__isnull=True), + archived_at__isnull=False, + is_draft=True, + ), + ) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py index 08032934b..044f1c89e 100644 --- a/apiserver/plane/app/views/issue/draft.py +++ b/apiserver/plane/app/views/issue/draft.py @@ -1,52 +1,61 @@ # Python imports import json -# Django imports -from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, - UUIDField, -) -from django.core.serializers.json import DjangoJSONEncoder -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField +from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import ( + Case, + CharField, + Exists, + F, + Func, + Max, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, + When, +) from django.db.models.functions import Coalesce +# Django imports +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from rest_framework import status + # Third Party imports from rest_framework.response import Response -from rest_framework import status + +from plane.app.permissions import ProjectEntityPermission +from plane.app.serializers import ( + IssueCreateSerializer, + IssueDetailSerializer, + IssueFlatSerializer, + IssueSerializer, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.db.models import ( + Issue, + IssueAttachment, + IssueLink, + IssueReaction, + IssueSubscriber, + Project, +) +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) +from plane.utils.issue_filters import issue_filters +from plane.utils.order_queryset import order_issue_queryset +from plane.utils.paginator import GroupedOffsetPaginator # Module imports from .. import BaseViewSet -from plane.app.serializers import ( - IssueSerializer, - IssueCreateSerializer, - IssueFlatSerializer, - IssueDetailSerializer, -) -from plane.app.permissions import ProjectEntityPermission -from plane.db.models import ( - Project, - Issue, - IssueLink, - IssueAttachment, - IssueSubscriber, - IssueReaction, -) -from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.issue_filters import issue_filters class IssueDraftViewSet(BaseViewSet): @@ -86,154 +95,63 @@ class IssueDraftViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=~Q(issue_module__module_id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) ).distinct() @method_decorator(gzip_page) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] - - # 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) - # 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") + # Issue queryset + issue_queryset = order_issue_queryset( + issue_queryset=issue_queryset, + order_by_param=order_by_param, + ) - # 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) + # Group by + group_by = request.GET.get("group_by", False) - # Only use serializer when expand else return by values - if self.expand or self.fields: - issues = IssueSerializer( - issue_queryset, - many=True, - fields=self.fields, - expand=self.expand, - ).data - else: - issues = issue_queryset.values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", + issue_queryset = issue_queryset_grouper( + queryset=issue_queryset, field=group_by + ) + + # List Paginate + if not group_by: + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), ) - return Response(issues, status=status.HTTP_200_OK) + + # Group paginate + return self.paginate( + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues + ), + paginator_cls=GroupedOffsetPaginator, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + group_by_field_name=group_by, + count_filter=Q( + Q(issue_inbox__status=1) + | Q(issue_inbox__status=-1) + | Q(issue_inbox__status=2) + | Q(issue_inbox__isnull=True), + archived_at__isnull=False, + is_draft=True, + ), + ) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 250cf3719..5569768c5 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -50,7 +50,11 @@ from plane.db.models import ( Project, ) from plane.utils.analytics_plot import burndown_plot -from plane.utils.grouper import issue_on_results, issue_queryset_grouper +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator @@ -522,6 +526,12 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues ), + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), paginator_cls=GroupedOffsetPaginator, group_by_field_name=group_by, count_filter=Q( diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 14e9f9e4e..9c14246bf 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -188,6 +188,7 @@ class GlobalViewIssuesViewSet(BaseViewSet): group_by_fields=issue_group_values( field=group_by, slug=slug, + filters=filters, ), paginator_cls=GroupedOffsetPaginator, group_by_field_name=group_by, diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index d24db5ae9..4515c4c81 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -1,17 +1,16 @@ # Python import # Django imports +# Third party imports +from celery import shared_task +from django.conf import settings from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags -from django.conf import settings - -# Third party imports -from celery import shared_task from sentry_sdk import capture_exception # Module imports -from plane.db.models import Project, User, ProjectMemberInvite +from plane.db.models import Project, ProjectMemberInvite, User from plane.license.utils.instance_value import get_email_configuration diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py index e5cb36eec..63e7db784 100644 --- a/apiserver/plane/space/views/issue.py +++ b/apiserver/plane/space/views/issue.py @@ -1,62 +1,65 @@ # Python imports import json +from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import ( + Case, + CharField, + Exists, + F, + Func, + IntegerField, + Max, + OuterRef, + Prefetch, + Q, + Value, + When, +) + # Django imports from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, - IntegerField, -) -from django.core.serializers.json import DjangoJSONEncoder - -# Third Party imports -from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import AllowAny, IsAuthenticated -# Module imports -from .base import BaseViewSet, BaseAPIView -from plane.app.serializers import ( - IssueCommentSerializer, - IssueReactionSerializer, - CommentReactionSerializer, - IssueVoteSerializer, - IssuePublicSerializer, -) +# Third Party imports +from rest_framework.response import Response -from plane.db.models import ( - Issue, - IssueComment, - Label, - IssueLink, - IssueAttachment, - State, - ProjectMember, - IssueReaction, - CommentReaction, - ProjectDeployBoard, - IssueVote, - ProjectPublicMember, -) -from plane.utils.grouper import ( - issue_queryset_grouper, - issue_on_results, +from plane.app.serializers import ( + CommentReactionSerializer, + IssueCommentSerializer, + IssuePublicSerializer, + IssueReactionSerializer, + IssueVoteSerializer, ) from plane.bgtasks.issue_activites_task import issue_activity +from plane.db.models import ( + CommentReaction, + Issue, + IssueAttachment, + IssueComment, + IssueLink, + IssueReaction, + IssueVote, + Label, + ProjectDeployBoard, + ProjectMember, + ProjectPublicMember, + State, +) +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator +# Module imports +from .base import BaseAPIView, BaseViewSet + + class IssueCommentPublicViewSet(BaseViewSet): serializer_class = IssueCommentSerializer model = IssueComment @@ -594,6 +597,11 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): group_by=group_by, issues=issues ), paginator_cls=GroupedOffsetPaginator, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + filters=filters, + ), group_by_field_name=group_by, count_filter=Q( Q(issue_inbox__status=1) diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py index dba89c4a6..6a89f66c8 100644 --- a/apiserver/plane/utils/cache.py +++ b/apiserver/plane/utils/cache.py @@ -1,7 +1,8 @@ -from django.core.cache import cache # from django.utils.encoding import force_bytes # import hashlib from functools import wraps + +from django.core.cache import cache from rest_framework.response import Response @@ -22,18 +23,17 @@ def cache_response(timeout=60 * 60, path=None, user=True): def _wrapped_view(instance, request, *args, **kwargs): # Function to generate cache key auth_header = ( - None if request.user.is_anonymous else str(request.user.id) if user else None + None + if request.user.is_anonymous + else str(request.user.id) if user else None ) custom_path = path if path is not None else request.get_full_path() key = generate_cache_key(custom_path, auth_header) cached_result = cache.get(key) if cached_result is not None: - print("Cache Hit") return Response( cached_result["data"], status=cached_result["status"] ) - - print("Cache Miss") response = view_func(instance, request, *args, **kwargs) if response.status_code == 200: @@ -71,11 +71,12 @@ def invalidate_cache(path=None, url_params=False, user=True): ) auth_header = ( - None if request.user.is_anonymous else str(request.user.id) if user else None + None + if request.user.is_anonymous + else str(request.user.id) if user else None ) key = generate_cache_key(custom_path, auth_header) cache.delete(key) - print("Invalidating cache") # Execute the view function return view_func(instance, request, *args, **kwargs) diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index c814ab72d..d432333e7 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -1,17 +1,18 @@ # Django imports -from django.db.models import Q, F from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField -from django.db.models import Value, UUIDField +from django.db.models import F, Q, UUIDField, Value from django.db.models.functions import Coalesce # Module imports from plane.db.models import ( - State, - Label, - ProjectMember, Cycle, + Issue, + Label, Module, + Project, + ProjectMember, + State, WorkspaceMember, ) @@ -144,7 +145,7 @@ def issue_on_results(issues, group_by): return issues.values(*required_fields) -def issue_group_values(field, slug, project_id=None): +def issue_group_values(field, slug, project_id=None, filters={}): if field == "state_id": queryset = State.objects.filter( ~Q(name="Triage"), @@ -191,6 +192,11 @@ def issue_group_values(field, slug, project_id=None): return list(queryset.filter(project_id=project_id)) + ["None"] else: return list(queryset) + ["None"] + if field == "project_id": + queryset = Project.objects.filter(workspace__slug=slug).values_list( + "id", flat=True + ) + return list(queryset) if field == "priority": return [ "low", @@ -207,4 +213,26 @@ def issue_group_values(field, slug, project_id=None): "completed", "cancelled", ] + if field == "target_date": + queryset = ( + Issue.issue_objects.filter(workspace__slug=slug) + .filter(**filters) + .values_list("target_date", flat=True) + .distinct() + ) + if project_id: + return list(queryset.filter(project_id=project_id)) + else: + return list(queryset) + if field == "start_date": + queryset = ( + Issue.issue_objects.filter(workspace__slug=slug) + .filter(**filters) + .values_list("start_date", flat=True) + .distinct() + ) + if project_id: + return list(queryset.filter(project_id=project_id)) + else: + return list(queryset) return [] diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index db8e1bd94..66114713d 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -1,15 +1,15 @@ # Python imports import math -from collections.abc import Sequence from collections import defaultdict +from collections.abc import Sequence # Django imports -from django.db.models import Window, F, Count, Q -from django.db.models.functions import RowNumber, DenseRank +from django.db.models import Count, F, Q, Window +from django.db.models.functions import DenseRank, RowNumber +from rest_framework.exceptions import ParseError, ValidationError # Third party imports from rest_framework.response import Response -from rest_framework.exceptions import ParseError, ValidationError # Module imports from plane.db.models import Issue @@ -325,7 +325,6 @@ class GroupedOffsetPaginator(OffsetPaginator): def __query_grouper(self, results): processed_results = self.__get_field_dict() - print(results) for result in results: group_value = str(result.get(self.group_by_field_name)) if group_value in processed_results: