From 75ea97f9dd538de68b461b9bd3a56c1e6eb496f3 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 23 Jul 2025 14:24:59 +0530 Subject: [PATCH 1/2] chore: added field validations in serializer --- apps/api/plane/api/serializers/issue.py | 17 ++++- apps/api/plane/app/serializers/draft.py | 89 ++++++++++++++++++++++--- apps/api/plane/app/serializers/issue.py | 66 ++++++++++++++++-- 3 files changed, 156 insertions(+), 16 deletions(-) diff --git a/apps/api/plane/api/serializers/issue.py b/apps/api/plane/api/serializers/issue.py index f906d4085f3..f3b0ada96ab 100644 --- a/apps/api/plane/api/serializers/issue.py +++ b/apps/api/plane/api/serializers/issue.py @@ -20,6 +20,7 @@ ProjectMember, State, User, + EstimatePoint, ) from .base import BaseSerializer @@ -105,13 +106,27 @@ def validate(self, data): if ( data.get("parent") and not Issue.objects.filter( - workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id + workspace_id=self.context.get("workspace_id"), + project_id=self.context.get("project_id"), + pk=data.get("parent").id, ).exists() ): raise serializers.ValidationError( "Parent is not valid issue_id please pass a valid issue_id" ) + if ( + data.get("estimate_point") + and not EstimatePoint.objects.filter( + workspace_id=self.context.get("workspace_id"), + project_id=self.context.get("project_id"), + pk=data.get("estimate_point").id, + ).exists() + ): + raise serializers.ValidationError( + "Estimate point is not valid please pass a valid estimate_point_id" + ) + return data def create(self, validated_data): diff --git a/apps/api/plane/app/serializers/draft.py b/apps/api/plane/app/serializers/draft.py index f308352633b..d32cd718e69 100644 --- a/apps/api/plane/app/serializers/draft.py +++ b/apps/api/plane/app/serializers/draft.py @@ -1,3 +1,5 @@ +from lxml import html + # Django imports from django.utils import timezone @@ -16,6 +18,8 @@ DraftIssueLabel, DraftIssueCycle, DraftIssueModule, + ProjectMember, + EstimatePoint, ) @@ -57,14 +61,77 @@ def to_representation(self, instance): data["label_ids"] = label_ids if label_ids else [] return data - def validate(self, data): + def validate(self, attrs): 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) + attrs.get("start_date", None) is not None + and attrs.get("target_date", None) is not None + and attrs.get("start_date", None) > attrs.get("target_date", None) ): raise serializers.ValidationError("Start date cannot exceed target date") - return data + + try: + if attrs.get("description_html", None) is not None: + parsed = html.fromstring(attrs["description_html"]) + parsed_str = html.tostring(parsed, encoding="unicode") + attrs["description_html"] = parsed_str + + except Exception: + raise serializers.ValidationError("Invalid HTML passed") + + # Validate assignees are from project + if attrs.get("assignee_ids", []): + attrs["assignee_ids"] = ProjectMember.objects.filter( + project_id=self.context["project_id"], + role__gte=15, + is_active=True, + member_id__in=attrs["assignee_ids"], + ).values_list("member_id", flat=True) + + # Validate labels are from project + if attrs.get("label_ids"): + label_ids = [label.id for label in attrs["label_ids"]] + attrs["label_ids"] = list( + Label.objects.filter( + project_id=self.context.get("project_id"), id__in=label_ids + ).values_list("id", flat=True) + ) + + # # Check state is from the project only else raise validation error + if ( + attrs.get("state") + and not State.objects.filter( + project_id=self.context.get("project_id"), + pk=attrs.get("state").id, + ).exists() + ): + raise serializers.ValidationError( + "State is not valid please pass a valid state_id" + ) + + # # Check parent issue is from workspace as it can be cross workspace + if ( + attrs.get("parent") + and not Issue.objects.filter( + project_id=self.context.get("project_id"), + pk=attrs.get("parent").id, + ).exists() + ): + raise serializers.ValidationError( + "Parent is not valid issue_id please pass a valid issue_id" + ) + + if ( + attrs.get("estimate_point") + and not EstimatePoint.objects.filter( + project_id=self.context.get("project_id"), + pk=attrs.get("estimate_point").id, + ).exists() + ): + raise serializers.ValidationError( + "Estimate point is not valid please pass a valid estimate_point_id" + ) + + return attrs def create(self, validated_data): assignees = validated_data.pop("assignee_ids", None) @@ -89,14 +156,14 @@ def create(self, validated_data): DraftIssueAssignee.objects.bulk_create( [ DraftIssueAssignee( - assignee=user, + assignee_id=assignee_id, draft_issue=issue, workspace_id=workspace_id, project_id=project_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for user in assignees + for assignee_id in assignees ], batch_size=10, ) @@ -105,14 +172,14 @@ def create(self, validated_data): DraftIssueLabel.objects.bulk_create( [ DraftIssueLabel( - label=label, + label_id=label_id, draft_issue=issue, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for label in labels + for label_id in labels ], batch_size=10, ) @@ -163,14 +230,14 @@ def update(self, instance, validated_data): DraftIssueAssignee.objects.bulk_create( [ DraftIssueAssignee( - assignee=user, + assignee_id=assignee_id, draft_issue=instance, workspace_id=workspace_id, project_id=project_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for user in assignees + for assignee_id in assignees ], batch_size=10, ) diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index 965d78aa2b6..7f3301126dc 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -1,3 +1,5 @@ +from lxml import html + # Django imports from django.utils import timezone from django.core.validators import URLValidator @@ -37,6 +39,7 @@ IssueVersion, IssueDescriptionVersion, ProjectMember, + EstimatePoint, ) @@ -119,6 +122,16 @@ def validate(self, attrs): ): raise serializers.ValidationError("Start date cannot exceed target date") + try: + if attrs.get("description_html", None) is not None: + parsed = html.fromstring(attrs["description_html"]) + parsed_str = html.tostring(parsed, encoding="unicode") + attrs["description_html"] = parsed_str + + except Exception: + raise serializers.ValidationError("Invalid HTML passed") + + # Validate assignees are from project if attrs.get("assignee_ids", []): attrs["assignee_ids"] = ProjectMember.objects.filter( project_id=self.context["project_id"], @@ -127,6 +140,51 @@ def validate(self, attrs): member_id__in=attrs["assignee_ids"], ).values_list("member_id", flat=True) + # Validate labels are from project + if attrs.get("label_ids"): + label_ids = [label.id for label in attrs["label_ids"]] + attrs["label_ids"] = list( + Label.objects.filter( + project_id=self.context.get("project_id"), + id__in=label_ids, + ).values_list("id", flat=True) + ) + + # Check state is from the project only else raise validation error + if ( + attrs.get("state") + and not State.objects.filter( + project_id=self.context.get("project_id"), + pk=attrs.get("state").id, + ).exists() + ): + raise serializers.ValidationError( + "State is not valid please pass a valid state_id" + ) + + # Check parent issue is from workspace as it can be cross workspace + if ( + attrs.get("parent") + and not Issue.objects.filter( + project_id=self.context.get("project_id"), + pk=attrs.get("parent").id, + ).exists() + ): + raise serializers.ValidationError( + "Parent is not valid issue_id please pass a valid issue_id" + ) + + if ( + attrs.get("estimate_point") + and not EstimatePoint.objects.filter( + project_id=self.context.get("project_id"), + pk=attrs.get("estimate_point").id, + ).exists() + ): + raise serializers.ValidationError( + "Estimate point is not valid please pass a valid estimate_point_id" + ) + return attrs def create(self, validated_data): @@ -190,14 +248,14 @@ def create(self, validated_data): IssueLabel.objects.bulk_create( [ IssueLabel( - label=label, + label_id=label_id, issue=issue, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for label in labels + for label_id in labels ], batch_size=10, ) @@ -243,14 +301,14 @@ def update(self, instance, validated_data): IssueLabel.objects.bulk_create( [ IssueLabel( - label=label, + label_id=label_id, issue=instance, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for label in labels + for label_id in labels ], batch_size=10, ignore_conflicts=True, From 01354d80dc9ff77a5c2f0f76c2016a1ae569f39d Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 23 Jul 2025 23:44:16 +0530 Subject: [PATCH 2/2] chore: added enum for roles --- apps/api/plane/app/serializers/draft.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/app/serializers/draft.py b/apps/api/plane/app/serializers/draft.py index d32cd718e69..86a5e3686e1 100644 --- a/apps/api/plane/app/serializers/draft.py +++ b/apps/api/plane/app/serializers/draft.py @@ -21,6 +21,7 @@ ProjectMember, EstimatePoint, ) +from plane.app.permissions import ROLE class DraftIssueCreateSerializer(BaseSerializer): @@ -82,7 +83,7 @@ def validate(self, attrs): if attrs.get("assignee_ids", []): attrs["assignee_ids"] = ProjectMember.objects.filter( project_id=self.context["project_id"], - role__gte=15, + role__gte=ROLE.MEMBER.value, is_active=True, member_id__in=attrs["assignee_ids"], ).values_list("member_id", flat=True)