forked from github/plane
feat: add a relation to an issue (#1995)
* feat: add issue relation to an issue * fix: deleted the migration file * fix: changed link to relates to in choice fields * fix: added the migration file * fix: changed migration file * fix: project id issue fixed * fix: added issue in the payload * fix: changed the query param for blocker
This commit is contained in:
parent
164e0b9301
commit
a34b0b059d
@ -31,8 +31,6 @@ from .issue import (
|
|||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssuePropertySerializer,
|
IssuePropertySerializer,
|
||||||
BlockerIssueSerializer,
|
|
||||||
BlockedIssueSerializer,
|
|
||||||
IssueAssigneeSerializer,
|
IssueAssigneeSerializer,
|
||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
@ -45,6 +43,8 @@ from .issue import (
|
|||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,12 +17,10 @@ from plane.db.models import (
|
|||||||
IssueActivity,
|
IssueActivity,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
IssueProperty,
|
IssueProperty,
|
||||||
IssueBlocker,
|
|
||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
IssueLabel,
|
IssueLabel,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
Cycle,
|
Cycle,
|
||||||
Module,
|
Module,
|
||||||
@ -32,6 +30,7 @@ from plane.db.models import (
|
|||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
|
IssueRelation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -81,25 +80,12 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# List of issues that are blocking this issue
|
|
||||||
blockers_list = serializers.ListField(
|
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
|
||||||
write_only=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
labels_list = serializers.ListField(
|
labels_list = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# List of issues that are blocked by this issue
|
|
||||||
blocks_list = serializers.ListField(
|
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
|
||||||
write_only=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -122,10 +108,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
labels = validated_data.pop("labels_list", None)
|
labels = validated_data.pop("labels_list", None)
|
||||||
blocks = validated_data.pop("blocks_list", None)
|
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project_id = self.context["project_id"]
|
||||||
workspace_id = self.context["workspace_id"]
|
workspace_id = self.context["workspace_id"]
|
||||||
@ -137,22 +121,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
created_by_id = issue.created_by_id
|
created_by_id = issue.created_by_id
|
||||||
updated_by_id = issue.updated_by_id
|
updated_by_id = issue.updated_by_id
|
||||||
|
|
||||||
if blockers is not None and len(blockers):
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=issue,
|
|
||||||
blocked_by=blocker,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for blocker in blockers
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if assignees is not None and len(assignees):
|
if assignees is not None and len(assignees):
|
||||||
IssueAssignee.objects.bulk_create(
|
IssueAssignee.objects.bulk_create(
|
||||||
[
|
[
|
||||||
@ -196,29 +164,11 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
batch_size=10,
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if blocks is not None and len(blocks):
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=block,
|
|
||||||
blocked_by=issue,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for block in blocks
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
return issue
|
return issue
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
labels = validated_data.pop("labels_list", None)
|
labels = validated_data.pop("labels_list", None)
|
||||||
blocks = validated_data.pop("blocks_list", None)
|
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
@ -226,23 +176,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
created_by_id = instance.created_by_id
|
created_by_id = instance.created_by_id
|
||||||
updated_by_id = instance.updated_by_id
|
updated_by_id = instance.updated_by_id
|
||||||
|
|
||||||
if blockers is not None:
|
|
||||||
IssueBlocker.objects.filter(block=instance).delete()
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=instance,
|
|
||||||
blocked_by=blocker,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for blocker in blockers
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if assignees is not None:
|
if assignees is not None:
|
||||||
IssueAssignee.objects.filter(issue=instance).delete()
|
IssueAssignee.objects.filter(issue=instance).delete()
|
||||||
IssueAssignee.objects.bulk_create(
|
IssueAssignee.objects.bulk_create(
|
||||||
@ -277,23 +210,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
batch_size=10,
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if blocks is not None:
|
|
||||||
IssueBlocker.objects.filter(blocked_by=instance).delete()
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=block,
|
|
||||||
blocked_by=instance,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for block in blocks
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Time updation occues even when other related models are updated
|
# Time updation occues even when other related models are updated
|
||||||
instance.updated_at = timezone.now()
|
instance.updated_at = timezone.now()
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
@ -375,32 +291,39 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BlockedIssueSerializer(BaseSerializer):
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
|
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueBlocker
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"blocked_issue_detail",
|
"related_issue_detail",
|
||||||
"blocked_by",
|
"relation_type",
|
||||||
"block",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
class RelatedIssueSerializer(BaseSerializer):
|
||||||
class BlockerIssueSerializer(BaseSerializer):
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||||
blocker_issue_detail = IssueProjectLiteSerializer(
|
|
||||||
source="blocked_by", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueBlocker
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"blocker_issue_detail",
|
"issue_detail",
|
||||||
"blocked_by",
|
"relation_type",
|
||||||
"block",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
|
||||||
class IssueAssigneeSerializer(BaseSerializer):
|
class IssueAssigneeSerializer(BaseSerializer):
|
||||||
@ -617,10 +540,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
# List of issues blocked by this issue
|
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||||
# List of issues that block this issue
|
|
||||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
|
||||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||||
|
@ -90,6 +90,7 @@ from plane.api.views import (
|
|||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
IssueCommentPublicViewSet,
|
IssueCommentPublicViewSet,
|
||||||
IssueReactionViewSet,
|
IssueReactionViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
IssueDraftViewSet,
|
IssueDraftViewSet,
|
||||||
## End Issues
|
## End Issues
|
||||||
@ -1011,6 +1012,26 @@ urlpatterns = [
|
|||||||
name="project-issue-archive",
|
name="project-issue-archive",
|
||||||
),
|
),
|
||||||
## End Issue Archives
|
## End Issue Archives
|
||||||
|
## Issue Relation
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||||
|
IssueRelationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-relation",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||||
|
IssueRelationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-relation",
|
||||||
|
),
|
||||||
|
## End Issue Relation
|
||||||
## Issue Drafts
|
## Issue Drafts
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||||
|
@ -86,6 +86,7 @@ from .issue import (
|
|||||||
IssueReactionPublicViewSet,
|
IssueReactionPublicViewSet,
|
||||||
CommentReactionPublicViewSet,
|
CommentReactionPublicViewSet,
|
||||||
IssueVotePublicViewSet,
|
IssueVotePublicViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
IssueRetrievePublicEndpoint,
|
IssueRetrievePublicEndpoint,
|
||||||
ProjectIssuesPublicEndpoint,
|
ProjectIssuesPublicEndpoint,
|
||||||
IssueDraftViewSet,
|
IssueDraftViewSet,
|
||||||
@ -168,6 +169,4 @@ from .analytic import (
|
|||||||
|
|
||||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||||
|
|
||||||
from .exporter import (
|
from .exporter import ExportIssuesEndpoint
|
||||||
ExportIssuesEndpoint,
|
|
||||||
)
|
|
@ -24,6 +24,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -51,6 +52,7 @@ from plane.api.serializers import (
|
|||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
@ -76,6 +78,7 @@ from plane.db.models import (
|
|||||||
CommentReaction,
|
CommentReaction,
|
||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
|
IssueRelation,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
@ -2040,6 +2043,98 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRelationViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueRelationSerializer
|
||||||
|
model = IssueRelation
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_relation.activity.deleted",
|
||||||
|
requested_data=json.dumps({"related_list": None}),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueRelationSerializer(current_instance).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
related_list = request.data.get("related_list", [])
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
issueRelation = IssueRelation.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueRelation(
|
||||||
|
issue_id=related_issue["issue"],
|
||||||
|
related_issue_id=related_issue["related_issue"],
|
||||||
|
relation_type=related_issue["relation_type"],
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for related_issue in related_list
|
||||||
|
],
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
IssueRelationSerializer(issueRelation, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"name": "The issue is already taken"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("issue")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
|
@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get("workspace_search", "false")
|
||||||
parent = request.query_params.get("parent", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
module = request.query_params.get("module", "false")
|
module = request.query_params.get("module", "false")
|
||||||
sub_issue = request.query_params.get("sub_issue", "false")
|
sub_issue = request.query_params.get("sub_issue", "false")
|
||||||
@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
"parent_id", flat=True
|
"parent_id", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if blocker_blocked_by == "true" and issue_id:
|
if issue_relation == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
issues = issues.filter(
|
issues = issues.filter(
|
||||||
~Q(pk=issue_id),
|
~Q(pk=issue_id),
|
||||||
~Q(blocked_issues__block=issue),
|
~Q(issue_related__issue=issue),
|
||||||
~Q(blocker_issues__blocked_by=issue),
|
~Q(issue_relation__related_issue=issue),
|
||||||
)
|
)
|
||||||
if sub_issue == "true" and issue_id:
|
if sub_issue == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
|
@ -393,130 +393,6 @@ def track_assignees(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Track changes in blocking issues
|
|
||||||
def track_blocks(
|
|
||||||
requested_data,
|
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
):
|
|
||||||
if len(requested_data.get("blocks_list")) > len(
|
|
||||||
current_instance.get("blocked_issues")
|
|
||||||
):
|
|
||||||
for block in requested_data.get("blocks_list"):
|
|
||||||
if (
|
|
||||||
len(
|
|
||||||
[
|
|
||||||
blocked
|
|
||||||
for blocked in current_instance.get("blocked_issues")
|
|
||||||
if blocked.get("block") == block
|
|
||||||
]
|
|
||||||
)
|
|
||||||
== 0
|
|
||||||
):
|
|
||||||
issue = Issue.objects.get(pk=block)
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value="",
|
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
field="blocks",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
new_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Blocked Issue Removal
|
|
||||||
if len(requested_data.get("blocks_list")) < len(
|
|
||||||
current_instance.get("blocked_issues")
|
|
||||||
):
|
|
||||||
for blocked in current_instance.get("blocked_issues"):
|
|
||||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
|
||||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field="blocks",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
old_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Track changes in blocked_by issues
|
|
||||||
def track_blockings(
|
|
||||||
requested_data,
|
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
):
|
|
||||||
if len(requested_data.get("blockers_list")) > len(
|
|
||||||
current_instance.get("blocker_issues")
|
|
||||||
):
|
|
||||||
for block in requested_data.get("blockers_list"):
|
|
||||||
if (
|
|
||||||
len(
|
|
||||||
[
|
|
||||||
blocked
|
|
||||||
for blocked in current_instance.get("blocker_issues")
|
|
||||||
if blocked.get("blocked_by") == block
|
|
||||||
]
|
|
||||||
)
|
|
||||||
== 0
|
|
||||||
):
|
|
||||||
issue = Issue.objects.get(pk=block)
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value="",
|
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
field="blocking",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
new_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Blocked Issue Removal
|
|
||||||
if len(requested_data.get("blockers_list")) < len(
|
|
||||||
current_instance.get("blocker_issues")
|
|
||||||
):
|
|
||||||
for blocked in current_instance.get("blocker_issues"):
|
|
||||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
|
||||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field="blocking",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
old_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_issue_activity(
|
def create_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
):
|
):
|
||||||
@ -637,8 +513,6 @@ def update_issue_activity(
|
|||||||
"start_date": track_start_date,
|
"start_date": track_start_date,
|
||||||
"labels_list": track_labels,
|
"labels_list": track_labels,
|
||||||
"assignees_list": track_assignees,
|
"assignees_list": track_assignees,
|
||||||
"blocks_list": track_blocks,
|
|
||||||
"blockers_list": track_blockings,
|
|
||||||
"estimate_point": track_estimate_points,
|
"estimate_point": track_estimate_points,
|
||||||
"archived_at": track_archive_at,
|
"archived_at": track_archive_at,
|
||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
@ -1170,6 +1044,57 @@ def delete_issue_vote_activity(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_issue_relation_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if current_instance is None and requested_data.get("related_list") is not None:
|
||||||
|
for issue_relation in requested_data.get("related_list"):
|
||||||
|
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_relation.get("issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="created",
|
||||||
|
old_value="",
|
||||||
|
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
field=f'{issue_relation.get("relation_type")}',
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||||
|
old_identifier=issue_relation.get("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_issue_relation_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if current_instance is not None and requested_data.get("related_list") is None:
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=current_instance.get("issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=f'{current_instance.get("relation_type")}',
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'deleted the {current_instance.get("relation_type")} relation',
|
||||||
|
old_identifier=current_instance.get("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1233,6 +1158,8 @@ def issue_activity(
|
|||||||
"link.activity.deleted": delete_link_activity,
|
"link.activity.deleted": delete_link_activity,
|
||||||
"attachment.activity.created": create_attachment_activity,
|
"attachment.activity.created": create_attachment_activity,
|
||||||
"attachment.activity.deleted": delete_attachment_activity,
|
"attachment.activity.deleted": delete_attachment_activity,
|
||||||
|
"issue_relation.activity.created": create_issue_relation_activity,
|
||||||
|
"issue_relation.activity.deleted": delete_issue_relation_activity,
|
||||||
"issue_reaction.activity.created": create_issue_reaction_activity,
|
"issue_reaction.activity.created": create_issue_reaction_activity,
|
||||||
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
||||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-09-12 07:29
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from plane.db.models import IssueRelation
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def create_issue_relation(apps, schema_editor):
|
||||||
|
try:
|
||||||
|
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
|
||||||
|
updated_issue_relation = []
|
||||||
|
for blocked_issue in IssueBlockerModel.objects.all():
|
||||||
|
updated_issue_relation.append(
|
||||||
|
IssueRelation(
|
||||||
|
issue_id=blocked_issue.block_id,
|
||||||
|
related_issue_id=blocked_issue.blocked_by_id,
|
||||||
|
relation_type="blocked_by",
|
||||||
|
project_id=blocked_issue.project_id,
|
||||||
|
workspace_id=blocked_issue.workspace_id,
|
||||||
|
created_by_id=blocked_issue.created_by_id,
|
||||||
|
updated_by_id=blocked_issue.updated_by_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0042_alter_analyticview_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='IssueRelation',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||||
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
|
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||||
|
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
|
||||||
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Issue Relation',
|
||||||
|
'verbose_name_plural': 'Issue Relations',
|
||||||
|
'db_table': 'issue_relations',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'unique_together': {('issue', 'related_issue')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(create_issue_relation),
|
||||||
|
]
|
@ -32,6 +32,7 @@ from .issue import (
|
|||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
IssueBlocker,
|
||||||
|
IssueRelation,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueSequence,
|
IssueSequence,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
@ -180,6 +180,37 @@ class IssueBlocker(ProjectBaseModel):
|
|||||||
return f"{self.block.name} {self.blocked_by.name}"
|
return f"{self.block.name} {self.blocked_by.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRelation(ProjectBaseModel):
|
||||||
|
RELATION_CHOICES = (
|
||||||
|
("duplicate", "Duplicate"),
|
||||||
|
("relates_to", "Relates To"),
|
||||||
|
("blocked_by", "Blocked By"),
|
||||||
|
)
|
||||||
|
|
||||||
|
issue = models.ForeignKey(
|
||||||
|
Issue, related_name="issue_relation", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
related_issue = models.ForeignKey(
|
||||||
|
Issue, related_name="issue_related", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
relation_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=RELATION_CHOICES,
|
||||||
|
verbose_name="Issue Relation Type",
|
||||||
|
default="blocked_by",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["issue", "related_issue"]
|
||||||
|
verbose_name = "Issue Relation"
|
||||||
|
verbose_name_plural = "Issue Relations"
|
||||||
|
db_table = "issue_relations"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.issue.name} {self.related_issue.name}"
|
||||||
|
|
||||||
|
|
||||||
class IssueAssignee(ProjectBaseModel):
|
class IssueAssignee(ProjectBaseModel):
|
||||||
issue = models.ForeignKey(
|
issue = models.ForeignKey(
|
||||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||||
|
Loading…
Reference in New Issue
Block a user