mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
[MOBIL-168] issue relations (#1206)
* chore: issue relation query * chore: import error * chore: blocking issues list update * chore: updated search and remove in issue relation * chore: updated add and remove relation * chore: created issue comment mutation * chore: replaced the project filter with workspace filter
This commit is contained in:
2
apiserver/plane/graphql/mutations/issues/__init__.py
Normal file
2
apiserver/plane/graphql/mutations/issues/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .relation import IssueRelationMutation
|
||||
from .comment import IssueCommentMutation
|
||||
47
apiserver/plane/graphql/mutations/issues/comment.py
Normal file
47
apiserver/plane/graphql/mutations/issues/comment.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Strawberry imports
|
||||
import strawberry
|
||||
from strawberry.types import Info
|
||||
from strawberry.permission import PermissionExtension
|
||||
|
||||
# Third-party imports
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.graphql.permissions.project import ProjectBasePermission
|
||||
from plane.db.models import Workspace, IssueComment
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class IssueCommentMutation:
|
||||
# adding issue comment
|
||||
@strawberry.mutation(
|
||||
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
|
||||
)
|
||||
async def addIssueComment(
|
||||
self,
|
||||
info: Info,
|
||||
slug: str,
|
||||
project: strawberry.ID,
|
||||
issue: strawberry.ID,
|
||||
comment_html: str,
|
||||
) -> bool:
|
||||
workspace_details = await sync_to_async(
|
||||
Workspace.objects.filter(slug=slug).first
|
||||
)()
|
||||
if not workspace_details:
|
||||
return False
|
||||
|
||||
await sync_to_async(
|
||||
lambda: IssueComment.objects.create(
|
||||
workspace_id=workspace_details.id,
|
||||
project_id=project,
|
||||
issue_id=issue,
|
||||
comment_html=comment_html,
|
||||
actor=info.context.user,
|
||||
created_by=info.context.user,
|
||||
updated_by=info.context.user,
|
||||
)
|
||||
)()
|
||||
|
||||
return True
|
||||
98
apiserver/plane/graphql/mutations/issues/relation.py
Normal file
98
apiserver/plane/graphql/mutations/issues/relation.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Strawberry imports
|
||||
import strawberry
|
||||
from strawberry.types import Info
|
||||
from strawberry.permission import PermissionExtension
|
||||
|
||||
# Third-party imports
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.graphql.permissions.project import ProjectBasePermission
|
||||
from plane.db.models import Workspace, IssueRelation
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class IssueRelationMutation:
|
||||
# adding issue relation
|
||||
@strawberry.mutation(
|
||||
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
|
||||
)
|
||||
async def addIssueRelation(
|
||||
self,
|
||||
info: Info,
|
||||
slug: str,
|
||||
project: strawberry.ID,
|
||||
issue: strawberry.ID,
|
||||
relation_type: str,
|
||||
related_issue_ids: list[strawberry.ID],
|
||||
) -> bool:
|
||||
workspace_details = await sync_to_async(
|
||||
Workspace.objects.filter(slug=slug).first
|
||||
)()
|
||||
if not workspace_details:
|
||||
return False
|
||||
|
||||
issue_relations = [
|
||||
IssueRelation(
|
||||
issue_id=(
|
||||
related_issue_id if relation_type == "blocking" else issue
|
||||
),
|
||||
related_issue_id=(
|
||||
issue if relation_type == "blocking" else related_issue_id
|
||||
),
|
||||
relation_type=(
|
||||
"blocked_by"
|
||||
if relation_type == "blocking"
|
||||
else relation_type
|
||||
),
|
||||
project_id=project,
|
||||
workspace_id=workspace_details.id,
|
||||
created_by=info.context.user,
|
||||
updated_by=info.context.user,
|
||||
)
|
||||
for related_issue_id in related_issue_ids
|
||||
]
|
||||
|
||||
await sync_to_async(
|
||||
lambda: IssueRelation.objects.bulk_create(
|
||||
issue_relations,
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
)()
|
||||
|
||||
return True
|
||||
|
||||
# removing issue relation
|
||||
@strawberry.mutation(
|
||||
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
|
||||
)
|
||||
async def removeIssueRelation(
|
||||
self,
|
||||
info: Info,
|
||||
slug: str,
|
||||
project: strawberry.ID,
|
||||
issue: strawberry.ID,
|
||||
relation_type: str,
|
||||
related_issue: strawberry.ID,
|
||||
) -> bool:
|
||||
issue_relation = await sync_to_async(
|
||||
lambda: IssueRelation.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project,
|
||||
issue_id=(
|
||||
related_issue if relation_type == "blocking" else issue
|
||||
),
|
||||
related_issue_id=(
|
||||
issue if relation_type == "blocking" else related_issue
|
||||
),
|
||||
)
|
||||
)()
|
||||
|
||||
if not issue_relation:
|
||||
return False
|
||||
|
||||
await sync_to_async(lambda: issue_relation.delete())()
|
||||
|
||||
return True
|
||||
2
apiserver/plane/graphql/queries/issues/__init__.py
Normal file
2
apiserver/plane/graphql/queries/issues/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .relation import IssueRelationQuery
|
||||
from .search import IssuesSearchQuery
|
||||
96
apiserver/plane/graphql/queries/issues/relation.py
Normal file
96
apiserver/plane/graphql/queries/issues/relation.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Third-Party Imports
|
||||
import strawberry
|
||||
|
||||
|
||||
# Python Standard Library Imports
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
# Django Imports
|
||||
from django.db.models import Q, Value, CharField
|
||||
|
||||
# Strawberry Imports
|
||||
from strawberry.types import Info
|
||||
from strawberry.permission import PermissionExtension
|
||||
|
||||
# Module Imports
|
||||
from plane.graphql.permissions.project import ProjectBasePermission
|
||||
from plane.db.models import IssueRelation, Issue
|
||||
from plane.graphql.types.issues.relation import IssueRelationType
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class IssueRelationQuery:
|
||||
# getting issue relation issues
|
||||
@strawberry.field(
|
||||
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
|
||||
)
|
||||
async def issueRelation(
|
||||
self,
|
||||
info: Info,
|
||||
slug: str,
|
||||
project: strawberry.ID,
|
||||
issue: strawberry.ID,
|
||||
) -> IssueRelationType:
|
||||
# construct the issue relation query
|
||||
issue_relation_query = (
|
||||
IssueRelation.objects.filter(workspace__slug=slug)
|
||||
.filter(project__id=project)
|
||||
.filter(
|
||||
project__project_projectmember__member=info.context.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.filter(Q(issue_id=issue) | Q(related_issue=issue))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# getting all blocking issues
|
||||
blocking_issue_ids = await sync_to_async(list)(
|
||||
issue_relation_query.filter(
|
||||
relation_type="blocked_by", related_issue_id=issue
|
||||
).values_list("issue_id", flat=True)
|
||||
)
|
||||
|
||||
# getting all blocked by issues
|
||||
blocked_by_issue_ids = await sync_to_async(list)(
|
||||
issue_relation_query.filter(
|
||||
relation_type="blocked_by", issue_id=issue
|
||||
).values_list("related_issue_id", flat=True)
|
||||
)
|
||||
|
||||
# constructing the issue query
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project
|
||||
)
|
||||
.filter(
|
||||
project__project_projectmember__member=info.context.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
)
|
||||
|
||||
# getting all blocking issues
|
||||
blocking_issues = await sync_to_async(list)(
|
||||
issue_queryset.filter(id__in=blocking_issue_ids).annotate(
|
||||
relation_type=Value("blocking", output_field=CharField())
|
||||
)
|
||||
)
|
||||
|
||||
# getting all blocked by issues
|
||||
blocked_by_issues = await sync_to_async(list)(
|
||||
issue_queryset.filter(id__in=blocked_by_issue_ids).annotate(
|
||||
relation_type=Value("blocked_by", output_field=CharField())
|
||||
)
|
||||
)
|
||||
|
||||
relation_response = IssueRelationType(
|
||||
blocking=blocking_issues,
|
||||
blocked_by=blocked_by_issues,
|
||||
)
|
||||
|
||||
return relation_response
|
||||
112
apiserver/plane/graphql/queries/issues/search.py
Normal file
112
apiserver/plane/graphql/queries/issues/search.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# Python imports
|
||||
import re
|
||||
|
||||
# Third-Party Imports
|
||||
import strawberry
|
||||
|
||||
from typing import Optional
|
||||
|
||||
# Python Standard Library Imports
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
# Django Imports
|
||||
from django.db.models import Q
|
||||
|
||||
# Strawberry Imports
|
||||
from strawberry.types import Info
|
||||
from strawberry.permission import PermissionExtension
|
||||
|
||||
# Module Imports
|
||||
from plane.graphql.permissions.workspace import WorkspaceBasePermission
|
||||
from plane.db.models import Issue
|
||||
from plane.graphql.types.issue import IssueLiteType
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class IssuesSearchQuery:
|
||||
# getting issues which are not related
|
||||
@strawberry.field(
|
||||
extensions=[
|
||||
PermissionExtension(permissions=[WorkspaceBasePermission()])
|
||||
]
|
||||
)
|
||||
|
||||
# getting issue relation issues
|
||||
async def issuesSearch(
|
||||
self,
|
||||
info: Info,
|
||||
slug: str,
|
||||
project: Optional[strawberry.ID] = None,
|
||||
module: Optional[strawberry.ID] = None,
|
||||
cycle: Optional[strawberry.ID] = None,
|
||||
issue: Optional[strawberry.ID] = None,
|
||||
relation_type: Optional[bool] = False,
|
||||
search: Optional[str] = None,
|
||||
) -> list[IssueLiteType]:
|
||||
issue_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=info.context.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
project__archived_at__isnull=True,
|
||||
)
|
||||
|
||||
# workspace issues
|
||||
if project:
|
||||
issue_queryset = issue_queryset.filter(project_id=project)
|
||||
|
||||
# module issues
|
||||
if module:
|
||||
issue_queryset = issue_queryset.filter(
|
||||
issue_module__module_id=module
|
||||
)
|
||||
|
||||
# cycle issues
|
||||
if cycle:
|
||||
issue_queryset = issue_queryset.filter(issue_cycle__cycle_id=cycle)
|
||||
|
||||
# issue relation issues
|
||||
if relation_type and issue:
|
||||
issue_queryset = issue_queryset.filter(
|
||||
~Q(pk=issue),
|
||||
~Q(
|
||||
issue_related__issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
~Q(
|
||||
issue_relation__related_issue=issue,
|
||||
issue_related__deleted_at__isnull=True,
|
||||
),
|
||||
)
|
||||
|
||||
# apply search filter
|
||||
q = Q()
|
||||
if search:
|
||||
fields = ["name", "sequence_id", "project__identifier"]
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
# Match whole integers only (exclude decimal numbers)
|
||||
sequences = re.findall(r"\b\d+\b", search)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
else:
|
||||
q |= Q(**{f"{field}__icontains": search})
|
||||
|
||||
issues = await sync_to_async(
|
||||
lambda: list(
|
||||
issue_queryset.filter(q)
|
||||
.distinct()
|
||||
.values(
|
||||
"id",
|
||||
"sequence_id",
|
||||
"name",
|
||||
"project",
|
||||
"project__identifier",
|
||||
)
|
||||
)
|
||||
)()
|
||||
|
||||
for issue in issues:
|
||||
issue["project_identifier"] = issue["project__identifier"]
|
||||
del issue["project__identifier"]
|
||||
|
||||
return [IssueLiteType(**issue) for issue in issues]
|
||||
@@ -42,6 +42,7 @@ from .queries.search import GlobalSearchQuery
|
||||
from .queries.attachment import IssueAttachmentQuery
|
||||
from .queries.link import IssueLinkQuery
|
||||
from .queries.estimate import EstimatePointQuery
|
||||
from .queries.issues import IssueRelationQuery, IssuesSearchQuery
|
||||
|
||||
# mutations
|
||||
from .mutations.workspace import WorkspaceMutation, WorkspaceInviteMutation
|
||||
@@ -71,6 +72,7 @@ from .mutations.module import (
|
||||
)
|
||||
from .mutations.link import IssueLinkMutation
|
||||
from .mutations.favorite import UserFavoriteMutation
|
||||
from .mutations.issues import IssueRelationMutation, IssueCommentMutation
|
||||
|
||||
|
||||
# combined query class for all
|
||||
@@ -114,6 +116,8 @@ class Query(
|
||||
UserPageQuery,
|
||||
CycleIssueUserPropertyQuery,
|
||||
ModuleIssueUserPropertyQuery,
|
||||
IssueRelationQuery,
|
||||
IssuesSearchQuery,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -141,6 +145,8 @@ class Mutation(
|
||||
UserFavoriteMutation,
|
||||
CycleIssueUserPropertyMutation,
|
||||
ModuleIssueUserPropertyMutation,
|
||||
IssueRelationMutation,
|
||||
IssueCommentMutation,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
0
apiserver/plane/graphql/types/issues/__init__.py
Normal file
0
apiserver/plane/graphql/types/issues/__init__.py
Normal file
12
apiserver/plane/graphql/types/issues/relation.py
Normal file
12
apiserver/plane/graphql/types/issues/relation.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Strawberry imports
|
||||
import strawberry_django
|
||||
|
||||
# Module Imports
|
||||
from plane.db.models import IssueRelation
|
||||
from plane.graphql.types.issue import IssuesType
|
||||
|
||||
|
||||
@strawberry_django.type(IssueRelation)
|
||||
class IssueRelationType:
|
||||
blocking: list[IssuesType]
|
||||
blocked_by: list[IssuesType]
|
||||
Reference in New Issue
Block a user