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
2 changes: 2 additions & 0 deletions apiserver/plane/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)

from .module import (
Expand Down
63 changes: 63 additions & 0 deletions apiserver/plane/app/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
IssueVote,
IssueRelation,
State,
IssueVersion,
IssueDescriptionVersion,
)


Expand Down Expand Up @@ -667,3 +669,64 @@ class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]


class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]

Comment on lines +674 to +711
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

Remove the duplicate 'name' field.

The name field appears twice in the fields list at lines 685 and 702.

Apply this diff to remove the duplicate:

             "type",
             "cycle",
             "modules",
             "meta",
-            "name",
             "last_saved_at",
             "owned_by",
📝 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
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]

Comment on lines +674 to +711
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

Fix duplicate field and improve code organization.

The serializer has the following issues:

  1. The field 'name' is duplicated (lines 685 and 702)
  2. Missing class-level docstring to explain the serializer's purpose
  3. Related fields could be grouped together for better readability

Apply this diff to fix the issues:

 class IssueVersionDetailSerializer(BaseSerializer):
+    """Serializer for IssueVersion model to track historical versions of issues."""
+
     class Meta:
         model = IssueVersion
         fields = [
             "id",
             # Relationship fields
             "workspace",
             "project",
             "issue",
             "parent",
             "state",
             "cycle",
             "modules",
             # Core issue fields
             "name",
             "priority",
             "estimate_point",
             "sequence_id",
             "sort_order",
             # Date fields
             "start_date",
             "target_date",
             "completed_at",
             "archived_at",
             "last_saved_at",
             "created_at",
             "updated_at",
             # Relationship arrays
             "assignees",
             "labels",
             # Status flags
             "is_draft",
             # External references
             "external_source",
             "external_id",
             "type",
             # Additional data
             "meta",
             # Ownership
             "owned_by",
             "created_by",
             "updated_by",
-            "name",  # Remove duplicate field
         ]
         read_only_fields = ["workspace", "project", "issue"]
📝 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
class IssueVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueVersion
fields = [
"id",
"workspace",
"project",
"issue",
"parent",
"state",
"estimate_point",
"name",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
"is_draft",
"external_source",
"external_id",
"type",
"cycle",
"modules",
"meta",
"name",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
class IssueVersionDetailSerializer(BaseSerializer):
"""Serializer for IssueVersion model to track historical versions of issues."""
class Meta:
model = IssueVersion
fields = [
"id",
# Relationship fields
"workspace",
"project",
"issue",
"parent",
"state",
"cycle",
"modules",
# Core issue fields
"name",
"priority",
"estimate_point",
"sequence_id",
"sort_order",
# Date fields
"start_date",
"target_date",
"completed_at",
"archived_at",
"last_saved_at",
"created_at",
"updated_at",
# Relationship arrays
"assignees",
"labels",
# Status flags
"is_draft",
# External references
"external_source",
"external_id",
"type",
# Additional data
"meta",
# Ownership
"owned_by",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]


class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"project",
"issue",
"description_binary",
"description_html",
"description_stripped",
"description_json",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "issue"]
22 changes: 22 additions & 0 deletions apiserver/plane/app/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
)

urlpatterns = [
Expand Down Expand Up @@ -256,4 +258,24 @@
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
Comment on lines +261 to +280
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

Fix inconsistent URL pattern names.

The URL pattern names "page-versions" seem incorrect for issue-related endpoints. Consider using more appropriate names like "issue-versions" and "issue-description-versions".

 path(
     "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
     IssueVersionEndpoint.as_view(),
-    name="page-versions",
+    name="issue-versions",
 ),
 path(
     "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
     IssueVersionEndpoint.as_view(),
-    name="page-versions",
+    name="issue-version-detail",
 ),
 path(
     "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
     IssueDescriptionVersionEndpoint.as_view(),
-    name="page-versions",
+    name="issue-description-versions",
 ),
 path(
     "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
     IssueDescriptionVersionEndpoint.as_view(),
-    name="page-versions",
+    name="issue-description-version-detail",
 ),
📝 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
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="issue-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="issue-version-detail",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/",
IssueDescriptionVersionEndpoint.as_view(),
name="issue-description-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/description-versions/<uuid:pk>/",
IssueDescriptionVersionEndpoint.as_view(),
name="issue-description-version-detail",
),

]
2 changes: 2 additions & 0 deletions apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@

