chore: cycle and module endpoints (#790)

* chore: module and cycles endpoint

* chore: CRUD operation for cycle and modules

* chore: resolvers in projects cycles
This commit is contained in:
Bavisetti Narayan
2024-08-12 18:57:33 +05:30
committed by GitHub
parent a1795bdc64
commit de4d95d90c
11 changed files with 680 additions and 11 deletions

View File

@@ -0,0 +1,157 @@
# Python imports
import json
# Strawberry imports
import strawberry
from strawberry.types import Info
from strawberry.scalars import JSON
from strawberry.permission import PermissionExtension
# Third-party imports
from asgiref.sync import sync_to_async
# Django imports
from django.db.models import Q
from django.utils import timezone
from django.core import serializers
# Module imports
from plane.graphql.permissions.project import (
ProjectMemberPermission,
)
from plane.db.models import (
CycleIssue,
Cycle,
)
from plane.graphql.bgtasks.issue_activity_task import issue_activity
@strawberry.type
class CycleIssueMutation:
@strawberry.mutation(
extensions=[
PermissionExtension(permissions=[ProjectMemberPermission()])
]
)
async def create_cycle_issue(
self,
info: Info,
slug: str,
project: strawberry.ID,
cycle: strawberry.ID,
issues: JSON,
) -> bool:
# Fetch the cycle object asynchronously
cycle = await sync_to_async(Cycle.objects.get)(
workspace__slug=slug, project_id=project, pk=cycle
)
# Fetch existing CycleIssues for the given issues asynchronously
existing_cycle_issues = await sync_to_async(
lambda: list(
CycleIssue.objects.filter(
~Q(cycle_id=cycle), issue_id__in=issues
).all()
)
)()
# Extract issue_ids from the existing CycleIssue objects
existing_issue_ids = set(
map(lambda ci: str(ci.issue_id), existing_cycle_issues)
)
# Convert issues to a set of strings
new_issue_ids = set(map(str, issues))
# Determine new issues to create
new_issue_ids -= existing_issue_ids
# Update existing CycleIssues to the new cycle asynchronously
await sync_to_async(
lambda: CycleIssue.objects.filter(
~Q(cycle=cycle), issue_id__in=existing_issue_ids
).update(cycle=cycle)
)()
# Create new CycleIssues for new issues
new_cycle_issues = [
CycleIssue(
project_id=project,
workspace_id=cycle.workspace_id,
created_by_id=info.context.user.id,
updated_by_id=info.context.user.id,
cycle=cycle,
issue_id=issue_id,
)
for issue_id in new_issue_ids
]
created_records = await sync_to_async(CycleIssue.objects.bulk_create)(
new_cycle_issues, batch_size=10
)
update_cycle_issue_activity = [
{
"old_cycle_id": str(cycle_issue.cycle_id),
"new_cycle_id": str(cycle.id),
"issue_id": str(cycle_issue.issue_id),
}
for cycle_issue in existing_cycle_issues
]
# Capture Issue Activity asynchronously
await sync_to_async(issue_activity.delay)(
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": list(issues)}),
actor_id=str(info.context.user.id),
issue_id=None,
project_id=str(project),
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=info.context.request.META.get("HTTP_ORIGIN"),
)
return True
@strawberry.mutation(
extensions=[
PermissionExtension(permissions=[ProjectMemberPermission()])
]
)
async def delete_cycle_issue(
self,
info: Info,
slug: str,
project: strawberry.ID,
cycle: strawberry.ID,
issue: strawberry.ID,
) -> bool:
cycle_issue = await sync_to_async(CycleIssue.objects.filter)(
cycle_id=cycle, project_id=project, workspace__slug=slug, issue_id=issue
)
await sync_to_async(issue_activity.delay)(
type="cycle.activity.deleted",
requested_data=json.dumps(
{
"cycle_id": str(cycle),
"issues": [str(issue)],
}
),
actor_id=str(info.context.user.id),
issue_id=str(issue),
project_id=str(project),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=info.context.request.META.get("HTTP_ORIGIN"),
)
await sync_to_async(cycle_issue.delete)()
return True

View File

@@ -6,6 +6,7 @@ import strawberry
from strawberry.types import Info
from strawberry.scalars import JSON
from strawberry.permission import PermissionExtension
from strawberry.file_uploads import Upload
# Third-party imports
from typing import Optional
@@ -262,7 +263,28 @@ class IssueUserPropertyMutation:
@strawberry.type
class IssueAttachmentMutation:
@strawberry.field(
# @strawberry.field(
# extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
# )
# async def create_issue_attachment(
# ) -> IssueAttachment:
# pass
# @strawberry.mutation(
# extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
# )
# def upload_file(self, file: Upload, info: Info) -> bool:
# content = file.read()
# filename = file.filename
# # Save the file using Django's file storage
# # file_name = default_storage.save(filename, content)
# return True
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def deleteIssueAttachment(

View File

@@ -0,0 +1,117 @@
# Python imports
import json
# Strawberry imports
import strawberry
from strawberry.scalars import JSON
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Third-party imports
from asgiref.sync import sync_to_async
# Django imports
from django.utils import timezone
# Module imports
from plane.graphql.permissions.project import (
ProjectMemberPermission,
)
from plane.db.models import (
Project,
ModuleIssue,
)
from plane.graphql.bgtasks.issue_activity_task import issue_activity
@strawberry.type
class ModuleIssueMutation:
@strawberry.mutation(
extensions=[
PermissionExtension(permissions=[ProjectMemberPermission()])
]
)
async def create_module_issues(
self,
info: Info,
slug: str,
project: strawberry.ID,
module: strawberry.ID,
issues: JSON,
) -> bool:
project = await sync_to_async(Project.objects.get)(pk=project)
print("issue", issues)
# Create ModuleIssues asynchronously
await sync_to_async(
lambda: ModuleIssue.objects.bulk_create(
[
ModuleIssue(
issue_id=issue_id,
module_id=module,
project_id=project.id,
workspace_id=project.workspace_id,
created_by_id=info.context.user.id,
updated_by_id=info.context.user.id,
)
for issue_id in issues
],
batch_size=10,
ignore_conflicts=True,
)
)()
await sync_to_async(
lambda: [
issue_activity.delay(
type="module.activity.created",
requested_data=json.dumps({"module_id": str(module)}),
actor_id=str(info.context.user.id),
issue_id=str(issue),
project_id=str(project.id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=info.context.request.META.get("HTTP_ORIGIN"),
)
for issue in issues
]
)()
return True
@strawberry.mutation(
extensions=[
PermissionExtension(permissions=[ProjectMemberPermission()])
]
)
async def delete_module_issue(
self,
info: Info,
slug: str,
project: strawberry.ID,
module: strawberry.ID,
issue: strawberry.ID,
) -> bool:
module_issue = await sync_to_async(ModuleIssue.objects.filter)(
module_id=module,
project_id=project,
workspace__slug=slug,
issue_id=issue,
)
await sync_to_async(issue_activity.delay)(
requested_data=json.dumps({"module_id": str(module)}),
actor_id=str(info.context.user.id),
issue_id=str(issue),
project_id=str(project),
current_instance=None,
type="module.activity.deleted",
epoch=int(timezone.now().timestamp()),
notification=True,
origin=info.context.request.META.get("HTTP_ORIGIN"),
)
# Delete the module issue in a synchronous context
await sync_to_async(module_issue.delete)()
return True

View File

@@ -0,0 +1,82 @@
# Third-Party Imports
import strawberry
# Python Standard Library Imports
from asgiref.sync import sync_to_async
# Strawberry Imports
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Module Imports
from plane.db.models import Cycle, Issue
from plane.graphql.types.cycle import CycleType
from plane.graphql.types.issue import IssueType
from plane.graphql.permissions.project import ProjectBasePermission
@strawberry.type
class CycleQuery:
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def cycles(
self,
info: Info,
slug: str,
project: strawberry.ID,
) -> list[CycleType]:
cycles = await sync_to_async(list)(
Cycle.objects.filter(workspace__slug=slug)
.filter(project_id=project)
.filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
)
return cycles
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def cycle(
self,
info: Info,
slug: str,
project: strawberry.ID,
cycle: strawberry.ID,
) -> CycleType:
cycle = await sync_to_async(Cycle.objects.get)(
workspace__slug=slug,
project_id=project,
id=cycle,
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
return cycle
@strawberry.type
class CycleIssueQuery:
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def cycleIssues(
self,
info: Info,
slug: str,
project: strawberry.ID,
cycle: strawberry.ID,
) -> list[IssueType]:
cycles_issues = await sync_to_async(list)(
Issue.issue_objects.filter(workspace__slug=slug)
.filter(project_id=project)
.filter(issue_cycle__cycle_id=cycle)
.filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
)
return cycles_issues

View File

@@ -45,9 +45,16 @@ class IssueQuery:
filters: Optional[JSON] = {},
orderBy: Optional[str] = "-created_at",
groupBy: Optional[str] = None,
type: Optional[str] = "all",
) -> list[IssueType]:
filters = issue_filters(filters, "POST")
if type == "backlog":
filters["state__group__in"] = ["backlog"]
elif type == "active":
filters["state__group__in"] = ["unstarted", "started"]
issues = await sync_to_async(list)(
Issue.issue_objects.filter(
workspace__slug=slug, project_id=project
@@ -114,7 +121,9 @@ class RecentIssuesQuery:
).filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)[:5]
)[
:5
]
)
return issues

View File

@@ -0,0 +1,82 @@
# Third-Party Imports
import strawberry
# Python Standard Library Imports
from asgiref.sync import sync_to_async
# Strawberry Imports
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Module Imports
from plane.db.models import Module, Issue
from plane.graphql.types.module import ModuleType
from plane.graphql.types.issue import IssueType
from plane.graphql.permissions.project import ProjectBasePermission
@strawberry.type
class ModuleQuery:
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def modules(
self,
info: Info,
slug: str,
project: strawberry.ID,
) -> list[ModuleType]:
modules = await sync_to_async(list)(
Module.objects.filter(workspace__slug=slug)
.filter(project_id=project)
.filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
)
return modules
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def module(
self,
info: Info,
slug: str,
project: strawberry.ID,
module: strawberry.ID,
) -> ModuleType:
module = await sync_to_async(Module.objects.get)(
workspace__slug=slug,
project_id=project,
id=module,
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
return module
@strawberry.type
class ModuleIssueQuery:
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def moduleIssues(
self,
info: Info,
slug: str,
project: strawberry.ID,
module: strawberry.ID,
) -> list[IssueType]:
module_issues = await sync_to_async(list)(
Issue.issue_objects.filter(workspace__slug=slug)
.filter(project_id=project)
.filter(issue_module__module_id=module)
.filter(
project__project_projectmember__member=info.context.user,
project__project_projectmember__is_active=True,
)
)
return module_issues

View File

@@ -21,6 +21,8 @@ from .queries.issue import (
IssueCommentActivityQuery,
)
from .queries.page import PageQuery
from .queries.cycle import CycleQuery, CycleIssueQuery
from .queries.module import ModuleQuery, ModuleIssueQuery
from .queries.search import ProjectSearchQuery
from .queries.attachment import IssueAttachmentQuery
@@ -39,7 +41,8 @@ from .mutations.issue import (
from .mutations.notification import NotificationMutation
from .mutations.user import ProfileMutation
from .mutations.page import PageFavoriteMutation
from .mutations.cycle import CycleIssueMutation
from .mutations.module import ModuleIssueMutation
# combined query class for all
@strawberry.type
@@ -64,6 +67,10 @@ class Query(
WorkspaceStateQuery,
ProjectSearchQuery,
IssueAttachmentQuery,
CycleQuery,
CycleIssueQuery,
ModuleQuery,
ModuleIssueQuery,
):
pass
@@ -82,6 +89,8 @@ class Mutation(
ProfileMutation,
PageFavoriteMutation,
IssueAttachmentMutation,
CycleIssueMutation,
ModuleIssueMutation,
):
pass

View File

@@ -0,0 +1,76 @@
# python imports
from typing import Optional
from datetime import date, datetime
# Strawberry imports
import strawberry
import strawberry_django
from strawberry.types import Info
from strawberry.scalars import JSON
# Third-party library imports
from asgiref.sync import sync_to_async
# Module Imports
from plane.db.models import Cycle, Issue
@strawberry_django.type(Cycle)
class CycleType:
id: strawberry.ID
name: str
description: Optional[str]
start_date: Optional[date]
end_date: Optional[date]
owned_by: strawberry.ID
view_props: Optional[JSON]
sort_order: Optional[float]
external_source: Optional[str]
external_id: Optional[strawberry.ID]
progress_snapshot: Optional[JSON]
archived_at: Optional[datetime]
logo_props: Optional[JSON]
project: strawberry.ID
workspace: strawberry.ID
created_by: strawberry.ID
updated_by: strawberry.ID
created_at: datetime
updated_at: datetime
total_issues: int
completed_issues: int
@strawberry.field
def owned_by(self) -> int:
return self.owned_by_id
@strawberry.field
def project(self) -> int:
return self.project_id
@strawberry.field
def workspace(self) -> int:
return self.workspace_id
@strawberry.field
def created_by(self) -> int:
return self.created_by_id
@strawberry.field
async def total_issues(self, info: Info) -> int:
total_issues = await sync_to_async(
lambda: Issue.issue_objects.filter(
issue_cycle__cycle_id=self.id
).count()
)()
return total_issues
@strawberry.field
async def completed_issues(self, info: Info) -> int:
total_issues = await sync_to_async(
lambda: Issue.issue_objects.filter(
issue_cycle__cycle_id=self.id,
state__group="completed"
).count()
)()
return total_issues

View File

@@ -9,6 +9,8 @@ from asgiref.sync import sync_to_async
import strawberry
import strawberry_django
from strawberry.scalars import JSON
from strawberry.types import Info
# Module Imports
from plane.db.models import (
@@ -16,6 +18,8 @@ from plane.db.models import (
IssueUserProperty,
IssueActivity,
IssueComment,
CycleIssue,
ModuleIssue,
)
@@ -48,6 +52,8 @@ class IssueType:
created_at: datetime
updated_at: datetime
sequence_id: int
cycle: Optional[strawberry.ID]
modules: Optional[list[strawberry.ID]]
@strawberry.field
def state(self) -> int:
@@ -83,13 +89,28 @@ class IssueType:
labels = await sync_to_async(list)(self.labels.all())
return [label.id for label in labels]
# @strawberry.field
# async def attachments(self) -> list[JSON]:
# return await sync_to_async(list) list(
# IssueAttachment.objects.filter(issue_id=self.id).values(
# "id", "attributes", "asset"
# )
# )
@strawberry.field
async def cycle(self, info: Info) -> strawberry.ID:
cycle_issue = await sync_to_async(
CycleIssue.objects.filter(issue_id=self.id).first
)()
if cycle_issue:
return str(cycle_issue.cycle_id)
return None
@strawberry.field
async def modules(self, info: Info) -> list[strawberry.ID]:
# Fetch related module IDs in a synchronous context
module_issues = await sync_to_async(
lambda: list(
ModuleIssue.objects.filter(issue_id=self.id).values_list(
"module_id", flat=True
)
)
)()
# Return the module IDs as strings
return [str(module_id) for module_id in module_issues]
@strawberry_django.type(IssueUserProperty)

View File

@@ -0,0 +1,72 @@
# python imports
from typing import Optional
from datetime import date, datetime
# Strawberry imports
import strawberry
import strawberry_django
from strawberry.types import Info
from strawberry.scalars import JSON
# Module Imports
from plane.db.models import Module, Issue
# Third-party library imports
from asgiref.sync import sync_to_async
@strawberry_django.type(Module)
class ModuleType:
name: str
status: str
id: strawberry.ID
project: strawberry.ID
workspace: strawberry.ID
created_by: strawberry.ID
updated_by: strawberry.ID
created_at: datetime
updated_at: datetime
description: Optional[str]
description_text: Optional[str]
description_html: Optional[str]
start_date: Optional[date]
target_date: Optional[date]
lead: Optional[strawberry.ID]
members: Optional[list[strawberry.ID]]
view_props: Optional[JSON]
sort_order: float
external_source: Optional[str]
external_id: Optional[strawberry.ID]
archived_at: Optional[datetime]
logo_props: Optional[JSON]
total_issues: int
completed_issues: int
@strawberry.field
def project(self) -> int:
return self.project_id
@strawberry.field
def workspace(self) -> int:
return self.workspace_id
@strawberry.field
def created_by(self) -> int:
return self.created_by_id
@strawberry.field
async def total_issues(self, info: Info) -> int:
total_issues = await sync_to_async(
lambda: Issue.issue_objects.filter(
issue_module__module_id=self.id
).count()
)()
return total_issues
@strawberry.field
async def completed_issues(self, info: Info) -> int:
total_issues = await sync_to_async(
lambda: Issue.issue_objects.filter(
issue_module__module_id=self.id, state__group="completed"
).count()
)()
return total_issues

View File

@@ -5,10 +5,14 @@ from datetime import datetime
# Strawberry imports
import strawberry
import strawberry_django
from strawberry.types import Info
from strawberry.scalars import JSON
# Module imports
from plane.db.models import Project, ProjectMember
from plane.db.models import Project, ProjectMember, Issue
# Third-party library imports
from asgiref.sync import sync_to_async
@strawberry_django.type(Project)
@@ -37,6 +41,8 @@ class ProjectType:
archived_at: Optional[datetime]
is_member: bool
is_favorite: bool
total_members: int
total_issues: int
@strawberry.field
def workspace(self) -> int:
@@ -58,6 +64,22 @@ class ProjectType:
def default_state(self) -> int:
return self.default_state_id
@strawberry.field
async def total_members(self, info: strawberry.Info) -> int:
projects = await sync_to_async(
lambda: ProjectMember.objects.filter(
project_id=self.id, is_active=True
).count()
)()
return projects
@strawberry.field
async def total_issues(self, info: Info) -> int:
projects = await sync_to_async(
lambda: Issue.issue_objects.filter(project_id=self.id).count()
)()
return projects
@strawberry_django.type(ProjectMember)
class ProjectMemberType: