* chore: changed the html validation * chore: added requirements for nh3 * chore: removed the json validations
260 lines
8.5 KiB
Python
260 lines
8.5 KiB
Python
# Third party imports
|
|
from rest_framework import serializers
|
|
|
|
# Module imports
|
|
from plane.db.models import (
|
|
Project,
|
|
ProjectIdentifier,
|
|
WorkspaceMember,
|
|
State,
|
|
Estimate,
|
|
)
|
|
|
|
from plane.utils.content_validator import (
|
|
validate_html_content,
|
|
)
|
|
from .base import BaseSerializer
|
|
|
|
|
|
class ProjectCreateSerializer(BaseSerializer):
|
|
"""
|
|
Serializer for creating projects with workspace validation.
|
|
|
|
Handles project creation including identifier validation, member verification,
|
|
and workspace association for new project initialization.
|
|
"""
|
|
|
|
class Meta:
|
|
model = Project
|
|
fields = [
|
|
"name",
|
|
"description",
|
|
"project_lead",
|
|
"default_assignee",
|
|
"identifier",
|
|
"icon_prop",
|
|
"emoji",
|
|
"cover_image",
|
|
"module_view",
|
|
"cycle_view",
|
|
"issue_views_view",
|
|
"page_view",
|
|
"intake_view",
|
|
"guest_view_all_features",
|
|
"archive_in",
|
|
"close_in",
|
|
"timezone",
|
|
"logo_props",
|
|
"external_source",
|
|
"external_id",
|
|
"is_issue_type_enabled",
|
|
]
|
|
|
|
read_only_fields = [
|
|
"id",
|
|
"workspace",
|
|
"created_at",
|
|
"updated_at",
|
|
"created_by",
|
|
"updated_by",
|
|
]
|
|
|
|
def validate(self, data):
|
|
if data.get("project_lead", None) is not None:
|
|
# Check if the project lead is a member of the workspace
|
|
if not WorkspaceMember.objects.filter(
|
|
workspace_id=self.context["workspace_id"],
|
|
member_id=data.get("project_lead"),
|
|
).exists():
|
|
raise serializers.ValidationError(
|
|
"Project lead should be a user in the workspace"
|
|
)
|
|
|
|
if data.get("default_assignee", None) is not None:
|
|
# Check if the default assignee is a member of the workspace
|
|
if not WorkspaceMember.objects.filter(
|
|
workspace_id=self.context["workspace_id"],
|
|
member_id=data.get("default_assignee"),
|
|
).exists():
|
|
raise serializers.ValidationError(
|
|
"Default assignee should be a user in the workspace"
|
|
)
|
|
|
|
return data
|
|
|
|
def create(self, validated_data):
|
|
identifier = validated_data.get("identifier", "").strip().upper()
|
|
if identifier == "":
|
|
raise serializers.ValidationError(detail="Project Identifier is required")
|
|
|
|
if ProjectIdentifier.objects.filter(
|
|
name=identifier, workspace_id=self.context["workspace_id"]
|
|
).exists():
|
|
raise serializers.ValidationError(detail="Project Identifier is taken")
|
|
|
|
project = Project.objects.create(
|
|
**validated_data, workspace_id=self.context["workspace_id"]
|
|
)
|
|
return project
|
|
|
|
|
|
class ProjectUpdateSerializer(ProjectCreateSerializer):
|
|
"""
|
|
Serializer for updating projects with enhanced state and estimation management.
|
|
|
|
Extends project creation with update-specific validations including default state
|
|
assignment, estimation configuration, and project setting modifications.
|
|
"""
|
|
|
|
class Meta(ProjectCreateSerializer.Meta):
|
|
model = Project
|
|
fields = ProjectCreateSerializer.Meta.fields + [
|
|
"default_state",
|
|
"estimate",
|
|
]
|
|
|
|
read_only_fields = ProjectCreateSerializer.Meta.read_only_fields
|
|
|
|
def update(self, instance, validated_data):
|
|
"""Update a project"""
|
|
if (
|
|
validated_data.get("default_state", None) is not None
|
|
and not State.objects.filter(
|
|
project=instance, id=validated_data.get("default_state")
|
|
).exists()
|
|
):
|
|
# Check if the default state is a state in the project
|
|
raise serializers.ValidationError(
|
|
"Default state should be a state in the project"
|
|
)
|
|
|
|
if (
|
|
validated_data.get("estimate", None) is not None
|
|
and not Estimate.objects.filter(
|
|
project=instance, id=validated_data.get("estimate")
|
|
).exists()
|
|
):
|
|
# Check if the estimate is a estimate in the project
|
|
raise serializers.ValidationError(
|
|
"Estimate should be a estimate in the project"
|
|
)
|
|
return super().update(instance, validated_data)
|
|
|
|
|
|
class ProjectSerializer(BaseSerializer):
|
|
"""
|
|
Comprehensive project serializer with metrics and member context.
|
|
|
|
Provides complete project data including member counts, cycle/module totals,
|
|
deployment status, and user-specific context for project management.
|
|
"""
|
|
|
|
total_members = serializers.IntegerField(read_only=True)
|
|
total_cycles = serializers.IntegerField(read_only=True)
|
|
total_modules = serializers.IntegerField(read_only=True)
|
|
is_member = serializers.BooleanField(read_only=True)
|
|
sort_order = serializers.FloatField(read_only=True)
|
|
member_role = serializers.IntegerField(read_only=True)
|
|
is_deployed = serializers.BooleanField(read_only=True)
|
|
cover_image_url = serializers.CharField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Project
|
|
fields = "__all__"
|
|
read_only_fields = [
|
|
"id",
|
|
"emoji",
|
|
"workspace",
|
|
"created_at",
|
|
"updated_at",
|
|
"created_by",
|
|
"updated_by",
|
|
"deleted_at",
|
|
"cover_image_url",
|
|
]
|
|
|
|
def validate(self, data):
|
|
# Check project lead should be a member of the workspace
|
|
if (
|
|
data.get("project_lead", None) is not None
|
|
and not WorkspaceMember.objects.filter(
|
|
workspace_id=self.context["workspace_id"],
|
|
member_id=data.get("project_lead"),
|
|
).exists()
|
|
):
|
|
raise serializers.ValidationError(
|
|
"Project lead should be a user in the workspace"
|
|
)
|
|
|
|
# Check default assignee should be a member of the workspace
|
|
if (
|
|
data.get("default_assignee", None) is not None
|
|
and not WorkspaceMember.objects.filter(
|
|
workspace_id=self.context["workspace_id"],
|
|
member_id=data.get("default_assignee"),
|
|
).exists()
|
|
):
|
|
raise serializers.ValidationError(
|
|
"Default assignee should be a user in the workspace"
|
|
)
|
|
|
|
# Validate description content for security
|
|
if "description_html" in data and data["description_html"]:
|
|
if isinstance(data["description_html"], dict):
|
|
is_valid, error_msg, sanitized_html = validate_html_content(
|
|
str(data["description_html"])
|
|
)
|
|
# Update the data with sanitized HTML if available
|
|
if sanitized_html is not None:
|
|
data["description_html"] = sanitized_html
|
|
if not is_valid:
|
|
raise serializers.ValidationError(
|
|
{"error": "html content is not valid"}
|
|
)
|
|
|
|
return data
|
|
|
|
def create(self, validated_data):
|
|
identifier = validated_data.get("identifier", "").strip().upper()
|
|
if identifier == "":
|
|
raise serializers.ValidationError(detail="Project Identifier is required")
|
|
|
|
if ProjectIdentifier.objects.filter(
|
|
name=identifier, workspace_id=self.context["workspace_id"]
|
|
).exists():
|
|
raise serializers.ValidationError(detail="Project Identifier is taken")
|
|
|
|
project = Project.objects.create(
|
|
**validated_data, workspace_id=self.context["workspace_id"]
|
|
)
|
|
_ = ProjectIdentifier.objects.create(
|
|
name=project.identifier,
|
|
project=project,
|
|
workspace_id=self.context["workspace_id"],
|
|
)
|
|
return project
|
|
|
|
|
|
class ProjectLiteSerializer(BaseSerializer):
|
|
"""
|
|
Lightweight project serializer for minimal data transfer.
|
|
|
|
Provides essential project information including identifiers, visual properties,
|
|
and basic metadata optimized for list views and references.
|
|
"""
|
|
|
|
cover_image_url = serializers.CharField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Project
|
|
fields = [
|
|
"id",
|
|
"identifier",
|
|
"name",
|
|
"cover_image",
|
|
"icon_prop",
|
|
"emoji",
|
|
"description",
|
|
"cover_image_url",
|
|
]
|
|
read_only_fields = fields
|