from .issue.subscriber import IssueSubscriberViewSet

from .issue.version import IssueVersionEndpoint, IssueDescriptionVersionEndpoint

from .module.base import (
ModuleViewSet,
ModuleLinkViewSet,
Expand Down
118 changes: 118 additions & 0 deletions apiserver/plane/app/views/issue/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response

# Module imports
from plane.db.models import IssueVersion, IssueDescriptionVersion
from ..base import BaseAPIView
from plane.app.serializers import (
IssueVersionDetailSerializer,
IssueDescriptionVersionDetailSerializer,
)
from plane.app.permissions import allow_permission, ROLE
from plane.utils.global_paginator import paginate
from plane.utils.timezone_converter import user_timezone_converter


class IssueVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)

datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)

return paginated_data

@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
Comment on lines +31 to +33
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

Handle exceptions when retrieving objects with .get().

The IssueVersion.objects.get() method will raise an exception if the object does not exist, leading to a server error. To prevent this, consider handling the IssueVersion.DoesNotExist exception and returning an appropriate 404 response.

Apply this fix:

 def get(self, request, slug, project_id, issue_id, pk=None):
     if pk:
-        issue_version = IssueVersion.objects.get(
+        try:
+            issue_version = IssueVersion.objects.get(
                 workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
+            )
+        except IssueVersion.DoesNotExist:
+            return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

         serializer = IssueVersionDetailSerializer(issue_version)
         return Response(serializer.data, status=status.HTTP_200_OK)
📝 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
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
try:
issue_version = IssueVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
except IssueVersion.DoesNotExist:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = IssueVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)

Comment on lines +31 to +33
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 error handling for non-existent objects.

The .get() calls could raise DoesNotExist exceptions. Consider adding proper error handling.

+from django.core.exceptions import ObjectDoesNotExist
+from rest_framework import status

 class IssueVersionEndpoint(BaseAPIView):
     def get(self, request, slug, project_id, issue_id, pk=None):
         if pk:
-            issue_version = IssueVersion.objects.get(
-                workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
-            )
+            try:
+                issue_version = IssueVersion.objects.get(
+                    workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
+                )
+            except ObjectDoesNotExist:
+                return Response(
+                    {"error": "Issue version not found"}, 
+                    status=status.HTTP_404_NOT_FOUND
+                )

Also applies to: 83-85


serializer = IssueVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)

cursor = request.GET.get("cursor", None)

required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]

issue_versions_queryset = IssueVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)

paginated_data = paginate(
base_queryset=issue_versions_queryset,
queryset=issue_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)

return Response(paginated_data, status=status.HTTP_200_OK)


class IssueDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)

datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
paginated_data, datetime_fields, timezone
)

return paginated_data

@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
issue_description_version = IssueDescriptionVersion.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
Comment on lines +83 to +85
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

Handle exceptions when retrieving objects with .get().

The IssueDescriptionVersion.objects.get() method will raise an exception if the object does not exist, leading to a server error. To prevent this, consider handling the IssueDescriptionVersion.DoesNotExist exception and returning an appropriate 404 response.

Apply this fix:

 def get(self, request, slug, project_id, issue_id, pk=None):
     if pk:
-        issue_description_version = IssueDescriptionVersion.objects.get(
+        try:
+            issue_description_version = IssueDescriptionVersion.objects.get(
                 workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
+            )
+        except IssueDescriptionVersion.DoesNotExist:
+            return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

         serializer = IssueDescriptionVersionDetailSerializer(
             issue_description_version

Committable suggestion skipped: line range outside the PR's diff.


serializer = IssueDescriptionVersionDetailSerializer(
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK)

cursor = request.GET.get("cursor", None)

required_fields = [
"id",
"workspace",
"project",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]

issue_description_versions_queryset = IssueDescriptionVersion.objects.filter(
workspace__slug=slug, project_id=project_id, issue_id=issue_id
)
paginated_data = paginate(
base_queryset=issue_description_versions_queryset,
queryset=issue_description_versions_queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
),
)
return Response(paginated_data, status=status.HTTP_200_OK)
Loading