diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py
index dfdd265cd..687747242 100644
--- a/apiserver/plane/app/serializers/module.py
+++ b/apiserver/plane/app/serializers/module.py
@@ -210,6 +210,7 @@ class ModuleSerializer(DynamicBaseSerializer):
"backlog_issues",
"created_at",
"updated_at",
+ "archived_at",
]
read_only_fields = fields
diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py
index 2cdab312c..3d7603e24 100644
--- a/apiserver/plane/app/views/__init__.py
+++ b/apiserver/plane/app/views/__init__.py
@@ -38,7 +38,7 @@ from .workspace.base import (
WorkSpaceAvailabilityCheckEndpoint,
UserWorkspaceDashboardEndpoint,
WorkspaceThemeViewSet,
- ExportWorkspaceUserActivityEndpoint
+ ExportWorkspaceUserActivityEndpoint,
)
from .workspace.member import (
@@ -91,12 +91,14 @@ from .cycle.base import (
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
- CycleArchiveUnarchiveEndpoint,
CycleUserPropertiesEndpoint,
)
from .cycle.issue import (
CycleIssueViewSet,
)
+from .cycle.archive import (
+ CycleArchiveUnarchiveEndpoint,
+)
from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .issue.base import (
@@ -170,7 +172,6 @@ from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
- ModuleArchiveUnarchiveEndpoint,
ModuleUserPropertiesEndpoint,
)
@@ -178,6 +179,10 @@ from .module.issue import (
ModuleIssueViewSet,
)
+from .module.archive import (
+ ModuleArchiveUnarchiveEndpoint,
+)
+
from .api import ApiTokenEndpoint
diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py
new file mode 100644
index 000000000..e6d82795a
--- /dev/null
+++ b/apiserver/plane/app/views/cycle/archive.py
@@ -0,0 +1,409 @@
+# Django imports
+from django.contrib.postgres.aggregates import ArrayAgg
+from django.contrib.postgres.fields import ArrayField
+from django.db.models import (
+ Case,
+ CharField,
+ Count,
+ Exists,
+ F,
+ Func,
+ OuterRef,
+ Prefetch,
+ Q,
+ UUIDField,
+ Value,
+ When,
+)
+from django.db.models.functions import Coalesce
+from django.utils import timezone
+
+# Third party imports
+from rest_framework import status
+from rest_framework.response import Response
+from plane.app.permissions import ProjectEntityPermission
+from plane.db.models import (
+ Cycle,
+ CycleFavorite,
+ Issue,
+ Label,
+ User,
+)
+from plane.utils.analytics_plot import burndown_plot
+
+# Module imports
+from .. import BaseAPIView
+
+
+class CycleArchiveUnarchiveEndpoint(BaseAPIView):
+
+ permission_classes = [
+ ProjectEntityPermission,
+ ]
+
+ def get_queryset(self):
+ favorite_subquery = CycleFavorite.objects.filter(
+ user=self.request.user,
+ cycle_id=OuterRef("pk"),
+ project_id=self.kwargs.get("project_id"),
+ workspace__slug=self.kwargs.get("slug"),
+ )
+ return (
+ Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
+ .filter(project_id=self.kwargs.get("project_id"))
+ .filter(archived_at__isnull=False)
+ .filter(
+ project__project_projectmember__member=self.request.user,
+ project__project_projectmember__is_active=True,
+ )
+ .filter(project__archived_at__isnull=True)
+ .select_related("project", "workspace", "owned_by")
+ .prefetch_related(
+ Prefetch(
+ "issue_cycle__issue__assignees",
+ queryset=User.objects.only(
+ "avatar", "first_name", "id"
+ ).distinct(),
+ )
+ )
+ .prefetch_related(
+ Prefetch(
+ "issue_cycle__issue__labels",
+ queryset=Label.objects.only(
+ "name", "color", "id"
+ ).distinct(),
+ )
+ )
+ .annotate(is_favorite=Exists(favorite_subquery))
+ .annotate(
+ total_issues=Count(
+ "issue_cycle__issue__id",
+ distinct=True,
+ filter=Q(
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ completed_issues=Count(
+ "issue_cycle__issue__id",
+ distinct=True,
+ filter=Q(
+ issue_cycle__issue__state__group="completed",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ cancelled_issues=Count(
+ "issue_cycle__issue__id",
+ distinct=True,
+ filter=Q(
+ issue_cycle__issue__state__group="cancelled",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ started_issues=Count(
+ "issue_cycle__issue__id",
+ distinct=True,
+ filter=Q(
+ issue_cycle__issue__state__group="started",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ unstarted_issues=Count(
+ "issue_cycle__issue__id",
+ distinct=True,
+ filter=Q(
+ issue_cycle__issue__state__group="unstarted",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ backlog_issues=Count(
+ "issue_cycle__issue__id",
+ distinct=True,
+ filter=Q(
+ issue_cycle__issue__state__group="backlog",
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ status=Case(
+ When(
+ Q(start_date__lte=timezone.now())
+ & Q(end_date__gte=timezone.now()),
+ then=Value("CURRENT"),
+ ),
+ When(
+ start_date__gt=timezone.now(), then=Value("UPCOMING")
+ ),
+ When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
+ When(
+ Q(start_date__isnull=True) & Q(end_date__isnull=True),
+ then=Value("DRAFT"),
+ ),
+ default=Value("DRAFT"),
+ output_field=CharField(),
+ )
+ )
+ .annotate(
+ assignee_ids=Coalesce(
+ ArrayAgg(
+ "issue_cycle__issue__assignees__id",
+ distinct=True,
+ filter=~Q(
+ issue_cycle__issue__assignees__id__isnull=True
+ ),
+ ),
+ Value([], output_field=ArrayField(UUIDField())),
+ )
+ )
+ .order_by("-is_favorite", "name")
+ .distinct()
+ )
+
+ def get(self, request, slug, project_id, pk=None):
+ if pk is None:
+ queryset = (
+ self.get_queryset()
+ .annotate(
+ total_issues=Count(
+ "issue_cycle",
+ filter=Q(
+ issue_cycle__issue__archived_at__isnull=True,
+ issue_cycle__issue__is_draft=False,
+ ),
+ )
+ )
+ .values(
+ # necessary fields
+ "id",
+ "workspace_id",
+ "project_id",
+ # model fields
+ "name",
+ "description",
+ "start_date",
+ "end_date",
+ "owned_by_id",
+ "view_props",
+ "sort_order",
+ "external_source",
+ "external_id",
+ "progress_snapshot",
+ # meta fields
+ "total_issues",
+ "is_favorite",
+ "cancelled_issues",
+ "completed_issues",
+ "started_issues",
+ "unstarted_issues",
+ "backlog_issues",
+ "assignee_ids",
+ "status",
+ "archived_at",
+ )
+ ).order_by("-is_favorite", "-created_at")
+ return Response(queryset, status=status.HTTP_200_OK)
+ else:
+ queryset = (
+ self.get_queryset()
+ .filter(archived_at__isnull=False)
+ .filter(pk=pk)
+ )
+ data = (
+ self.get_queryset()
+ .filter(pk=pk)
+ .annotate(
+ sub_issues=Issue.issue_objects.filter(
+ project_id=self.kwargs.get("project_id"),
+ parent__isnull=False,
+ issue_cycle__cycle_id=pk,
+ )
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ .values(
+ # necessary fields
+ "id",
+ "workspace_id",
+ "project_id",
+ # model fields
+ "name",
+ "description",
+ "start_date",
+ "end_date",
+ "owned_by_id",
+ "view_props",
+ "sort_order",
+ "external_source",
+ "external_id",
+ "progress_snapshot",
+ "sub_issues",
+ # meta fields
+ "is_favorite",
+ "total_issues",
+ "cancelled_issues",
+ "completed_issues",
+ "started_issues",
+ "unstarted_issues",
+ "backlog_issues",
+ "assignee_ids",
+ "status",
+ )
+ .first()
+ )
+ queryset = queryset.first()
+
+ if data is None:
+ return Response(
+ {"error": "Cycle does not exist"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ # Assignee Distribution
+ assignee_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(first_name=F("assignees__first_name"))
+ .annotate(last_name=F("assignees__last_name"))
+ .annotate(assignee_id=F("assignees__id"))
+ .annotate(avatar=F("assignees__avatar"))
+ .annotate(display_name=F("assignees__display_name"))
+ .values(
+ "first_name",
+ "last_name",
+ "assignee_id",
+ "avatar",
+ "display_name",
+ )
+ .annotate(
+ total_issues=Count(
+ "id",
+ filter=Q(archived_at__isnull=True, is_draft=False),
+ ),
+ )
+ .annotate(
+ completed_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=False,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=True,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .order_by("first_name", "last_name")
+ )
+
+ # Label Distribution
+ label_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(label_name=F("labels__name"))
+ .annotate(color=F("labels__color"))
+ .annotate(label_id=F("labels__id"))
+ .values("label_name", "color", "label_id")
+ .annotate(
+ total_issues=Count(
+ "id",
+ filter=Q(archived_at__isnull=True, is_draft=False),
+ ),
+ )
+ .annotate(
+ completed_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=False,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=True,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .order_by("label_name")
+ )
+
+ data["distribution"] = {
+ "assignees": assignee_distribution,
+ "labels": label_distribution,
+ "completion_chart": {},
+ }
+
+ if queryset.start_date and queryset.end_date:
+ data["distribution"]["completion_chart"] = burndown_plot(
+ queryset=queryset,
+ slug=slug,
+ project_id=project_id,
+ cycle_id=pk,
+ )
+
+ return Response(
+ data,
+ status=status.HTTP_200_OK,
+ )
+
+ def post(self, request, slug, project_id, cycle_id):
+ cycle = Cycle.objects.get(
+ pk=cycle_id, project_id=project_id, workspace__slug=slug
+ )
+
+ if cycle.end_date >= timezone.now().date():
+ return Response(
+ {"error": "Only completed cycles can be archived"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ cycle.archived_at = timezone.now()
+ cycle.save()
+ return Response(
+ {"archived_at": str(cycle.archived_at)},
+ status=status.HTTP_200_OK,
+ )
+
+ def delete(self, request, slug, project_id, cycle_id):
+ cycle = Cycle.objects.get(
+ pk=cycle_id, project_id=project_id, workspace__slug=slug
+ )
+ cycle.archived_at = None
+ cycle.save()
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py
index 58dd9891f..dd9826c56 100644
--- a/apiserver/plane/app/views/cycle/base.py
+++ b/apiserver/plane/app/views/cycle/base.py
@@ -1,10 +1,9 @@
# Python imports
import json
+# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
-
-# Django imports
from django.db.models import (
Case,
CharField,
@@ -25,7 +24,6 @@ from django.utils import timezone
# Third party imports
from rest_framework import status
from rest_framework.response import Response
-
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
@@ -686,380 +684,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
-class CycleArchiveUnarchiveEndpoint(BaseAPIView):
-
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get_queryset(self):
- favorite_subquery = CycleFavorite.objects.filter(
- user=self.request.user,
- cycle_id=OuterRef("pk"),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- )
- return (
- Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(archived_at__isnull=False)
- .filter(
- project__project_projectmember__member=self.request.user,
- project__project_projectmember__is_active=True,
- )
- .filter(project__archived_at__isnull=True)
- .select_related("project", "workspace", "owned_by")
- .prefetch_related(
- Prefetch(
- "issue_cycle__issue__assignees",
- queryset=User.objects.only(
- "avatar", "first_name", "id"
- ).distinct(),
- )
- )
- .prefetch_related(
- Prefetch(
- "issue_cycle__issue__labels",
- queryset=Label.objects.only(
- "name", "color", "id"
- ).distinct(),
- )
- )
- .annotate(is_favorite=Exists(favorite_subquery))
- .annotate(
- total_issues=Count(
- "issue_cycle__issue__id",
- distinct=True,
- filter=Q(
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .annotate(
- completed_issues=Count(
- "issue_cycle__issue__id",
- distinct=True,
- filter=Q(
- issue_cycle__issue__state__group="completed",
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .annotate(
- cancelled_issues=Count(
- "issue_cycle__issue__id",
- distinct=True,
- filter=Q(
- issue_cycle__issue__state__group="cancelled",
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .annotate(
- started_issues=Count(
- "issue_cycle__issue__id",
- distinct=True,
- filter=Q(
- issue_cycle__issue__state__group="started",
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .annotate(
- unstarted_issues=Count(
- "issue_cycle__issue__id",
- distinct=True,
- filter=Q(
- issue_cycle__issue__state__group="unstarted",
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .annotate(
- backlog_issues=Count(
- "issue_cycle__issue__id",
- distinct=True,
- filter=Q(
- issue_cycle__issue__state__group="backlog",
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .annotate(
- status=Case(
- When(
- Q(start_date__lte=timezone.now())
- & Q(end_date__gte=timezone.now()),
- then=Value("CURRENT"),
- ),
- When(
- start_date__gt=timezone.now(), then=Value("UPCOMING")
- ),
- When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
- When(
- Q(start_date__isnull=True) & Q(end_date__isnull=True),
- then=Value("DRAFT"),
- ),
- default=Value("DRAFT"),
- output_field=CharField(),
- )
- )
- .annotate(
- assignee_ids=Coalesce(
- ArrayAgg(
- "issue_cycle__issue__assignees__id",
- distinct=True,
- filter=~Q(
- issue_cycle__issue__assignees__id__isnull=True
- ),
- ),
- Value([], output_field=ArrayField(UUIDField())),
- )
- )
- .order_by("-is_favorite", "name")
- .distinct()
- )
-
- def get(self, request, slug, project_id, pk=None):
- if pk is None:
- queryset = (
- self.get_queryset()
- .annotate(
- total_issues=Count(
- "issue_cycle",
- filter=Q(
- issue_cycle__issue__archived_at__isnull=True,
- issue_cycle__issue__is_draft=False,
- ),
- )
- )
- .values(
- # necessary fields
- "id",
- "workspace_id",
- "project_id",
- # model fields
- "name",
- "description",
- "start_date",
- "end_date",
- "owned_by_id",
- "view_props",
- "sort_order",
- "external_source",
- "external_id",
- "progress_snapshot",
- # meta fields
- "total_issues",
- "is_favorite",
- "cancelled_issues",
- "completed_issues",
- "started_issues",
- "unstarted_issues",
- "backlog_issues",
- "assignee_ids",
- "status",
- "archived_at",
- )
- ).order_by("-is_favorite", "-created_at")
- return Response(queryset, status=status.HTTP_200_OK)
- else:
- queryset = (
- self.get_queryset()
- .filter(archived_at__isnull=False)
- .filter(pk=pk)
- )
- data = (
- self.get_queryset()
- .filter(pk=pk)
- .annotate(
- sub_issues=Issue.issue_objects.filter(
- project_id=self.kwargs.get("project_id"),
- parent__isnull=False,
- issue_cycle__cycle_id=pk,
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- .values(
- # necessary fields
- "id",
- "workspace_id",
- "project_id",
- # model fields
- "name",
- "description",
- "start_date",
- "end_date",
- "owned_by_id",
- "view_props",
- "sort_order",
- "external_source",
- "external_id",
- "progress_snapshot",
- "sub_issues",
- # meta fields
- "is_favorite",
- "total_issues",
- "cancelled_issues",
- "completed_issues",
- "started_issues",
- "unstarted_issues",
- "backlog_issues",
- "assignee_ids",
- "status",
- )
- .first()
- )
- queryset = queryset.first()
-
- if data is None:
- return Response(
- {"error": "Cycle does not exist"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- # Assignee Distribution
- assignee_distribution = (
- Issue.objects.filter(
- issue_cycle__cycle_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(first_name=F("assignees__first_name"))
- .annotate(last_name=F("assignees__last_name"))
- .annotate(assignee_id=F("assignees__id"))
- .annotate(avatar=F("assignees__avatar"))
- .annotate(display_name=F("assignees__display_name"))
- .values(
- "first_name",
- "last_name",
- "assignee_id",
- "avatar",
- "display_name",
- )
- .annotate(
- total_issues=Count(
- "id",
- filter=Q(archived_at__isnull=True, is_draft=False),
- ),
- )
- .annotate(
- completed_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("first_name", "last_name")
- )
-
- # Label Distribution
- label_distribution = (
- Issue.objects.filter(
- issue_cycle__cycle_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(label_name=F("labels__name"))
- .annotate(color=F("labels__color"))
- .annotate(label_id=F("labels__id"))
- .values("label_name", "color", "label_id")
- .annotate(
- total_issues=Count(
- "id",
- filter=Q(archived_at__isnull=True, is_draft=False),
- ),
- )
- .annotate(
- completed_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("label_name")
- )
-
- data["distribution"] = {
- "assignees": assignee_distribution,
- "labels": label_distribution,
- "completion_chart": {},
- }
-
- if queryset.start_date and queryset.end_date:
- data["distribution"]["completion_chart"] = burndown_plot(
- queryset=queryset,
- slug=slug,
- project_id=project_id,
- cycle_id=pk,
- )
-
- return Response(
- data,
- status=status.HTTP_200_OK,
- )
-
- def post(self, request, slug, project_id, cycle_id):
- cycle = Cycle.objects.get(
- pk=cycle_id, project_id=project_id, workspace__slug=slug
- )
-
- if cycle.end_date >= timezone.now().date():
- return Response(
- {"error": "Only completed cycles can be archived"},
- status=status.HTTP_400_BAD_REQUEST,
- )
-
- cycle.archived_at = timezone.now()
- cycle.save()
- return Response(
- {"archived_at": str(cycle.archived_at)},
- status=status.HTTP_200_OK,
- )
-
- def delete(self, request, slug, project_id, cycle_id):
- cycle = Cycle.objects.get(
- pk=cycle_id, project_id=project_id, workspace__slug=slug
- )
- cycle.archived_at = None
- cycle.save()
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
class CycleDateCheckEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py
index 33b3cf9d5..9558348d9 100644
--- a/apiserver/plane/app/views/dashboard/base.py
+++ b/apiserver/plane/app/views/dashboard/base.py
@@ -571,14 +571,16 @@ def dashboard_recent_collaborators(self, request, slug):
return self.paginate(
request=request,
queryset=project_members_with_activities,
- controller=self.get_results_controller,
+ controller=lambda qs: self.get_results_controller(qs, slug),
)
class DashboardEndpoint(BaseAPIView):
- def get_results_controller(self, project_members_with_activities):
+ def get_results_controller(self, project_members_with_activities, slug):
user_active_issue_counts = (
- User.objects.filter(id__in=project_members_with_activities)
+ User.objects.filter(
+ id__in=project_members_with_activities,
+ )
.annotate(
active_issue_count=Count(
Case(
@@ -587,10 +589,13 @@ class DashboardEndpoint(BaseAPIView):
"unstarted",
"started",
],
- then=1,
+ issue_assignee__issue__workspace__slug=slug,
+ issue_assignee__issue__project__project_projectmember__is_active=True,
+ then=F("issue_assignee__issue__id"),
),
output_field=IntegerField(),
- )
+ ),
+ distinct=True,
)
)
.values("active_issue_count", user_id=F("id"))
diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py
index 62a0aa25c..077d7dcaf 100644
--- a/apiserver/plane/app/views/issue/draft.py
+++ b/apiserver/plane/app/views/issue/draft.py
@@ -1,6 +1,7 @@
# Python imports
import json
+# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.core.serializers.json import DjangoJSONEncoder
@@ -19,14 +20,12 @@ from django.db.models import (
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 import status
from rest_framework.response import Response
from plane.app.permissions import ProjectEntityPermission
diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py
new file mode 100644
index 000000000..9c0b6cca3
--- /dev/null
+++ b/apiserver/plane/app/views/module/archive.py
@@ -0,0 +1,356 @@
+from django.contrib.postgres.aggregates import ArrayAgg
+from django.contrib.postgres.fields import ArrayField
+from django.db.models import (
+ Count,
+ Exists,
+ F,
+ Func,
+ IntegerField,
+ OuterRef,
+ Prefetch,
+ Q,
+ Subquery,
+ UUIDField,
+ Value,
+)
+from django.db.models.functions import Coalesce
+from django.utils import timezone
+
+# Third party imports
+from rest_framework import status
+from rest_framework.response import Response
+from plane.app.permissions import (
+ ProjectEntityPermission,
+)
+from plane.app.serializers import (
+ ModuleDetailSerializer,
+)
+from plane.db.models import (
+ Issue,
+ Module,
+ ModuleFavorite,
+ ModuleLink,
+)
+from plane.utils.analytics_plot import burndown_plot
+
+# Module imports
+from .. import BaseAPIView
+
+
+class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
+
+ permission_classes = [
+ ProjectEntityPermission,
+ ]
+
+ def get_queryset(self):
+ favorite_subquery = ModuleFavorite.objects.filter(
+ user=self.request.user,
+ module_id=OuterRef("pk"),
+ project_id=self.kwargs.get("project_id"),
+ workspace__slug=self.kwargs.get("slug"),
+ )
+ cancelled_issues = (
+ Issue.issue_objects.filter(
+ state__group="cancelled",
+ issue_module__module_id=OuterRef("pk"),
+ )
+ .values("issue_module__module_id")
+ .annotate(cnt=Count("pk"))
+ .values("cnt")
+ )
+ completed_issues = (
+ Issue.issue_objects.filter(
+ state__group="completed",
+ issue_module__module_id=OuterRef("pk"),
+ )
+ .values("issue_module__module_id")
+ .annotate(cnt=Count("pk"))
+ .values("cnt")
+ )
+ started_issues = (
+ Issue.issue_objects.filter(
+ state__group="started",
+ issue_module__module_id=OuterRef("pk"),
+ )
+ .values("issue_module__module_id")
+ .annotate(cnt=Count("pk"))
+ .values("cnt")
+ )
+ unstarted_issues = (
+ Issue.issue_objects.filter(
+ state__group="unstarted",
+ issue_module__module_id=OuterRef("pk"),
+ )
+ .values("issue_module__module_id")
+ .annotate(cnt=Count("pk"))
+ .values("cnt")
+ )
+ backlog_issues = (
+ Issue.issue_objects.filter(
+ state__group="backlog",
+ issue_module__module_id=OuterRef("pk"),
+ )
+ .values("issue_module__module_id")
+ .annotate(cnt=Count("pk"))
+ .values("cnt")
+ )
+ total_issues = (
+ Issue.issue_objects.filter(
+ issue_module__module_id=OuterRef("pk"),
+ )
+ .values("issue_module__module_id")
+ .annotate(cnt=Count("pk"))
+ .values("cnt")
+ )
+ return (
+ Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
+ .filter(archived_at__isnull=False)
+ .annotate(is_favorite=Exists(favorite_subquery))
+ .select_related("workspace", "project", "lead")
+ .prefetch_related("members")
+ .prefetch_related(
+ Prefetch(
+ "link_module",
+ queryset=ModuleLink.objects.select_related(
+ "module", "created_by"
+ ),
+ )
+ )
+ .annotate(
+ completed_issues=Coalesce(
+ Subquery(completed_issues[:1]),
+ Value(0, output_field=IntegerField()),
+ )
+ )
+ .annotate(
+ cancelled_issues=Coalesce(
+ Subquery(cancelled_issues[:1]),
+ Value(0, output_field=IntegerField()),
+ )
+ )
+ .annotate(
+ started_issues=Coalesce(
+ Subquery(started_issues[:1]),
+ Value(0, output_field=IntegerField()),
+ )
+ )
+ .annotate(
+ unstarted_issues=Coalesce(
+ Subquery(unstarted_issues[:1]),
+ Value(0, output_field=IntegerField()),
+ )
+ )
+ .annotate(
+ backlog_issues=Coalesce(
+ Subquery(backlog_issues[:1]),
+ Value(0, output_field=IntegerField()),
+ )
+ )
+ .annotate(
+ total_issues=Coalesce(
+ Subquery(total_issues[:1]),
+ Value(0, output_field=IntegerField()),
+ )
+ )
+ .annotate(
+ member_ids=Coalesce(
+ ArrayAgg(
+ "members__id",
+ distinct=True,
+ filter=~Q(members__id__isnull=True),
+ ),
+ Value([], output_field=ArrayField(UUIDField())),
+ )
+ )
+ .order_by("-is_favorite", "-created_at")
+ )
+
+ def get(self, request, slug, project_id, pk=None):
+ if pk is None:
+ queryset = self.get_queryset()
+ modules = queryset.values( # Required fields
+ "id",
+ "workspace_id",
+ "project_id",
+ # Model fields
+ "name",
+ "description",
+ "description_text",
+ "description_html",
+ "start_date",
+ "target_date",
+ "status",
+ "lead_id",
+ "member_ids",
+ "view_props",
+ "sort_order",
+ "external_source",
+ "external_id",
+ # computed fields
+ "total_issues",
+ "is_favorite",
+ "cancelled_issues",
+ "completed_issues",
+ "started_issues",
+ "unstarted_issues",
+ "backlog_issues",
+ "created_at",
+ "updated_at",
+ "archived_at",
+ )
+ return Response(modules, status=status.HTTP_200_OK)
+ else:
+ queryset = (
+ self.get_queryset()
+ .filter(pk=pk)
+ .annotate(
+ sub_issues=Issue.issue_objects.filter(
+ project_id=self.kwargs.get("project_id"),
+ parent__isnull=False,
+ issue_module__module_id=pk,
+ )
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ )
+ assignee_distribution = (
+ Issue.objects.filter(
+ issue_module__module_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(first_name=F("assignees__first_name"))
+ .annotate(last_name=F("assignees__last_name"))
+ .annotate(assignee_id=F("assignees__id"))
+ .annotate(display_name=F("assignees__display_name"))
+ .annotate(avatar=F("assignees__avatar"))
+ .values(
+ "first_name",
+ "last_name",
+ "assignee_id",
+ "avatar",
+ "display_name",
+ )
+ .annotate(
+ total_issues=Count(
+ "id",
+ filter=Q(
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ completed_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=False,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=True,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .order_by("first_name", "last_name")
+ )
+
+ label_distribution = (
+ Issue.objects.filter(
+ issue_module__module_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(label_name=F("labels__name"))
+ .annotate(color=F("labels__color"))
+ .annotate(label_id=F("labels__id"))
+ .values("label_name", "color", "label_id")
+ .annotate(
+ total_issues=Count(
+ "id",
+ filter=Q(
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ ),
+ )
+ .annotate(
+ completed_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=False,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "id",
+ filter=Q(
+ completed_at__isnull=True,
+ archived_at__isnull=True,
+ is_draft=False,
+ ),
+ )
+ )
+ .order_by("label_name")
+ )
+
+ data = ModuleDetailSerializer(queryset.first()).data
+ data["distribution"] = {
+ "assignees": assignee_distribution,
+ "labels": label_distribution,
+ "completion_chart": {},
+ }
+
+ # Fetch the modules
+ modules = queryset.first()
+ if modules and modules.start_date and modules.target_date:
+ data["distribution"]["completion_chart"] = burndown_plot(
+ queryset=modules,
+ slug=slug,
+ project_id=project_id,
+ module_id=pk,
+ )
+
+ return Response(
+ data,
+ status=status.HTTP_200_OK,
+ )
+
+ def post(self, request, slug, project_id, module_id):
+ module = Module.objects.get(
+ pk=module_id, project_id=project_id, workspace__slug=slug
+ )
+ if module.status not in ["completed", "cancelled"]:
+ return Response(
+ {
+ "error": "Only completed or cancelled modules can be archived"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ module.archived_at = timezone.now()
+ module.save()
+ return Response(
+ {"archived_at": str(module.archived_at)},
+ status=status.HTTP_200_OK,
+ )
+
+ def delete(self, request, slug, project_id, module_id):
+ module = Module.objects.get(
+ pk=module_id, project_id=project_id, workspace__slug=slug
+ )
+ module.archived_at = None
+ module.save()
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py
index aaaf8fb67..4cd52b3b1 100644
--- a/apiserver/plane/app/views/module/base.py
+++ b/apiserver/plane/app/views/module/base.py
@@ -515,325 +515,6 @@ class ModuleLinkViewSet(BaseViewSet):
)
-class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
-
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def get_queryset(self):
- favorite_subquery = ModuleFavorite.objects.filter(
- user=self.request.user,
- module_id=OuterRef("pk"),
- project_id=self.kwargs.get("project_id"),
- workspace__slug=self.kwargs.get("slug"),
- )
- cancelled_issues = (
- Issue.issue_objects.filter(
- state__group="cancelled",
- issue_module__module_id=OuterRef("pk"),
- )
- .values("issue_module__module_id")
- .annotate(cnt=Count("pk"))
- .values("cnt")
- )
- completed_issues = (
- Issue.issue_objects.filter(
- state__group="completed",
- issue_module__module_id=OuterRef("pk"),
- )
- .values("issue_module__module_id")
- .annotate(cnt=Count("pk"))
- .values("cnt")
- )
- started_issues = (
- Issue.issue_objects.filter(
- state__group="started",
- issue_module__module_id=OuterRef("pk"),
- )
- .values("issue_module__module_id")
- .annotate(cnt=Count("pk"))
- .values("cnt")
- )
- unstarted_issues = (
- Issue.issue_objects.filter(
- state__group="unstarted",
- issue_module__module_id=OuterRef("pk"),
- )
- .values("issue_module__module_id")
- .annotate(cnt=Count("pk"))
- .values("cnt")
- )
- backlog_issues = (
- Issue.issue_objects.filter(
- state__group="backlog",
- issue_module__module_id=OuterRef("pk"),
- )
- .values("issue_module__module_id")
- .annotate(cnt=Count("pk"))
- .values("cnt")
- )
- total_issues = (
- Issue.issue_objects.filter(
- issue_module__module_id=OuterRef("pk"),
- )
- .values("issue_module__module_id")
- .annotate(cnt=Count("pk"))
- .values("cnt")
- )
- return (
- Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
- .filter(archived_at__isnull=False)
- .annotate(is_favorite=Exists(favorite_subquery))
- .select_related("workspace", "project", "lead")
- .prefetch_related("members")
- .prefetch_related(
- Prefetch(
- "link_module",
- queryset=ModuleLink.objects.select_related(
- "module", "created_by"
- ),
- )
- )
- .annotate(
- completed_issues=Coalesce(
- Subquery(completed_issues[:1]),
- Value(0, output_field=IntegerField()),
- )
- )
- .annotate(
- cancelled_issues=Coalesce(
- Subquery(cancelled_issues[:1]),
- Value(0, output_field=IntegerField()),
- )
- )
- .annotate(
- started_issues=Coalesce(
- Subquery(started_issues[:1]),
- Value(0, output_field=IntegerField()),
- )
- )
- .annotate(
- unstarted_issues=Coalesce(
- Subquery(unstarted_issues[:1]),
- Value(0, output_field=IntegerField()),
- )
- )
- .annotate(
- backlog_issues=Coalesce(
- Subquery(backlog_issues[:1]),
- Value(0, output_field=IntegerField()),
- )
- )
- .annotate(
- total_issues=Coalesce(
- Subquery(total_issues[:1]),
- Value(0, output_field=IntegerField()),
- )
- )
- .annotate(
- member_ids=Coalesce(
- ArrayAgg(
- "members__id",
- distinct=True,
- filter=~Q(members__id__isnull=True),
- ),
- Value([], output_field=ArrayField(UUIDField())),
- )
- )
- .order_by("-is_favorite", "-created_at")
- )
-
- def get(self, request, slug, project_id, pk=None):
- if pk is None:
- queryset = self.get_queryset()
- modules = queryset.values( # Required fields
- "id",
- "workspace_id",
- "project_id",
- # Model fields
- "name",
- "description",
- "description_text",
- "description_html",
- "start_date",
- "target_date",
- "status",
- "lead_id",
- "member_ids",
- "view_props",
- "sort_order",
- "external_source",
- "external_id",
- # computed fields
- "total_issues",
- "is_favorite",
- "cancelled_issues",
- "completed_issues",
- "started_issues",
- "unstarted_issues",
- "backlog_issues",
- "created_at",
- "updated_at",
- "archived_at",
- )
- return Response(modules, status=status.HTTP_200_OK)
- else:
- queryset = (
- self.get_queryset()
- .filter(pk=pk)
- .annotate(
- sub_issues=Issue.issue_objects.filter(
- project_id=self.kwargs.get("project_id"),
- parent__isnull=False,
- issue_module__module_id=pk,
- )
- .order_by()
- .annotate(count=Func(F("id"), function="Count"))
- .values("count")
- )
- )
- assignee_distribution = (
- Issue.objects.filter(
- issue_module__module_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(first_name=F("assignees__first_name"))
- .annotate(last_name=F("assignees__last_name"))
- .annotate(assignee_id=F("assignees__id"))
- .annotate(display_name=F("assignees__display_name"))
- .annotate(avatar=F("assignees__avatar"))
- .values(
- "first_name",
- "last_name",
- "assignee_id",
- "avatar",
- "display_name",
- )
- .annotate(
- total_issues=Count(
- "id",
- filter=Q(
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- completed_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("first_name", "last_name")
- )
-
- label_distribution = (
- Issue.objects.filter(
- issue_module__module_id=pk,
- workspace__slug=slug,
- project_id=project_id,
- )
- .annotate(label_name=F("labels__name"))
- .annotate(color=F("labels__color"))
- .annotate(label_id=F("labels__id"))
- .values("label_name", "color", "label_id")
- .annotate(
- total_issues=Count(
- "id",
- filter=Q(
- archived_at__isnull=True,
- is_draft=False,
- ),
- ),
- )
- .annotate(
- completed_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=False,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .annotate(
- pending_issues=Count(
- "id",
- filter=Q(
- completed_at__isnull=True,
- archived_at__isnull=True,
- is_draft=False,
- ),
- )
- )
- .order_by("label_name")
- )
-
- data = ModuleDetailSerializer(queryset.first()).data
- data["distribution"] = {
- "assignees": assignee_distribution,
- "labels": label_distribution,
- "completion_chart": {},
- }
-
- # Fetch the modules
- modules = queryset.first()
- if modules and modules.start_date and modules.target_date:
- data["distribution"]["completion_chart"] = burndown_plot(
- queryset=modules,
- slug=slug,
- project_id=project_id,
- module_id=pk,
- )
-
- return Response(
- data,
- status=status.HTTP_200_OK,
- )
-
- def post(self, request, slug, project_id, module_id):
- module = Module.objects.get(
- pk=module_id, project_id=project_id, workspace__slug=slug
- )
- if module.status not in ["completed", "cancelled"]:
- return Response(
- {
- "error": "Only completed or cancelled modules can be archived"
- },
- status=status.HTTP_400_BAD_REQUEST,
- )
- module.archived_at = timezone.now()
- module.save()
- return Response(
- {"archived_at": str(module.archived_at)},
- status=status.HTTP_200_OK,
- )
-
- def delete(self, request, slug, project_id, module_id):
- module = Module.objects.get(
- pk=module_id, project_id=project_id, workspace__slug=slug
- )
- module.archived_at = None
- module.save()
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
diff --git a/apiserver/plane/app/views/user/base.py b/apiserver/plane/app/views/user/base.py
index 4d69d1cf2..487e365cd 100644
--- a/apiserver/plane/app/views/user/base.py
+++ b/apiserver/plane/app/views/user/base.py
@@ -49,7 +49,12 @@ class UserEndpoint(BaseViewSet):
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
)
- @invalidate_cache(path="/api/users/me/")
+ @invalidate_cache(
+ path="/api/users/me/",
+ )
+ @invalidate_cache(
+ path="/api/users/me/settings/",
+ )
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
diff --git a/apiserver/plane/db/management/commands/test_email.py b/apiserver/plane/db/management/commands/test_email.py
index 99c5d9684..63b602518 100644
--- a/apiserver/plane/db/management/commands/test_email.py
+++ b/apiserver/plane/db/management/commands/test_email.py
@@ -15,7 +15,7 @@ class Command(BaseCommand):
receiver_email = options.get("to_email")
if not receiver_email:
- raise CommandError("Reciever email is required")
+ raise CommandError("Receiver email is required")
(
EMAIL_HOST,
@@ -54,7 +54,7 @@ class Command(BaseCommand):
connection=connection,
)
msg.send()
- self.stdout.write(self.style.SUCCESS("Email succesfully sent"))
+ self.stdout.write(self.style.SUCCESS("Email successfully sent"))
except Exception as e:
self.stdout.write(
self.style.ERROR(
diff --git a/packages/ui/src/form-fields/textarea.tsx b/packages/ui/src/form-fields/textarea.tsx
index de225d68f..2c47a65f5 100644
--- a/packages/ui/src/form-fields/textarea.tsx
+++ b/packages/ui/src/form-fields/textarea.tsx
@@ -11,17 +11,7 @@ export interface TextAreaProps extends React.TextareaHTMLAttributes
No matches found
diff --git a/web/components/cycles/dropdowns/filters/start-date.tsx b/web/components/cycles/dropdowns/filters/start-date.tsx index 2b55ada35..0b8408c2e 100644 --- a/web/components/cycles/dropdowns/filters/start-date.tsx +++ b/web/components/cycles/dropdowns/filters/start-date.tsx @@ -6,6 +6,8 @@ import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // constants import { DATE_AFTER_FILTER_OPTIONS } from "@/constants/filters"; +// helpers +import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; @@ -25,6 +27,17 @@ export const FilterStartDate: React.FCNo matches found
diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index d16e31f26..cfc3db4bf 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -417,7 +417,7 @@ export const PriorityDropdown: React.FC