dev: fix grouping on taget date and project_id

This commit is contained in:
pablohashescobar 2024-03-07 15:11:44 +05:30
parent 75458e33ba
commit 9d60aaddd7
11 changed files with 457 additions and 715 deletions

View File

@ -54,7 +54,11 @@ from plane.db.models import (
User, User,
) )
from plane.utils.analytics_plot import burndown_plot 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.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator from plane.utils.paginator import GroupedOffsetPaginator
@ -759,7 +763,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
# Group by # Group by
group_by = request.GET.get("group_by", False) group_by = request.GET.get("group_by", False)
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(
queryset=issue_queryset, field=group_by queryset=issue_queryset,
field=group_by,
) )
# List Paginate # List Paginate
@ -781,6 +786,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
), ),
paginator_cls=GroupedOffsetPaginator, paginator_cls=GroupedOffsetPaginator,
group_by_field_name=group_by, 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( count_filter=Q(
Q(issue_inbox__status=1) Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1) | Q(issue_inbox__status=-1)

View File

@ -1,52 +1,61 @@
# Python imports # Python imports
import json 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.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField 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 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 # Third Party imports
from rest_framework.response import Response 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 ( from plane.app.permissions import (
ProjectEntityPermission, ProjectEntityPermission,
) )
from plane.db.models import ( from plane.app.serializers import (
Issue, IssueDetailSerializer,
IssueLink, IssueFlatSerializer,
IssueAttachment, IssueSerializer,
IssueSubscriber,
IssueReaction,
) )
from plane.bgtasks.issue_activites_task import issue_activity 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.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): class IssueArchiveViewSet(BaseViewSet):
@ -92,32 +101,6 @@ class IssueArchiveViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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) @method_decorator(gzip_page)
@ -125,120 +108,63 @@ class IssueArchiveViewSet(BaseViewSet):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true") 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") order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = self.get_queryset().filter(**filters) 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 = (
issue_queryset issue_queryset
if show_sub_issues == "true" if show_sub_issues == "true"
else issue_queryset.filter(parent__isnull=True) else issue_queryset.filter(parent__isnull=True)
) )
if self.expand or self.fields: # Issue queryset
issues = IssueSerializer( issue_queryset = order_issue_queryset(
issue_queryset, issue_queryset=issue_queryset,
many=True, order_by_param=order_by_param,
fields=self.fields, )
).data
else: # Group by
issues = issue_queryset.values( group_by = request.GET.get("group_by", False)
"id",
"name", issue_queryset = issue_queryset_grouper(
"state_id", queryset=issue_queryset,
"sort_order", field=group_by,
"completed_at", )
"estimate_point",
"priority", # List Paginate
"start_date", if not group_by:
"target_date", return self.paginate(
"sequence_id", request=request,
"project_id", queryset=issue_queryset,
"parent_id", on_results=lambda issues: issue_on_results(
"cycle_id", group_by=group_by, issues=issues
"module_ids", ),
"label_ids", )
"assignee_ids",
"sub_issues_count", # Group paginate
"created_at", return self.paginate(
"updated_at", request=request,
"created_by", queryset=issue_queryset,
"updated_by", on_results=lambda issues: issue_on_results(
"attachment_count", group_by=group_by, issues=issues
"link_count", ),
"is_draft", paginator_cls=GroupedOffsetPaginator,
"archived_at", 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,
),
) )
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
issue = ( issue = (

View File

@ -1,84 +1,93 @@
# Python imports # Python imports
import json import json
import random import random
from collections import defaultdict
from itertools import chain 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 # Django imports
from django.utils import timezone 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.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError from rest_framework import status
from django.contrib.postgres.aggregates import ArrayAgg from rest_framework.parsers import FormParser, MultiPartParser
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
from django.db.models.functions import Coalesce
# Third Party imports # Third Party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
# Module imports from plane.app.permissions import (
from .. import BaseViewSet, BaseAPIView, WebhookMixin ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,
WorkSpaceAdminPermission,
)
from plane.app.serializers import ( from plane.app.serializers import (
CommentReactionSerializer,
IssueActivitySerializer, IssueActivitySerializer,
IssueAttachmentSerializer,
IssueCommentSerializer, IssueCommentSerializer,
IssuePropertySerializer,
IssueSerializer,
IssueCreateSerializer, IssueCreateSerializer,
LabelSerializer, IssueDetailSerializer,
IssueFlatSerializer, IssueFlatSerializer,
IssueLinkSerializer, IssueLinkSerializer,
IssueLiteSerializer, IssueLiteSerializer,
IssueAttachmentSerializer, IssuePropertySerializer,
IssueSubscriberSerializer,
ProjectMemberLiteSerializer,
IssueReactionSerializer, IssueReactionSerializer,
CommentReactionSerializer,
IssueRelationSerializer, IssueRelationSerializer,
IssueSerializer,
IssueSubscriberSerializer,
LabelSerializer,
ProjectMemberLiteSerializer,
RelatedIssueSerializer, 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.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.db.models import (
from plane.utils.issue_filters import issue_filters CommentReaction,
from collections import defaultdict Issue,
IssueActivity,
IssueAttachment,
IssueComment,
IssueLink,
IssueProperty,
IssueReaction,
IssueRelation,
IssueSubscriber,
Label,
Project,
ProjectMember,
)
from plane.utils.cache import invalidate_cache 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): class IssueListEndpoint(BaseAPIView):
@ -129,143 +138,58 @@ class IssueListEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
filters = issue_filters(request.query_params, "GET") 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") order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = queryset.filter(**filters) issue_queryset = queryset.filter(**filters)
# Issue queryset
issue_queryset = order_issue_queryset(
issue_queryset=issue_queryset,
order_by_param=order_by_param,
)
# Priority Ordering # Group by
if order_by_param == "priority" or order_by_param == "-priority": group_by = request.GET.get("group_by", False)
priority_order = ( issue_queryset = issue_queryset_grouper(
priority_order queryset=issue_queryset, field=group_by
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 # List Paginate
elif order_by_param in [ if not group_by:
"state__name", return self.paginate(
"state__group", request=request,
"-state__name", queryset=issue_queryset,
"-state__group", on_results=lambda issues: issue_on_results(
]: group_by=group_by, issues=issues
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)
if self.fields or self.expand: # Group paginate
issues = IssueSerializer( return self.paginate(
queryset, many=True, fields=self.fields, expand=self.expand request=request,
).data queryset=issue_queryset,
else: on_results=lambda issues: issue_on_results(
issues = issue_queryset.values( group_by=group_by, issues=issues
"id", ),
"name", paginator_cls=GroupedOffsetPaginator,
"state_id", group_by_field_name=group_by,
"sort_order", group_by_fields=issue_group_values(
"completed_at", field=group_by,
"estimate_point", slug=slug,
"priority", project_id=project_id,
"start_date", filters=filters,
"target_date", ),
"sequence_id", count_filter=Q(
"project_id", Q(issue_inbox__status=1)
"parent_id", | Q(issue_inbox__status=-1)
"cycle_id", | Q(issue_inbox__status=2)
"module_ids", | Q(issue_inbox__isnull=True),
"label_ids", archived_at__isnull=False,
"assignee_ids", is_draft=True,
"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)
class IssueViewSet(WebhookMixin, BaseViewSet): class IssueViewSet(WebhookMixin, BaseViewSet):
@ -323,32 +247,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
@ -358,112 +256,55 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
issue_queryset = self.get_queryset().filter(**filters) issue_queryset = self.get_queryset().filter(**filters)
# Custom ordering for priority and state # Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
# Priority Ordering # Issue queryset
if order_by_param == "priority" or order_by_param == "-priority": issue_queryset = order_issue_queryset(
priority_order = ( issue_queryset=issue_queryset,
priority_order order_by_param=order_by_param,
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 # Group by
elif order_by_param in [ group_by = request.GET.get("group_by", False)
"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)
# Only use serializer when expand or fields else return by values # issue queryset
if self.expand or self.fields: issue_queryset = issue_queryset_grouper(
issues = IssueSerializer( queryset=issue_queryset, field=group_by
issue_queryset, )
many=True,
fields=self.fields, # List Paginate
expand=self.expand, if not group_by:
).data return self.paginate(
else: request=request,
issues = issue_queryset.values( queryset=issue_queryset,
"id", on_results=lambda issues: issue_on_results(
"name", group_by=group_by, issues=issues
"state_id", ),
"sort_order", )
"completed_at",
"estimate_point", # Group paginate
"priority", return self.paginate(
"start_date", request=request,
"target_date", queryset=issue_queryset,
"sequence_id", on_results=lambda issues: issue_on_results(
"project_id", group_by=group_by, issues=issues
"parent_id", ),
"cycle_id", paginator_cls=GroupedOffsetPaginator,
"module_ids", group_by_fields=issue_group_values(
"label_ids", field=group_by,
"assignee_ids", slug=slug,
"sub_issues_count", project_id=project_id,
"created_at", filters=filters,
"updated_at", ),
"created_by", group_by_field_name=group_by,
"updated_by", count_filter=Q(
"attachment_count", Q(issue_inbox__status=1)
"link_count", | Q(issue_inbox__status=-1)
"is_draft", | Q(issue_inbox__status=2)
"archived_at", | Q(issue_inbox__isnull=True),
archived_at__isnull=False,
is_draft=True,
),
) )
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)

View File

@ -1,52 +1,61 @@
# Python imports # Python imports
import json 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.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField 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 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 # Third Party imports
from rest_framework.response import Response 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 # Module imports
from .. import BaseViewSet 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): class IssueDraftViewSet(BaseViewSet):
@ -86,154 +95,63 @@ class IssueDraftViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET") 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") order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = self.get_queryset().filter(**filters) issue_queryset = self.get_queryset().filter(**filters)
# Priority Ordering # Issue queryset
if order_by_param == "priority" or order_by_param == "-priority": issue_queryset = order_issue_queryset(
priority_order = ( issue_queryset=issue_queryset,
priority_order order_by_param=order_by_param,
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 # Group by
elif order_by_param in [ group_by = request.GET.get("group_by", False)
"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)
# Only use serializer when expand else return by values issue_queryset = issue_queryset_grouper(
if self.expand or self.fields: queryset=issue_queryset, field=group_by
issues = IssueSerializer( )
issue_queryset,
many=True, # List Paginate
fields=self.fields, if not group_by:
expand=self.expand, return self.paginate(
).data request=request,
else: queryset=issue_queryset,
issues = issue_queryset.values( on_results=lambda issues: issue_on_results(
"id", group_by=group_by, issues=issues
"name", ),
"state_id", )
"sort_order",
"completed_at", # Group paginate
"estimate_point", return self.paginate(
"priority", request=request,
"start_date", queryset=issue_queryset,
"target_date", on_results=lambda issues: issue_on_results(
"sequence_id", group_by=group_by, issues=issues
"project_id", ),
"parent_id", paginator_cls=GroupedOffsetPaginator,
"cycle_id", group_by_fields=issue_group_values(
"module_ids", field=group_by,
"label_ids", slug=slug,
"assignee_ids", project_id=project_id,
"sub_issues_count", filters=filters,
"created_at", ),
"updated_at", group_by_field_name=group_by,
"created_by", count_filter=Q(
"updated_by", Q(issue_inbox__status=1)
"attachment_count", | Q(issue_inbox__status=-1)
"link_count", | Q(issue_inbox__status=2)
"is_draft", | Q(issue_inbox__isnull=True),
"archived_at", archived_at__isnull=False,
is_draft=True,
),
) )
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)

View File

@ -50,7 +50,11 @@ from plane.db.models import (
Project, Project,
) )
from plane.utils.analytics_plot import burndown_plot 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.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator from plane.utils.paginator import GroupedOffsetPaginator
@ -522,6 +526,12 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
on_results=lambda issues: issue_on_results( on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues 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, paginator_cls=GroupedOffsetPaginator,
group_by_field_name=group_by, group_by_field_name=group_by,
count_filter=Q( count_filter=Q(

View File

@ -188,6 +188,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
group_by_fields=issue_group_values( group_by_fields=issue_group_values(
field=group_by, field=group_by,
slug=slug, slug=slug,
filters=filters,
), ),
paginator_cls=GroupedOffsetPaginator, paginator_cls=GroupedOffsetPaginator,
group_by_field_name=group_by, group_by_field_name=group_by,

View File

@ -1,17 +1,16 @@
# Python import # Python import
# Django imports # 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.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags 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 from sentry_sdk import capture_exception
# Module imports # 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 from plane.license.utils.instance_value import get_email_configuration

View File

@ -1,62 +1,65 @@
# Python imports # Python imports
import json 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 # Django imports
from django.utils import timezone 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 import status
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
# Module imports # Third Party imports
from .base import BaseViewSet, BaseAPIView from rest_framework.response import Response
from plane.app.serializers import (
IssueCommentSerializer,
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssuePublicSerializer,
)
from plane.db.models import ( from plane.app.serializers import (
Issue, CommentReactionSerializer,
IssueComment, IssueCommentSerializer,
Label, IssuePublicSerializer,
IssueLink, IssueReactionSerializer,
IssueAttachment, IssueVoteSerializer,
State,
ProjectMember,
IssueReaction,
CommentReaction,
ProjectDeployBoard,
IssueVote,
ProjectPublicMember,
)
from plane.utils.grouper import (
issue_queryset_grouper,
issue_on_results,
) )
from plane.bgtasks.issue_activites_task import issue_activity 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.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator from plane.utils.paginator import GroupedOffsetPaginator
# Module imports
from .base import BaseAPIView, BaseViewSet
class IssueCommentPublicViewSet(BaseViewSet): class IssueCommentPublicViewSet(BaseViewSet):
serializer_class = IssueCommentSerializer serializer_class = IssueCommentSerializer
model = IssueComment model = IssueComment
@ -594,6 +597,11 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
group_by=group_by, issues=issues group_by=group_by, issues=issues
), ),
paginator_cls=GroupedOffsetPaginator, paginator_cls=GroupedOffsetPaginator,
group_by_fields=issue_group_values(
field=group_by,
slug=slug,
filters=filters,
),
group_by_field_name=group_by, group_by_field_name=group_by,
count_filter=Q( count_filter=Q(
Q(issue_inbox__status=1) Q(issue_inbox__status=1)

View File

@ -1,7 +1,8 @@
from django.core.cache import cache
# from django.utils.encoding import force_bytes # from django.utils.encoding import force_bytes
# import hashlib # import hashlib
from functools import wraps from functools import wraps
from django.core.cache import cache
from rest_framework.response import Response 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): def _wrapped_view(instance, request, *args, **kwargs):
# Function to generate cache key # Function to generate cache key
auth_header = ( 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() custom_path = path if path is not None else request.get_full_path()
key = generate_cache_key(custom_path, auth_header) key = generate_cache_key(custom_path, auth_header)
cached_result = cache.get(key) cached_result = cache.get(key)
if cached_result is not None: if cached_result is not None:
print("Cache Hit")
return Response( return Response(
cached_result["data"], status=cached_result["status"] cached_result["data"], status=cached_result["status"]
) )
print("Cache Miss")
response = view_func(instance, request, *args, **kwargs) response = view_func(instance, request, *args, **kwargs)
if response.status_code == 200: if response.status_code == 200:
@ -71,11 +71,12 @@ def invalidate_cache(path=None, url_params=False, user=True):
) )
auth_header = ( 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) key = generate_cache_key(custom_path, auth_header)
cache.delete(key) cache.delete(key)
print("Invalidating cache")
# Execute the view function # Execute the view function
return view_func(instance, request, *args, **kwargs) return view_func(instance, request, *args, **kwargs)

View File

@ -1,17 +1,18 @@
# Django imports # Django imports
from django.db.models import Q, F
from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField 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 from django.db.models.functions import Coalesce
# Module imports # Module imports
from plane.db.models import ( from plane.db.models import (
State,
Label,
ProjectMember,
Cycle, Cycle,
Issue,
Label,
Module, Module,
Project,
ProjectMember,
State,
WorkspaceMember, WorkspaceMember,
) )
@ -144,7 +145,7 @@ def issue_on_results(issues, group_by):
return issues.values(*required_fields) 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": if field == "state_id":
queryset = State.objects.filter( queryset = State.objects.filter(
~Q(name="Triage"), ~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"] return list(queryset.filter(project_id=project_id)) + ["None"]
else: else:
return list(queryset) + ["None"] 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": if field == "priority":
return [ return [
"low", "low",
@ -207,4 +213,26 @@ def issue_group_values(field, slug, project_id=None):
"completed", "completed",
"cancelled", "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 [] return []

View File

@ -1,15 +1,15 @@
# Python imports # Python imports
import math import math
from collections.abc import Sequence
from collections import defaultdict from collections import defaultdict
from collections.abc import Sequence
# Django imports # Django imports
from django.db.models import Window, F, Count, Q from django.db.models import Count, F, Q, Window
from django.db.models.functions import RowNumber, DenseRank from django.db.models.functions import DenseRank, RowNumber
from rest_framework.exceptions import ParseError, ValidationError
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.exceptions import ParseError, ValidationError
# Module imports # Module imports
from plane.db.models import Issue from plane.db.models import Issue
@ -325,7 +325,6 @@ class GroupedOffsetPaginator(OffsetPaginator):
def __query_grouper(self, results): def __query_grouper(self, results):
processed_results = self.__get_field_dict() processed_results = self.__get_field_dict()
print(results)
for result in results: for result in results:
group_value = str(result.get(self.group_by_field_name)) group_value = str(result.get(self.group_by_field_name))
if group_value in processed_results: if group_value in processed_results: