# Python imports import json # Django imports from django.core import serializers from django.db.models import F, Func, OuterRef, Q, Subquery from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page # Third party imports from rest_framework import status from rest_framework.response import Response # Module imports from .. import BaseViewSet from plane.app.serializers import ( CycleIssueSerializer, ) from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( Cycle, CycleIssue, Issue, FileAsset, IssueLink, ) 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, SubGroupedOffsetPaginator, ) from plane.app.permissions import allow_permission, ROLE class CycleIssueViewSet(BaseViewSet): serializer_class = CycleIssueSerializer model = CycleIssue webhook_event = "cycle_issue" bulk = True filterset_fields = [ "issue__labels__id", "issue__assignees__id", ] def get_queryset(self): return self.filter_queryset( super() .get_queryset() .annotate( sub_issues_count=Issue.issue_objects.filter( parent=OuterRef("issue_id") ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter( project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, ) .filter(project__archived_at__isnull=True) .filter(cycle_id=self.kwargs.get("cycle_id")) .select_related("project") .select_related("workspace") .select_related("cycle") .select_related("issue", "issue__state", "issue__project") .prefetch_related("issue__assignees", "issue__labels") .distinct() ) @method_decorator(gzip_page) @allow_permission( [ ROLE.ADMIN, ROLE.MEMBER, ] ) def list(self, request, slug, project_id, cycle_id): order_by_param = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") issue_queryset = ( Issue.issue_objects.filter( issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True, ) .filter(project_id=project_id) .filter(workspace__slug=slug) .filter(**filters) .select_related("workspace", "project", "state", "parent") .prefetch_related( "assignees", "labels", "issue_module__module", "issue_cycle__cycle", ) .filter(**filters) .annotate( cycle_id=Subquery( CycleIssue.objects.filter( issue=OuterRef("id"), deleted_at__isnull=True ).values("cycle_id")[:1] ) ) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) .annotate( attachment_count=FileAsset.objects.filter( issue_id=OuterRef("id"), entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) .annotate( sub_issues_count=Issue.issue_objects.filter( parent=OuterRef("id") ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) ) filters = issue_filters(request.query_params, "GET") order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = issue_queryset.filter(**filters) # Issue queryset issue_queryset, order_by_param = order_issue_queryset( issue_queryset=issue_queryset, order_by_param=order_by_param, ) # Group by group_by = request.GET.get("group_by", False) sub_group_by = request.GET.get("sub_group_by", False) # issue queryset issue_queryset = issue_queryset_grouper( queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by, ) if group_by: # Check group and sub group value paginate if sub_group_by: if group_by == sub_group_by: return Response( { "error": "Group by and sub group by cannot have same parameters" }, status=status.HTTP_400_BAD_REQUEST, ) else: # group and sub group pagination return self.paginate( request=request, order_by=order_by_param, queryset=issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by, ), paginator_cls=SubGroupedOffsetPaginator, group_by_fields=issue_group_values( field=group_by, slug=slug, project_id=project_id, filters=filters, ), sub_group_by_fields=issue_group_values( field=sub_group_by, slug=slug, project_id=project_id, filters=filters, ), group_by_field_name=group_by, sub_group_by_field_name=sub_group_by, count_filter=Q( Q(issue_intake__status=1) | Q(issue_intake__status=-1) | Q(issue_intake__status=2) | Q(issue_intake__isnull=True), archived_at__isnull=True, is_draft=False, ), ) # Group Paginate else: # Group paginate return self.paginate( request=request, order_by=order_by_param, queryset=issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by, ), paginator_cls=GroupedOffsetPaginator, group_by_fields=issue_group_values( field=group_by, slug=slug, project_id=project_id, filters=filters, ), group_by_field_name=group_by, count_filter=Q( Q(issue_intake__status=1) | Q(issue_intake__status=-1) | Q(issue_intake__status=2) | Q(issue_intake__isnull=True), archived_at__isnull=True, is_draft=False, ), ) else: # List Paginate return self.paginate( order_by=order_by_param, request=request, queryset=issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), ) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) if not issues: return Response( {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST, ) cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=cycle_id ) if cycle.end_date is not None and cycle.end_date < timezone.now(): return Response( { "error": "The Cycle has already been completed so no new issues can be added" }, status=status.HTTP_400_BAD_REQUEST, ) # Get all CycleIssues already created cycle_issues = list( CycleIssue.objects.filter( ~Q(cycle_id=cycle_id), issue_id__in=issues ) ) existing_issues = [ str(cycle_issue.issue_id) for cycle_issue in cycle_issues ] new_issues = list(set(issues) - set(existing_issues)) # New issues to create created_records = CycleIssue.objects.bulk_create( [ CycleIssue( project_id=project_id, workspace_id=cycle.workspace_id, created_by_id=request.user.id, updated_by_id=request.user.id, cycle_id=cycle_id, issue_id=issue, ) for issue in new_issues ], batch_size=10, ) # Updated Issues updated_records = [] update_cycle_issue_activity = [] # Iterate over each cycle_issue in cycle_issues for cycle_issue in cycle_issues: old_cycle_id = cycle_issue.cycle_id # Update the cycle_issue's cycle_id cycle_issue.cycle_id = cycle_id # Add the modified cycle_issue to the records_to_update list updated_records.append(cycle_issue) # Record the update activity update_cycle_issue_activity.append( { "old_cycle_id": str(old_cycle_id), "new_cycle_id": str(cycle_id), "issue_id": str(cycle_issue.issue_id), } ) # Update the cycle issues CycleIssue.objects.bulk_update( updated_records, ["cycle_id"], batch_size=100 ) # Capture Issue Activity issue_activity.delay( type="cycle.activity.created", requested_data=json.dumps({"cycles_list": issues}), actor_id=str(self.request.user.id), issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=json.dumps( { "updated_cycle_issues": update_cycle_issue_activity, "created_cycle_issues": serializers.serialize( "json", created_records ), } ), epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), ) return Response({"message": "success"}, status=status.HTTP_201_CREATED) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.filter( issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id, ) issue_activity.delay( type="cycle.activity.deleted", requested_data=json.dumps( { "cycle_id": str(self.kwargs.get("cycle_id")), "issues": [str(issue_id)], } ), actor_id=str(self.request.user.id), issue_id=str(issue_id), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), ) cycle_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT)