Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apiserver/plane/api/views/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def get(self, request, slug, project_id, pk=None):
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now().date())
Q(end_date__gte=timezone.now())
| Q(end_date__isnull=True),
)
return self.paginate(
Expand Down Expand Up @@ -311,7 +311,7 @@ def patch(self, request, slug, project_id, pk):

if (
cycle.end_date is not None
and cycle.end_date < timezone.now().date()
and cycle.end_date < timezone.now()
):
if "sort_order" in request_data:
# Can only change sort order
Expand Down Expand Up @@ -537,7 +537,7 @@ def post(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
if cycle.end_date >= timezone.now().date():
if cycle.end_date >= timezone.now():
return Response(
{"error": "Only completed cycles can be archived"},
status=status.HTTP_400_BAD_REQUEST,
Expand Down Expand Up @@ -1146,7 +1146,7 @@ def post(self, request, slug, project_id, cycle_id):

if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now().date()
and new_cycle.end_date < timezone.now()
):
return Response(
{
Expand Down
6 changes: 6 additions & 0 deletions apiserver/plane/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,9 @@
from .dashboard import DashboardSerializer, WidgetSerializer

from .favorite import UserFavoriteSerializer

from .draft import (
DraftIssueCreateSerializer,
DraftIssueSerializer,
DraftIssueDetailSerializer,
)
278 changes: 278 additions & 0 deletions apiserver/plane/app/serializers/draft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
# Django imports
from django.utils import timezone

# Third Party imports
from rest_framework import serializers

# Module imports
from .base import BaseSerializer
from plane.db.models import (
User,
Issue,
Label,
State,
DraftIssue,
DraftIssueAssignee,
DraftIssueLabel,
DraftIssueCycle,
DraftIssueModule,
)


class DraftIssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state",
queryset=State.objects.all(),
required=False,
allow_null=True,
)
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
Comment on lines +36 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure consistent field definitions across serializers.

In DraftIssueCreateSerializer, label_ids and assignee_ids are defined as ListField with PrimaryKeyRelatedField children:

label_ids = serializers.ListField(
    child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
    write_only=True,
    required=False,
)

assignee_ids = serializers.ListField(
    child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
    write_only=True,
    required=False,
)

However, in DraftIssueSerializer, these fields are defined with UUIDField children:

label_ids = serializers.ListField(
    child=serializers.UUIDField(),
    required=False,
)

assignee_ids = serializers.ListField(
    child=serializers.UUIDField(),
    required=False,
)

This inconsistency may lead to confusion and potential validation issues. Consider standardizing the field definitions across serializers to ensure reliability.

Also applies to: 41-45

assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
Comment on lines +36 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing fields cycle_id and module_ids to DraftIssueCreateSerializer.

The create and update methods utilize cycle_id and module_ids, but these fields are not defined in the serializer. This omission can lead to these fields not being validated or properly processed.

Consider adding cycle_id and module_ids as serializer fields similar to label_ids and assignee_ids:

cycle_id = serializers.PrimaryKeyRelatedField(
    queryset=Cycle.objects.all(),
    write_only=True,
    required=False,
    allow_null=True,
)

module_ids = serializers.ListField(
    child=serializers.PrimaryKeyRelatedField(queryset=Module.objects.all()),
    write_only=True,
    required=False,
)


class Meta:
model = DraftIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
]

def to_representation(self, instance):
data = super().to_representation(instance)
assignee_ids = self.initial_data.get("assignee_ids")
data["assignee_ids"] = assignee_ids if assignee_ids else []
label_ids = self.initial_data.get("label_ids")
data["label_ids"] = label_ids if label_ids else []
return data
Comment on lines +59 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Retrieve data from instance rather than initial_data in to_representation.

Using self.initial_data in the to_representation method may not accurately reflect the current state of the instance, especially during updates. It's better to fetch assignee_ids and label_ids from the instance to ensure the serialized output is correct.

Apply this diff to fix the issue:

 def to_representation(self, instance):
     data = super().to_representation(instance)
-    assignee_ids = self.initial_data.get("assignee_ids")
+    assignee_ids = instance.assignees.values_list('assignee_id', flat=True)
     data["assignee_ids"] = list(assignee_ids) if assignee_ids else []
-    label_ids = self.initial_data.get("label_ids")
+    label_ids = instance.labels.values_list('label_id', flat=True)
     data["label_ids"] = list(label_ids) if label_ids else []
     return data
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def to_representation(self, instance):
data = super().to_representation(instance)
assignee_ids = self.initial_data.get("assignee_ids")
data["assignee_ids"] = assignee_ids if assignee_ids else []
label_ids = self.initial_data.get("label_ids")
data["label_ids"] = label_ids if label_ids else []
return data
def to_representation(self, instance):
data = super().to_representation(instance)
assignee_ids = instance.assignees.values_list('assignee_id', flat=True)
data["assignee_ids"] = list(assignee_ids) if assignee_ids else []
label_ids = instance.labels.values_list('label_id', flat=True)
data["label_ids"] = list(label_ids) if label_ids else []
return data


def validate(self, data):
if (
data.get("start_date", None) is not None
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data

def create(self, validated_data):
assignees = validated_data.pop("assignee_ids", None)
labels = validated_data.pop("label_ids", None)
modules = validated_data.pop("module_ids", None)
cycle_id = self.initial_data.get("cycle_id", None)
modules = self.initial_data.get("module_ids", None)
Comment on lines +81 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid redundant assignment and inconsistent data sources for modules.

In the create method, modules is first extracted from validated_data and then immediately overwritten by self.initial_data, which may lead to confusion and potential errors:

modules = validated_data.pop("module_ids", None)
modules = self.initial_data.get("module_ids", None)

Consider removing the redundant assignment and consistently using validated_data to ensure that the data has been properly validated.


workspace_id = self.context["workspace_id"]

# Create Issue
issue = DraftIssue.objects.create(
**validated_data,
workspace_id=workspace_id,
)

# Issue Audit Users
created_by_id = issue.created_by_id
updated_by_id = issue.updated_by_id

if assignees is not None and len(assignees):
DraftIssueAssignee.objects.bulk_create(
[
DraftIssueAssignee(
assignee=user,
issue=issue,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
)

if labels is not None and len(labels):
DraftIssueLabel.objects.bulk_create(
[
DraftIssueLabel(
label=label,
issue=issue,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)

if cycle_id is not None:
DraftIssueCycle.objects.create(
cycle_id=cycle_id,
draft_issue=issue,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)

if modules is not None and len(modules):
DraftIssueModule.objects.bulk_create(
[
DraftIssueModule(
module_id=module_id,
draft_issue=issue,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for module_id in modules
],
batch_size=10,
)

return issue

def update(self, instance, validated_data):
assignees = validated_data.pop("assignee_ids", None)
labels = validated_data.pop("label_ids", None)
cycle_id = self.initial_data.get("cycle_id", None)
modules = self.initial_data.get("module_ids", None)

Comment on lines +154 to +158
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consistently use validated_data in the update method for cycle_id and module_ids.

In the update method, cycle_id and module_ids are retrieved from self.initial_data instead of validated_data, bypassing serializer validation:

cycle_id = self.initial_data.get("cycle_id", None)
modules = self.initial_data.get("module_ids", None)

It's recommended to extract these fields from validated_data to ensure they are properly validated before use.

# Related models
workspace_id = instance.workspace_id
created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id

if assignees is not None:
DraftIssueAssignee.objects.filter(issue=instance).delete()
DraftIssueAssignee.objects.bulk_create(
[
DraftIssueAssignee(
assignee=user,
issue=instance,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user in assignees
],
batch_size=10,
)

if labels is not None:
DraftIssueLabel.objects.filter(issue=instance).delete()
DraftIssueLabel.objects.bulk_create(
[
DraftIssueLabel(
label=label,
issue=instance,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for label in labels
],
batch_size=10,
)

if cycle_id is not None:
DraftIssueCycle.objects.filter(draft_issue=instance).delete()
DraftIssueCycle.objects.create(
cycle_id=cycle_id,
draft_issue=instance,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)

if modules is not None:
DraftIssueModule.objects.filter(draft_issue=instance).delete()
DraftIssueModule.objects.bulk_create(
[
DraftIssueModule(
module=module,
draft_issue=instance,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for module in modules
],
batch_size=10,
)

# Time updation occurs even when other related models are updated
instance.updated_at = timezone.now()
return super().update(instance, validated_data)


class DraftIssueSerializer(BaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
Comment on lines +229 to +233
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure cycle_id and module_ids are properly defined in DraftIssueSerializer.

In DraftIssueSerializer, cycle_id is set as a read-only field without specifying a queryset, and module_ids is defined with UUIDField children. For consistency and proper validation, consider defining them with appropriate field types and querysets.


# Many to many
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)

class Meta:
model = DraftIssue
fields = [
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"project_id",
"parent_id",
"cycle_id",
"module_ids",
"label_ids",
"assignee_ids",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = fields


class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()

class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
]
read_only_fields = fields
23 changes: 0 additions & 23 deletions apiserver/plane/app/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
IssueActivityEndpoint,
IssueArchiveViewSet,
IssueCommentViewSet,
IssueDraftViewSet,
IssueListEndpoint,
IssueReactionViewSet,
IssueRelationViewSet,
Expand Down Expand Up @@ -290,28 +289,6 @@
name="issue-relation",
),
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/deleted-issues/",
DeletedIssuesListViewSet.as_view(),
Expand Down
22 changes: 22 additions & 0 deletions apiserver/plane/app/urls/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
WorkspaceCyclesEndpoint,
WorkspaceFavoriteEndpoint,
WorkspaceFavoriteGroupEndpoint,
WorkspaceDraftIssueViewSet,
)


Expand Down Expand Up @@ -254,4 +255,25 @@
WorkspaceFavoriteGroupEndpoint.as_view(),
name="workspace-user-favorites-groups",
),
path(
"workspaces/<str:slug>/draft-issues/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-draft-issues",
),
path(
"workspaces/<str:slug>/draft-issues/<uuid:pk>/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-drafts-issues",
),
]
Loading