diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index b19e38de7..b0191581e 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -77,6 +77,7 @@ from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import GroupedOffsetPaginator + class IssueListEndpoint(BaseAPIView): permission_classes = [ @@ -339,22 +340,24 @@ class IssueViewSet(WebhookMixin, BaseViewSet): "archived_at", ) - if request.GET.get("layout", "spreadsheet") in [ - "gantt", - "spreadsheet", - ]: + # Group by + group_by = request.GET.get("group_by", False) + + # List Paginate + if not group_by: return self.paginate( request=request, queryset=issue_queryset, on_results=on_results ) + # Group paginate return self.paginate( request=request, queryset=issue_queryset, on_results=on_results, paginator_cls=GroupedOffsetPaginator, - group_by_field_name="priority", + group_by_field_name=group_by, group_by_fields=issue_grouper( - field="priority", slug=slug, project_id=project_id + field=group_by, slug=slug, project_id=project_id ), ) @@ -2018,7 +2021,9 @@ class IssueDraftViewSet(BaseViewSet): status=status.HTTP_404_NOT_FOUND, ) - serializer = IssueCreateSerializer(issue, data=request.data, partial=True) + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index 7adbddf38..eb5c1d537 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -1,3 +1,7 @@ +# Django imports +from django.db.models import Q + +# Module imports from plane.db.models import State, Label, ProjectMember, Cycle, Module @@ -244,19 +248,21 @@ def group_results(results_data, group_by, sub_group_by=False): def issue_grouper(field, slug, project_id): - if field == "state": + if field == "state_id": return list( State.objects.filter( - workspace__slug=slug, project_id=project_id + ~Q(name="Triage"), + workspace__slug=slug, + project_id=project_id, ).values_list("id", flat=True) ) - if field == "labels": + if field == "label_ids": return list( Label.objects.filter( workspace__slug=slug, project_id=project_id ).values_list("id", flat=True) ) - if field == "assignees": + if field == "assignee_ids": return list( ProjectMember.objects.filter( workspace__slug=slug, project_id=project_id @@ -271,13 +277,13 @@ def issue_grouper(field, slug, project_id): workspace__slug=slug, project_id=project_id ).values_list("member_id", flat=True) ) - if field == "cycle": + if field == "cycle_id": return list( Cycle.objects.filter( workspace__slug=slug, project_id=project_id ).values_list("id", flat=True) ) - if field == "module": + if field == "module_ids": return list( Module.objects.filter( workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index 0e7929fb0..ba617762e 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -4,7 +4,7 @@ from collections.abc import Sequence # Django imports from django.db.models import Window, F, Count, Q -from django.db.models.functions import RowNumber +from django.db.models.functions import RowNumber, DenseRank # Third party imports from rest_framework.response import Response @@ -149,6 +149,9 @@ class OffsetPaginator: max_hits=max_hits, ) + def process_results(self, results): + raise NotImplementedError + class GroupedOffsetPaginator(OffsetPaginator): def __init__( @@ -186,6 +189,10 @@ class GroupedOffsetPaginator(OffsetPaginator): # Compute the results results = {} queryset = queryset.annotate( + # group_rank=Window( + # expression=DenseRank(), + # order_by=F(self.group_by_field_name).asc() + # ), row_number=Window( expression=RowNumber(), partition_by=[F(self.group_by_field_name)], @@ -193,6 +200,7 @@ class GroupedOffsetPaginator(OffsetPaginator): ) ) + # Filter the results results = queryset.filter(row_number__gte=offset, row_number__lt=stop) # Adjust cursors based on the grouped results for pagination @@ -212,7 +220,7 @@ class GroupedOffsetPaginator(OffsetPaginator): # Optionally, calculate the total count and max_hits if needed # This might require adjustments based on specific use cases max_hits = math.ceil( - self.queryset.values(self.group_by_field_name) + queryset.values(self.group_by_field_name) .annotate( count=Count( self.group_by_field_name, @@ -221,7 +229,6 @@ class GroupedOffsetPaginator(OffsetPaginator): .order_by("-count")[0]["count"] / limit ) - return CursorResult( results=results, next=next_cursor, @@ -230,6 +237,17 @@ class GroupedOffsetPaginator(OffsetPaginator): max_hits=max_hits, ) + def process_results(self, results): + processed_results = {} + for result in results: + group_value = str(result.get(self.group_by_field_name)) + if group_value not in processed_results: + processed_results[str(group_value)] = { + "results": [], + } + processed_results[str(group_value)]["results"].append(result) + return processed_results + class BasePaginator: """BasePaginator class can be inherited by any View to return a paginated view""" @@ -252,18 +270,6 @@ class BasePaginator: return per_page - def get_layout(self, request): - layout = request.GET.get("layout", "list") - if layout not in [ - "list", - "kanban", - "spreadsheet", - "calendar", - "gantt", - ]: - raise ValidationError(detail="Invalid layout given") - return layout - def paginate( self, request, @@ -292,8 +298,9 @@ class BasePaginator: raise ParseError(detail="Invalid cursor parameter.") if not paginator: - paginator_kwargs["group_by_fields"] = group_by_fields - paginator_kwargs["group_by_field_name"] = group_by_field_name + if group_by_fields and group_by_field_name: + paginator_kwargs["group_by_fields"] = group_by_fields + paginator_kwargs["group_by_field_name"] = group_by_field_name paginator = paginator_cls(**paginator_kwargs) try: @@ -306,17 +313,9 @@ class BasePaginator: if on_results: results = on_results(cursor_result.results) - processed_results = {} if group_by_field_name and group_by_fields: - for result in results: - group_value = str(result.get(group_by_field_name)) - if group_value not in processed_results: - processed_results[str(group_value)] = { - "results": [], - } - processed_results[str(group_value)]["results"].append(result) + results = paginator.process_results(results=results) - results = processed_results # Add Manipulation functions to the response if controller is not None: results = controller(results)