diff --git a/apps/api/plane/api/serializers/issue.py b/apps/api/plane/api/serializers/issue.py index c57f9d359..20f967e3b 100644 --- a/apps/api/plane/api/serializers/issue.py +++ b/apps/api/plane/api/serializers/issue.py @@ -20,6 +20,7 @@ from plane.db.models import ( ProjectMember, State, User, + EstimatePoint, ) from plane.utils.content_validator import ( validate_html_content, @@ -126,13 +127,27 @@ class IssueSerializer(BaseSerializer): 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 6479d44c8..57600bff9 100644 --- a/apps/api/plane/app/serializers/draft.py +++ b/apps/api/plane/app/serializers/draft.py @@ -16,12 +16,15 @@ from plane.db.models import ( DraftIssueLabel, DraftIssueCycle, DraftIssueModule, + ProjectMember, + EstimatePoint, ) from plane.utils.content_validator import ( validate_html_content, validate_json_content, validate_binary_data, ) +from plane.app.permissions import ROLE class DraftIssueCreateSerializer(BaseSerializer): @@ -62,31 +65,84 @@ class DraftIssueCreateSerializer(BaseSerializer): 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") # Validate description content for security - if "description" in data and data["description"]: - is_valid, error_msg = validate_json_content(data["description"]) + if "description" in attrs and attrs["description"]: + is_valid, error_msg = validate_json_content(attrs["description"]) if not is_valid: raise serializers.ValidationError({"description": error_msg}) - if "description_html" in data and data["description_html"]: - is_valid, error_msg = validate_html_content(data["description_html"]) + if "description_html" in attrs and attrs["description_html"]: + is_valid, error_msg = validate_html_content(attrs["description_html"]) if not is_valid: raise serializers.ValidationError({"description_html": error_msg}) - if "description_binary" in data and data["description_binary"]: - is_valid, error_msg = validate_binary_data(data["description_binary"]) + if "description_binary" in attrs and attrs["description_binary"]: + is_valid, error_msg = validate_binary_data(attrs["description_binary"]) if not is_valid: raise serializers.ValidationError({"description_binary": error_msg}) - return data + # Validate assignees are from project + if attrs.get("assignee_ids", []): + attrs["assignee_ids"] = ProjectMember.objects.filter( + project_id=self.context["project_id"], + role__gte=ROLE.MEMBER.value, + 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) @@ -111,14 +167,14 @@ class DraftIssueCreateSerializer(BaseSerializer): 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, ) @@ -127,14 +183,14 @@ class DraftIssueCreateSerializer(BaseSerializer): 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, ) @@ -185,14 +241,14 @@ class DraftIssueCreateSerializer(BaseSerializer): 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 98ceaaad6..897326431 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -37,6 +37,7 @@ from plane.db.models import ( IssueVersion, IssueDescriptionVersion, ProjectMember, + EstimatePoint, ) from plane.utils.content_validator import ( validate_html_content, @@ -124,14 +125,6 @@ class IssueCreateSerializer(BaseSerializer): ): raise serializers.ValidationError("Start date cannot exceed target date") - 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 description content for security if "description" in attrs and attrs["description"]: is_valid, error_msg = validate_json_content(attrs["description"]) @@ -148,6 +141,60 @@ class IssueCreateSerializer(BaseSerializer): if not is_valid: raise serializers.ValidationError({"description_binary": error_msg}) + # 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): @@ -211,14 +258,14 @@ class IssueCreateSerializer(BaseSerializer): 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, ) @@ -264,14 +311,14 @@ class IssueCreateSerializer(BaseSerializer): 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,