280 lines
11 KiB
Python
280 lines
11 KiB
Python
# Python imports
|
|
import json
|
|
|
|
# Django imports
|
|
from django.utils import timezone
|
|
from django.db.models import Q, OuterRef, F, Func, UUIDField, Value, CharField, Subquery
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
from django.db.models.functions import Coalesce
|
|
from django.contrib.postgres.aggregates import ArrayAgg
|
|
from django.contrib.postgres.fields import ArrayField
|
|
|
|
# Third Party imports
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
|
|
# Module imports
|
|
from .. import BaseViewSet
|
|
from plane.app.serializers import IssueRelationSerializer, RelatedIssueSerializer
|
|
from plane.app.permissions import ProjectEntityPermission
|
|
from plane.db.models import (
|
|
Project,
|
|
IssueRelation,
|
|
Issue,
|
|
FileAsset,
|
|
IssueLink,
|
|
CycleIssue,
|
|
)
|
|
from plane.bgtasks.issue_activities_task import issue_activity
|
|
from plane.utils.issue_relation_mapper import get_actual_relation
|
|
from plane.utils.host import base_host
|
|
|
|
|
|
class IssueRelationViewSet(BaseViewSet):
|
|
serializer_class = IssueRelationSerializer
|
|
model = IssueRelation
|
|
permission_classes = [ProjectEntityPermission]
|
|
|
|
def list(self, request, slug, project_id, issue_id):
|
|
issue_relations = (
|
|
IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id))
|
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
.select_related("project")
|
|
.select_related("workspace")
|
|
.select_related("issue")
|
|
.order_by("-created_at")
|
|
.distinct()
|
|
)
|
|
# get all blocking issues
|
|
blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id).values_list(
|
|
"issue_id", flat=True
|
|
)
|
|
|
|
# get all blocked by issues
|
|
blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id).values_list(
|
|
"related_issue_id", flat=True
|
|
)
|
|
|
|
# get all duplicate issues
|
|
duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate").values_list(
|
|
"related_issue_id", flat=True
|
|
)
|
|
|
|
# get all relates to issues
|
|
duplicate_issues_related = issue_relations.filter(
|
|
related_issue_id=issue_id, relation_type="duplicate"
|
|
).values_list("issue_id", flat=True)
|
|
|
|
# get all relates to issues
|
|
relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to").values_list(
|
|
"related_issue_id", flat=True
|
|
)
|
|
|
|
# get all relates to issues
|
|
relates_to_issues_related = issue_relations.filter(
|
|
related_issue_id=issue_id, relation_type="relates_to"
|
|
).values_list("issue_id", flat=True)
|
|
|
|
# get all start after issues
|
|
start_after_issues = issue_relations.filter(
|
|
relation_type="start_before", related_issue_id=issue_id
|
|
).values_list("issue_id", flat=True)
|
|
|
|
# get all start_before issues
|
|
start_before_issues = issue_relations.filter(relation_type="start_before", issue_id=issue_id).values_list(
|
|
"related_issue_id", flat=True
|
|
)
|
|
|
|
# get all finish after issues
|
|
finish_after_issues = issue_relations.filter(
|
|
relation_type="finish_before", related_issue_id=issue_id
|
|
).values_list("issue_id", flat=True)
|
|
|
|
# get all finish before issues
|
|
finish_before_issues = issue_relations.filter(relation_type="finish_before", issue_id=issue_id).values_list(
|
|
"related_issue_id", flat=True
|
|
)
|
|
|
|
queryset = (
|
|
Issue.issue_objects.filter(workspace__slug=slug)
|
|
.select_related("workspace", "project", "state", "parent")
|
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
|
.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")
|
|
)
|
|
.annotate(
|
|
label_ids=Coalesce(
|
|
ArrayAgg(
|
|
"labels__id",
|
|
distinct=True,
|
|
filter=Q(~Q(labels__id__isnull=True) & (Q(label_issue__deleted_at__isnull=True))),
|
|
),
|
|
Value([], output_field=ArrayField(UUIDField())),
|
|
),
|
|
assignee_ids=Coalesce(
|
|
ArrayAgg(
|
|
"assignees__id",
|
|
distinct=True,
|
|
filter=Q(
|
|
~Q(assignees__id__isnull=True)
|
|
& Q(assignees__member_project__is_active=True)
|
|
& Q(issue_assignee__deleted_at__isnull=True)
|
|
),
|
|
),
|
|
Value([], output_field=ArrayField(UUIDField())),
|
|
),
|
|
)
|
|
).distinct()
|
|
|
|
# Fields
|
|
fields = [
|
|
"id",
|
|
"name",
|
|
"state_id",
|
|
"sort_order",
|
|
"priority",
|
|
"sequence_id",
|
|
"project_id",
|
|
"label_ids",
|
|
"assignee_ids",
|
|
"created_at",
|
|
"updated_at",
|
|
"created_by",
|
|
"updated_by",
|
|
"relation_type",
|
|
]
|
|
|
|
response_data = {
|
|
"blocking": queryset.filter(pk__in=blocking_issues)
|
|
.annotate(relation_type=Value("blocking", output_field=CharField()))
|
|
.values(*fields),
|
|
"blocked_by": queryset.filter(pk__in=blocked_by_issues)
|
|
.annotate(relation_type=Value("blocked_by", output_field=CharField()))
|
|
.values(*fields),
|
|
"duplicate": queryset.filter(pk__in=duplicate_issues)
|
|
.annotate(relation_type=Value("duplicate", output_field=CharField()))
|
|
.values(*fields)
|
|
| queryset.filter(pk__in=duplicate_issues_related)
|
|
.annotate(relation_type=Value("duplicate", output_field=CharField()))
|
|
.values(*fields),
|
|
"relates_to": queryset.filter(pk__in=relates_to_issues)
|
|
.annotate(relation_type=Value("relates_to", output_field=CharField()))
|
|
.values(*fields)
|
|
| queryset.filter(pk__in=relates_to_issues_related)
|
|
.annotate(relation_type=Value("relates_to", output_field=CharField()))
|
|
.values(*fields),
|
|
"start_after": queryset.filter(pk__in=start_after_issues)
|
|
.annotate(relation_type=Value("start_after", output_field=CharField()))
|
|
.values(*fields),
|
|
"start_before": queryset.filter(pk__in=start_before_issues)
|
|
.annotate(relation_type=Value("start_before", output_field=CharField()))
|
|
.values(*fields),
|
|
"finish_after": queryset.filter(pk__in=finish_after_issues)
|
|
.annotate(relation_type=Value("finish_after", output_field=CharField()))
|
|
.values(*fields),
|
|
"finish_before": queryset.filter(pk__in=finish_before_issues)
|
|
.annotate(relation_type=Value("finish_before", output_field=CharField()))
|
|
.values(*fields),
|
|
}
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
|
|
def create(self, request, slug, project_id, issue_id):
|
|
relation_type = request.data.get("relation_type", None)
|
|
if relation_type is None:
|
|
return Response(
|
|
{"message": "Issue relation type is required"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
issues = request.data.get("issues", [])
|
|
project = Project.objects.get(pk=project_id)
|
|
|
|
issue_relation = IssueRelation.objects.bulk_create(
|
|
[
|
|
IssueRelation(
|
|
issue_id=(issue if relation_type in ["blocking", "start_after", "finish_after"] else issue_id),
|
|
related_issue_id=(
|
|
issue_id if relation_type in ["blocking", "start_after", "finish_after"] else issue
|
|
),
|
|
relation_type=(get_actual_relation(relation_type)),
|
|
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,
|
|
)
|
|
|
|
issue_activity.delay(
|
|
type="issue_relation.activity.created",
|
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
|
actor_id=str(request.user.id),
|
|
issue_id=str(issue_id),
|
|
project_id=str(project_id),
|
|
current_instance=None,
|
|
epoch=int(timezone.now().timestamp()),
|
|
notification=True,
|
|
origin=base_host(request=request, is_app=True),
|
|
)
|
|
|
|
if relation_type in ["blocking", "start_after", "finish_after"]:
|
|
return Response(
|
|
RelatedIssueSerializer(issue_relation, many=True).data,
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
else:
|
|
return Response(
|
|
IssueRelationSerializer(issue_relation, many=True).data,
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
def remove_relation(self, request, slug, project_id, issue_id):
|
|
related_issue = request.data.get("related_issue", None)
|
|
|
|
issue_relations = IssueRelation.objects.filter(
|
|
workspace__slug=slug,
|
|
).filter(
|
|
Q(issue_id=related_issue, related_issue_id=issue_id) | Q(issue_id=issue_id, related_issue_id=related_issue)
|
|
)
|
|
issue_relations = issue_relations.first()
|
|
current_instance = json.dumps(IssueRelationSerializer(issue_relations).data, cls=DjangoJSONEncoder)
|
|
issue_relations.delete()
|
|
issue_activity.delay(
|
|
type="issue_relation.activity.deleted",
|
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
|
actor_id=str(request.user.id),
|
|
issue_id=str(issue_id),
|
|
project_id=str(project_id),
|
|
current_instance=current_instance,
|
|
epoch=int(timezone.now().timestamp()),
|
|
notification=True,
|
|
origin=base_host(request=request, is_app=True),
|
|
)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|