# Copyright (c) 2023-present Plane Software, Inc. and contributors # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. # Python imports import copy import json from django.db.models import F, Func, OuterRef, Q, Subquery # Django Imports 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 from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import ModuleIssueSerializer from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( Issue, FileAsset, IssueLink, ModuleIssue, Project, CycleIssue, ) 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.utils.filters import ComplexFilterBackend from plane.utils.filters import IssueFilterSet from .. import BaseViewSet from plane.utils.host import base_host class ModuleIssueViewSet(BaseViewSet): serializer_class = ModuleIssueSerializer model = ModuleIssue webhook_event = "module_issue" bulk = True filter_backends = (ComplexFilterBackend,) filterset_class = IssueFilterSet def apply_annotations(self, issues): return ( issues.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") ) .prefetch_related("assignees", "labels", "issue_module__module") ) def get_queryset(self): return ( Issue.issue_objects.filter( project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), issue_module__module_id=self.kwargs.get("module_id"), issue_module__deleted_at__isnull=True, ) ).distinct() @method_decorator(gzip_page) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def list(self, request, slug, project_id, module_id): filters = issue_filters(request.query_params, "GET") issue_queryset = self.get_queryset() # Apply filtering from filterset issue_queryset = self.filter_queryset(issue_queryset) # Apply legacy filters issue_queryset = issue_queryset.filter(**filters) # Total count queryset total_issue_queryset = copy.deepcopy(issue_queryset) # Apply annotations to the issue queryset issue_queryset = self.apply_annotations(issue_queryset) order_by_param = request.GET.get("order_by", "created_at") # 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, total_count_queryset=total_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, queryset=total_issue_queryset, ), sub_group_by_fields=issue_group_values( field=sub_group_by, slug=slug, project_id=project_id, filters=filters, queryset=total_issue_queryset, ), 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, total_count_queryset=total_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, queryset=total_issue_queryset, ), 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, total_count_queryset=total_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]) # create multiple issues inside a module def create_module_issues(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) if not issues: return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST) project = Project.objects.get(pk=project_id) _ = ModuleIssue.objects.bulk_create( [ ModuleIssue( issue_id=str(issue), module_id=module_id, project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, updated_by=request.user, ) for issue in issues ], batch_size=10, ignore_conflicts=True, ) # Bulk Update the activity _ = [ issue_activity.delay( type="module.activity.created", requested_data=json.dumps({"module_id": str(module_id)}), actor_id=str(request.user.id), issue_id=str(issue), project_id=project_id, current_instance=None, epoch=int(timezone.now().timestamp()), notification=True, origin=base_host(request=request, is_app=True), ) for issue in issues ] return Response({"message": "success"}, status=status.HTTP_201_CREATED) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) # add multiple module inside an issue and remove multiple modules from an issue def create_issue_modules(self, request, slug, project_id, issue_id): modules = request.data.get("modules", []) removed_modules = request.data.get("removed_modules", []) project = Project.objects.get(pk=project_id) if modules: _ = ModuleIssue.objects.bulk_create( [ ModuleIssue( issue_id=issue_id, module_id=module, project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, updated_by=request.user, ) for module in modules ], batch_size=10, ignore_conflicts=True, ) # Bulk Update the activity _ = [ issue_activity.delay( type="module.activity.created", requested_data=json.dumps({"module_id": module}), actor_id=str(request.user.id), issue_id=issue_id, project_id=project_id, current_instance=None, epoch=int(timezone.now().timestamp()), notification=True, origin=base_host(request=request, is_app=True), ) for module in modules ] for module_id in removed_modules: module_issue = ModuleIssue.objects.filter( workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id, ) issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps({"module_id": str(module_id)}), actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), current_instance=json.dumps( { "module_name": ( module_issue.first().module.name if (module_issue.first() and module_issue.first().module) else None ) } ), epoch=int(timezone.now().timestamp()), notification=True, origin=base_host(request=request, is_app=True), ) module_issue.delete() return Response({"message": "success"}, status=status.HTTP_201_CREATED) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def destroy(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.filter( workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id, ) issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps({"module_id": str(module_id)}), actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), current_instance=json.dumps({"module_name": module_issue.first().module.name}), epoch=int(timezone.now().timestamp()), notification=True, origin=base_host(request=request, is_app=True), ) module_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT)