bottleneckcrm/backend/tasks/serializer.py

423 lines
12 KiB
Python

from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from common.serializer import (
AttachmentsSerializer,
CommentSerializer,
ProfileSerializer,
TagsSerializer,
TeamsSerializer,
UserSerializer,
)
from contacts.serializer import ContactSerializer
from tasks.models import (
Board,
BoardColumn,
BoardMember,
BoardTask,
Task,
TaskPipeline,
TaskStage,
)
class BoardMemberSerializer(serializers.ModelSerializer):
"""Serializer for board members"""
profile = ProfileSerializer(read_only=True)
profile_id = serializers.UUIDField(write_only=True, required=False)
class Meta:
model = BoardMember
fields = "__all__"
read_only_fields = ("id", "created_at", "updated_at", "board")
class BoardTaskSerializer(serializers.ModelSerializer):
"""Serializer for board tasks"""
assigned_to = ProfileSerializer(many=True, read_only=True)
assigned_to_ids = serializers.ListField(
child=serializers.UUIDField(), write_only=True, required=False
)
is_completed = serializers.BooleanField(read_only=True)
is_overdue = serializers.BooleanField(read_only=True)
class Meta:
model = BoardTask
fields = "__all__"
read_only_fields = ("id", "created_at", "updated_at", "completed_at")
class BoardColumnSerializer(serializers.ModelSerializer):
"""Serializer for board columns"""
tasks = BoardTaskSerializer(many=True, read_only=True)
task_count = serializers.SerializerMethodField()
class Meta:
model = BoardColumn
fields = "__all__"
read_only_fields = ("id", "created_at", "updated_at", "board")
@extend_schema_field(int)
def get_task_count(self, obj):
return obj.tasks.count()
class BoardColumnListSerializer(serializers.ModelSerializer):
"""Simplified column serializer for lists"""
task_count = serializers.SerializerMethodField()
class Meta:
model = BoardColumn
fields = ["id", "name", "order", "color", "limit", "task_count"]
@extend_schema_field(int)
def get_task_count(self, obj):
return obj.tasks.count()
class BoardSerializer(serializers.ModelSerializer):
"""Serializer for boards"""
owner = ProfileSerializer(read_only=True)
columns = BoardColumnListSerializer(many=True, read_only=True)
members = BoardMemberSerializer(source="memberships", many=True, read_only=True)
member_count = serializers.SerializerMethodField()
task_count = serializers.SerializerMethodField()
class Meta:
model = Board
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at",
"created_by",
"updated_by",
"owner",
"org",
)
@extend_schema_field(int)
def get_member_count(self, obj):
return obj.members.count()
@extend_schema_field(int)
def get_task_count(self, obj):
return BoardTask.objects.filter(column__board=obj).count()
class BoardListSerializer(serializers.ModelSerializer):
"""Simplified board serializer for lists"""
owner = ProfileSerializer(read_only=True)
member_count = serializers.SerializerMethodField()
column_count = serializers.SerializerMethodField()
task_count = serializers.SerializerMethodField()
class Meta:
model = Board
fields = [
"id",
"name",
"description",
"owner",
"is_archived",
"member_count",
"column_count",
"task_count",
"created_at",
"updated_at",
]
@extend_schema_field(int)
def get_member_count(self, obj):
return obj.members.count()
@extend_schema_field(int)
def get_column_count(self, obj):
return obj.columns.count()
@extend_schema_field(int)
def get_task_count(self, obj):
return BoardTask.objects.filter(column__board=obj).count()
class TaskSerializer(serializers.ModelSerializer):
created_by = UserSerializer()
assigned_to = ProfileSerializer(read_only=True, many=True)
contacts = ContactSerializer(read_only=True, many=True)
teams = TeamsSerializer(read_only=True, many=True)
tags = TagsSerializer(read_only=True, many=True)
task_attachment = AttachmentsSerializer(read_only=True, many=True)
task_comments = CommentSerializer(read_only=True, many=True)
class Meta:
model = Task
fields = (
"id",
"title",
"status",
"priority",
"due_date",
"description",
"account",
"opportunity",
"case",
"lead",
"created_by",
"created_at",
"contacts",
"teams",
"assigned_to",
"tags",
"task_attachment",
"task_comments",
)
class TaskCreateSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
request_obj = kwargs.pop("request_obj", None)
super().__init__(*args, **kwargs)
self.org = request_obj.profile.org
self.fields["title"].required = True
def validate_title(self, title):
if self.instance:
if (
Task.objects.filter(title__iexact=title, org=self.org)
.exclude(id=self.instance.id)
.exists()
):
raise serializers.ValidationError("Task already exists with this title")
else:
if Task.objects.filter(title__iexact=title, org=self.org).exists():
raise serializers.ValidationError("Task already exists with this title")
return title
def validate(self, attrs):
"""Validate that task has at most one parent entity."""
attrs = super().validate(attrs)
parent_fields = ["account", "opportunity", "case", "lead"]
set_parents = [field for field in parent_fields if attrs.get(field)]
if len(set_parents) > 1:
raise serializers.ValidationError(
{
"account": (
"A task can only be linked to one parent entity "
f"(Account, Opportunity, Case, or Lead). "
f"Currently set: {', '.join(set_parents)}"
)
}
)
return attrs
class Meta:
model = Task
fields = (
"id",
"title",
"status",
"priority",
"due_date",
"description",
"account",
"opportunity",
"case",
"lead",
"created_by",
"created_at",
)
class TaskDetailEditSwaggerSerializer(serializers.Serializer):
comment = serializers.CharField()
task_attachment = serializers.FileField()
class TaskCommentEditSwaggerSerializer(serializers.Serializer):
comment = serializers.CharField()
class TaskCreateSwaggerSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = (
"title",
"status",
"priority",
"due_date",
"description",
"account",
"opportunity",
"case",
"lead",
"contacts",
"teams",
"assigned_to",
"tags",
)
# ============================================================================
# Kanban Serializers
# ============================================================================
class TaskStageSerializer(serializers.ModelSerializer):
"""Serializer for task stages."""
task_count = serializers.SerializerMethodField()
class Meta:
model = TaskStage
fields = [
"id",
"name",
"order",
"color",
"stage_type",
"maps_to_status",
"wip_limit",
"task_count",
"created_at",
"updated_at",
]
read_only_fields = ("id", "created_at", "updated_at", "org")
@extend_schema_field(int)
def get_task_count(self, obj):
return obj.tasks.count()
class TaskPipelineSerializer(serializers.ModelSerializer):
"""Serializer for task pipelines with nested stages."""
stages = TaskStageSerializer(many=True, read_only=True)
stage_count = serializers.SerializerMethodField()
task_count = serializers.SerializerMethodField()
class Meta:
model = TaskPipeline
fields = [
"id",
"name",
"description",
"is_default",
"is_active",
"stages",
"stage_count",
"task_count",
"created_at",
"updated_at",
]
read_only_fields = ("id", "created_at", "updated_at", "org")
@extend_schema_field(int)
def get_stage_count(self, obj):
return obj.stages.count()
@extend_schema_field(int)
def get_task_count(self, obj):
return Task.objects.filter(stage__pipeline=obj).count()
class TaskPipelineListSerializer(serializers.ModelSerializer):
"""Simplified pipeline serializer for lists."""
stage_count = serializers.SerializerMethodField()
task_count = serializers.SerializerMethodField()
class Meta:
model = TaskPipeline
fields = [
"id",
"name",
"description",
"is_default",
"is_active",
"stage_count",
"task_count",
"created_at",
]
@extend_schema_field(int)
def get_stage_count(self, obj):
return obj.stages.count()
@extend_schema_field(int)
def get_task_count(self, obj):
return Task.objects.filter(stage__pipeline=obj).count()
class RelatedEntitySerializer(serializers.Serializer):
"""Minimal serializer for related entities on kanban cards."""
id = serializers.UUIDField()
name = serializers.CharField()
class TaskKanbanCardSerializer(serializers.ModelSerializer):
"""Lightweight serializer for kanban cards (minimal fields for performance)."""
assigned_to = ProfileSerializer(read_only=True, many=True)
is_overdue = serializers.BooleanField(read_only=True)
related_entity = serializers.SerializerMethodField()
class Meta:
model = Task
fields = [
"id",
"title",
"status",
"priority",
"due_date",
"is_overdue",
"stage",
"kanban_order",
"assigned_to",
"related_entity",
"created_at",
]
@extend_schema_field(RelatedEntitySerializer(allow_null=True))
def get_related_entity(self, obj):
"""Return the related entity (account, lead, opportunity, or case) if any."""
if obj.account_id:
return {"id": obj.account_id, "name": obj.account.name, "type": "account"}
if obj.lead_id:
return {"id": obj.lead_id, "name": str(obj.lead), "type": "lead"}
if obj.opportunity_id:
return {
"id": obj.opportunity_id,
"name": obj.opportunity.name,
"type": "opportunity",
}
if obj.case_id:
return {"id": obj.case_id, "name": obj.case.name, "type": "case"}
return None
class TaskMoveSerializer(serializers.Serializer):
"""Serializer for moving tasks in kanban."""
stage_id = serializers.UUIDField(required=False, allow_null=True)
status = serializers.ChoiceField(choices=Task.STATUS_CHOICES, required=False)
kanban_order = serializers.DecimalField(
max_digits=15, decimal_places=6, required=False
)
above_task_id = serializers.UUIDField(required=False, allow_null=True)
below_task_id = serializers.UUIDField(required=False, allow_null=True)
def validate(self, attrs):
# Must provide either stage_id or status
if not attrs.get("stage_id") and not attrs.get("status"):
raise serializers.ValidationError(
"Either stage_id or status must be provided"
)
return attrs