forked from github/plane
[WEB-989] chore: archived module and cycle sidebar details (#4191)
* chore: module and cycle archive * fix: archived cycle sidebar details fetch * fix: archived module sidebar details fetch --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
7a4ee509da
commit
54196aafc0
@ -34,7 +34,7 @@ urlpatterns = [
|
|||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/archive/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/archive/",
|
||||||
CycleArchiveUnarchiveAPIEndpoint.as_view(),
|
CycleArchiveUnarchiveAPIEndpoint.as_view(),
|
||||||
name="cycle-archive-unarchive",
|
name="cycle-archive-unarchive",
|
||||||
),
|
),
|
||||||
|
@ -154,9 +154,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
data,
|
data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
queryset = (
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
self.get_queryset().filter(archived_at__isnull=True)
|
|
||||||
)
|
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
|
||||||
# Current Cycle
|
# Current Cycle
|
||||||
@ -495,17 +493,22 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request, slug, project_id, pk):
|
def post(self, request, slug, project_id, cycle_id):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug
|
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.archived_at = timezone.now()
|
||||||
cycle.save()
|
cycle.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, cycle_id):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug
|
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||||
)
|
)
|
||||||
cycle.archived_at = None
|
cycle.archived_at = None
|
||||||
cycle.save()
|
cycle.save()
|
||||||
|
@ -554,7 +554,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id, pk):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(self.get_queryset()),
|
queryset=(self.get_queryset()),
|
||||||
@ -570,6 +570,13 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug
|
pk=pk, 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.archived_at = timezone.now()
|
||||||
module.save()
|
module.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -101,4 +101,9 @@ urlpatterns = [
|
|||||||
CycleArchiveUnarchiveEndpoint.as_view(),
|
CycleArchiveUnarchiveEndpoint.as_view(),
|
||||||
name="cycle-archive-unarchive",
|
name="cycle-archive-unarchive",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-cycles/<uuid:pk>/",
|
||||||
|
CycleArchiveUnarchiveEndpoint.as_view(),
|
||||||
|
name="cycle-archive-unarchive",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -121,4 +121,9 @@ urlpatterns = [
|
|||||||
ModuleArchiveUnarchiveEndpoint.as_view(),
|
ModuleArchiveUnarchiveEndpoint.as_view(),
|
||||||
name="module-archive-unarchive",
|
name="module-archive-unarchive",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-modules/<uuid:pk>/",
|
||||||
|
ModuleArchiveUnarchiveEndpoint.as_view(),
|
||||||
|
name="module-archive-unarchive",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -726,9 +726,20 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(is_favorite=Exists(favorite_subquery))
|
.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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__id",
|
||||||
|
distinct=True,
|
||||||
filter=Q(
|
filter=Q(
|
||||||
issue_cycle__issue__state__group="completed",
|
issue_cycle__issue__state__group="completed",
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
@ -738,7 +749,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__id",
|
||||||
|
distinct=True,
|
||||||
filter=Q(
|
filter=Q(
|
||||||
issue_cycle__issue__state__group="cancelled",
|
issue_cycle__issue__state__group="cancelled",
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
@ -748,7 +760,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__id",
|
||||||
|
distinct=True,
|
||||||
filter=Q(
|
filter=Q(
|
||||||
issue_cycle__issue__state__group="started",
|
issue_cycle__issue__state__group="started",
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
@ -758,7 +771,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__id",
|
||||||
|
distinct=True,
|
||||||
filter=Q(
|
filter=Q(
|
||||||
issue_cycle__issue__state__group="unstarted",
|
issue_cycle__issue__state__group="unstarted",
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
@ -768,7 +782,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__id",
|
||||||
|
distinct=True,
|
||||||
filter=Q(
|
filter=Q(
|
||||||
issue_cycle__issue__state__group="backlog",
|
issue_cycle__issue__state__group="backlog",
|
||||||
issue_cycle__issue__archived_at__isnull=True,
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
@ -802,9 +817,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
distinct=True,
|
distinct=True,
|
||||||
filter=~Q(
|
filter=~Q(
|
||||||
issue_cycle__issue__assignees__id__isnull=True
|
issue_cycle__issue__assignees__id__isnull=True
|
||||||
)
|
|
||||||
& Q(
|
|
||||||
issue_cycle__issue__assignees__member_project__is_active=True
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
@ -814,7 +826,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
|
if pk is None:
|
||||||
queryset = (
|
queryset = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -856,11 +869,181 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
).order_by("-is_favorite", "-created_at")
|
).order_by("-is_favorite", "-created_at")
|
||||||
return Response(queryset, status=status.HTTP_200_OK)
|
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):
|
def post(self, request, slug, project_id, cycle_id):
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
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.archived_at = timezone.now()
|
||||||
cycle.save()
|
cycle.save()
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -528,13 +528,64 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
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 (
|
return (
|
||||||
Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(archived_at__isnull=False)
|
.filter(archived_at__isnull=False)
|
||||||
.annotate(is_favorite=Exists(favorite_subquery))
|
.annotate(is_favorite=Exists(favorite_subquery))
|
||||||
.select_related("project")
|
.select_related("workspace", "project", "lead")
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("lead")
|
|
||||||
.prefetch_related("members")
|
.prefetch_related("members")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
@ -545,68 +596,39 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
completed_issues=Coalesce(
|
||||||
"issue_module",
|
Subquery(completed_issues[:1]),
|
||||||
filter=Q(
|
Value(0, output_field=IntegerField()),
|
||||||
issue_module__issue__archived_at__isnull=True,
|
|
||||||
issue_module__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
distinct=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
completed_issues=Count(
|
|
||||||
"issue_module__issue__state__group",
|
|
||||||
filter=Q(
|
|
||||||
issue_module__issue__state__group="completed",
|
|
||||||
issue_module__issue__archived_at__isnull=True,
|
|
||||||
issue_module__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
distinct=True,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Coalesce(
|
||||||
"issue_module__issue__state__group",
|
Subquery(cancelled_issues[:1]),
|
||||||
filter=Q(
|
Value(0, output_field=IntegerField()),
|
||||||
issue_module__issue__state__group="cancelled",
|
|
||||||
issue_module__issue__archived_at__isnull=True,
|
|
||||||
issue_module__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
distinct=True,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Coalesce(
|
||||||
"issue_module__issue__state__group",
|
Subquery(started_issues[:1]),
|
||||||
filter=Q(
|
Value(0, output_field=IntegerField()),
|
||||||
issue_module__issue__state__group="started",
|
|
||||||
issue_module__issue__archived_at__isnull=True,
|
|
||||||
issue_module__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
distinct=True,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Coalesce(
|
||||||
"issue_module__issue__state__group",
|
Subquery(unstarted_issues[:1]),
|
||||||
filter=Q(
|
Value(0, output_field=IntegerField()),
|
||||||
issue_module__issue__state__group="unstarted",
|
|
||||||
issue_module__issue__archived_at__isnull=True,
|
|
||||||
issue_module__issue__is_draft=False,
|
|
||||||
),
|
|
||||||
distinct=True,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Coalesce(
|
||||||
"issue_module__issue__state__group",
|
Subquery(backlog_issues[:1]),
|
||||||
filter=Q(
|
Value(0, output_field=IntegerField()),
|
||||||
issue_module__issue__state__group="backlog",
|
)
|
||||||
issue_module__issue__archived_at__isnull=True,
|
)
|
||||||
issue_module__issue__is_draft=False,
|
.annotate(
|
||||||
),
|
total_issues=Coalesce(
|
||||||
distinct=True,
|
Subquery(total_issues[:1]),
|
||||||
|
Value(0, output_field=IntegerField()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -622,7 +644,8 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
.order_by("-is_favorite", "-created_at")
|
.order_by("-is_favorite", "-created_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
|
if pk is None:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
modules = queryset.values( # Required fields
|
modules = queryset.values( # Required fields
|
||||||
"id",
|
"id",
|
||||||
@ -655,11 +678,146 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
"archived_at",
|
"archived_at",
|
||||||
)
|
)
|
||||||
return Response(modules, status=status.HTTP_200_OK)
|
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):
|
def post(self, request, slug, project_id, module_id):
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
pk=module_id, project_id=project_id, workspace__slug=slug
|
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.archived_at = timezone.now()
|
||||||
module.save()
|
module.save()
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -19,7 +19,7 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
|
|||||||
// refs
|
// refs
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchCycleDetails } = useCycle();
|
const { fetchCycleDetails, fetchArchivedCycleDetails } = useCycle();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
delete router.query.peekCycle;
|
delete router.query.peekCycle;
|
||||||
@ -30,9 +30,10 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peekCycle || isArchived) return;
|
if (!peekCycle) return;
|
||||||
fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
||||||
}, [fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
|
else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
|
||||||
|
}, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -19,7 +19,7 @@ export const ModulePeekOverview: React.FC<Props> = observer(({ projectId, worksp
|
|||||||
// refs
|
// refs
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchModuleDetails } = useModule();
|
const { fetchModuleDetails, fetchArchivedModuleDetails } = useModule();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
delete router.query.peekModule;
|
delete router.query.peekModule;
|
||||||
@ -30,10 +30,10 @@ export const ModulePeekOverview: React.FC<Props> = observer(({ projectId, worksp
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peekModule || isArchived) return;
|
if (!peekModule) return;
|
||||||
|
if (isArchived) fetchArchivedModuleDetails(workspaceSlug, projectId, peekModule.toString());
|
||||||
fetchModuleDetails(workspaceSlug, projectId, peekModule.toString());
|
else fetchModuleDetails(workspaceSlug, projectId, peekModule.toString());
|
||||||
}, [fetchModuleDetails, isArchived, peekModule, projectId, workspaceSlug]);
|
}, [fetchArchivedModuleDetails, fetchModuleDetails, isArchived, peekModule, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -18,6 +18,14 @@ export class CycleArchiveService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getArchivedCycleDetails(workspaceSlug: string, projectId: string, cycleId: string): Promise<ICycle> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-cycles/${cycleId}/`)
|
||||||
|
.then((res) => res?.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async archiveCycle(
|
async archiveCycle(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -18,6 +18,14 @@ export class ModuleArchiveService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getArchivedModuleDetails(workspaceSlug: string, projectId: string, moduleId: string): Promise<IModule> {
|
||||||
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-modules/${moduleId}/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async archiveModule(
|
async archiveModule(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -46,6 +46,7 @@ export interface ICycleStore {
|
|||||||
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
||||||
fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
||||||
fetchArchivedCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
fetchArchivedCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
||||||
|
fetchArchivedCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
||||||
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
||||||
// crud
|
// crud
|
||||||
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
|
createCycle: (workspaceSlug: string, projectId: string, data: Partial<ICycle>) => Promise<ICycle>;
|
||||||
@ -99,6 +100,7 @@ export class CycleStore implements ICycleStore {
|
|||||||
fetchAllCycles: action,
|
fetchAllCycles: action,
|
||||||
fetchActiveCycle: action,
|
fetchActiveCycle: action,
|
||||||
fetchArchivedCycles: action,
|
fetchArchivedCycles: action,
|
||||||
|
fetchArchivedCycleDetails: action,
|
||||||
fetchCycleDetails: action,
|
fetchCycleDetails: action,
|
||||||
createCycle: action,
|
createCycle: action,
|
||||||
updateCycleDetails: action,
|
updateCycleDetails: action,
|
||||||
@ -415,6 +417,21 @@ export class CycleStore implements ICycleStore {
|
|||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description fetches cycle details
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param cycleId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
fetchArchivedCycleDetails = async (workspaceSlug: string, projectId: string, cycleId: string) =>
|
||||||
|
await this.cycleArchiveService.getArchivedCycleDetails(workspaceSlug, projectId, cycleId).then((response) => {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.cycleMap, [response.id], { ...this.cycleMap?.[response.id], ...response });
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description fetches cycle details
|
* @description fetches cycle details
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
@ -33,6 +33,7 @@ export interface IModuleStore {
|
|||||||
fetchWorkspaceModules: (workspaceSlug: string) => Promise<IModule[]>;
|
fetchWorkspaceModules: (workspaceSlug: string) => Promise<IModule[]>;
|
||||||
fetchModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
fetchModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
||||||
fetchArchivedModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
fetchArchivedModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
||||||
|
fetchArchivedModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<IModule>;
|
||||||
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<IModule>;
|
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<IModule>;
|
||||||
// crud
|
// crud
|
||||||
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
|
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
|
||||||
@ -91,6 +92,7 @@ export class ModulesStore implements IModuleStore {
|
|||||||
fetchWorkspaceModules: action,
|
fetchWorkspaceModules: action,
|
||||||
fetchModules: action,
|
fetchModules: action,
|
||||||
fetchArchivedModules: action,
|
fetchArchivedModules: action,
|
||||||
|
fetchArchivedModuleDetails: action,
|
||||||
fetchModuleDetails: action,
|
fetchModuleDetails: action,
|
||||||
createModule: action,
|
createModule: action,
|
||||||
updateModuleDetails: action,
|
updateModuleDetails: action,
|
||||||
@ -274,6 +276,21 @@ export class ModulesStore implements IModuleStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description fetch module details
|
||||||
|
* @param workspaceSlug
|
||||||
|
* @param projectId
|
||||||
|
* @param moduleId
|
||||||
|
* @returns IModule
|
||||||
|
*/
|
||||||
|
fetchArchivedModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string) =>
|
||||||
|
await this.moduleArchiveService.getArchivedModuleDetails(workspaceSlug, projectId, moduleId).then((response) => {
|
||||||
|
runInAction(() => {
|
||||||
|
set(this.moduleMap, [moduleId], response);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description fetch module details
|
* @description fetch module details
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
Loading…
Reference in New Issue
Block a user