from rest_framework import serializers from apps.accounts.serializers import UserSerializer from apps.core.choices.serializers import RichChoiceSerializerField from .models import EmailThread, Ticket class SubmitterProfileSerializer(serializers.Serializer): """Nested serializer for submitter profile data.""" display_name = serializers.CharField(source="profile.display_name", allow_null=True) created_at = serializers.DateTimeField(source="date_joined", allow_null=True) coaster_count = serializers.IntegerField(source="profile.coaster_credit_count", allow_null=True, default=0) ride_count = serializers.IntegerField(source="profile.ride_credit_count", allow_null=True, default=0) park_count = serializers.IntegerField(source="profile.park_credit_count", allow_null=True, default=0) review_count = serializers.IntegerField(source="profile.review_count", allow_null=True, default=0) avatar_url = serializers.CharField(source="profile.avatar_url", allow_null=True) class TicketSerializer(serializers.ModelSerializer): """Serializer for Ticket model with full frontend compatibility.""" # User fields user = UserSerializer(read_only=True) user_id = serializers.UUIDField(source="user.id", read_only=True, allow_null=True) submitter_username = serializers.CharField(source="user.username", read_only=True, allow_null=True) submitter_reputation = serializers.SerializerMethodField() submitter_profile = serializers.SerializerMethodField() # Choice display fields category_display = serializers.CharField(source="get_category_display", read_only=True) status_display = serializers.CharField(source="get_status_display", read_only=True) # Computed fields thread_id = serializers.CharField(read_only=True) response_count = serializers.IntegerField(read_only=True) last_admin_response_at = serializers.DateTimeField(read_only=True, allow_null=True) # Resolution tracking (alias resolved_by username) resolved_by_username = serializers.CharField(source="resolved_by.username", read_only=True, allow_null=True) # Admin fields assigned_to_username = serializers.CharField(source="assigned_to.username", read_only=True, allow_null=True) archived_by_username = serializers.CharField(source="archived_by.username", read_only=True, allow_null=True) class Meta: model = Ticket fields = [ # Core fields "id", "created_at", "updated_at", # User/submitter fields "user", "user_id", "submitter_username", "submitter_reputation", "submitter_profile", "name", "email", # Ticket content "subject", "message", "category", "category_display", # Status "status", "status_display", # Ticket number "ticket_number", # Admin management "admin_notes", "assigned_to", "assigned_to_username", # Resolution "resolved_at", "resolved_by", "resolved_by_username", # Thread info "thread_id", "last_admin_response_at", "response_count", # Archive "archived_at", "archived_by", "archived_by_username", ] read_only_fields = [ "id", "created_at", "updated_at", "user", "user_id", "submitter_username", "submitter_reputation", "submitter_profile", "ticket_number", "thread_id", "response_count", "last_admin_response_at", ] def get_submitter_reputation(self, obj): """Get the submitter's reputation score.""" if obj.user and hasattr(obj.user, "profile"): return getattr(obj.user.profile, "reputation", 0) return None def get_submitter_profile(self, obj): """Get a subset of profile data for display.""" if not obj.user or not hasattr(obj.user, "profile"): return None profile = obj.user.profile return { "display_name": getattr(profile, "display_name", None), "created_at": obj.user.date_joined.isoformat() if obj.user.date_joined else None, "coaster_count": getattr(profile, "coaster_credit_count", 0), "ride_count": getattr(profile, "ride_credit_count", 0), "park_count": getattr(profile, "park_credit_count", 0), "review_count": getattr(profile, "review_count", 0), "avatar_url": getattr(profile, "avatar_url", None), } def validate(self, data): # Ensure email is provided if user is anonymous request = self.context.get("request") if request and not request.user.is_authenticated and not data.get("email"): raise serializers.ValidationError({"email": "Email is required for guests."}) return data class EmailThreadSerializer(serializers.ModelSerializer): """Serializer for EmailThread model.""" # NOTE: Frontend uses submission_id, we provide ticket as that field submission_id = serializers.UUIDField(source="ticket.id", read_only=True) sent_by_username = serializers.CharField(source="sent_by.username", read_only=True, allow_null=True) class Meta: model = EmailThread fields = [ "id", "created_at", "submission_id", "message_id", "from_email", "to_email", "subject", "body_text", "direction", "sent_by", "sent_by_username", ] read_only_fields = ["id", "created_at", "submission_id"] class ReportSerializer(serializers.ModelSerializer): """Serializer for Report model.""" reporter_username = serializers.CharField(source="reporter.username", read_only=True, allow_null=True) resolved_by_username = serializers.CharField(source="resolved_by.username", read_only=True, allow_null=True) report_type_display = serializers.CharField(source="get_report_type_display", read_only=True) status_display = serializers.CharField(source="get_status_display", read_only=True) content_type_name = serializers.CharField(source="content_type.model", read_only=True) is_resolved = serializers.BooleanField(read_only=True) class Meta: from .models import Report model = Report fields = [ "id", "reporter", "reporter_username", "content_type", "content_type_name", "object_id", "report_type", "report_type_display", "reason", "status", "status_display", "resolved_at", "resolved_by", "resolved_by_username", "resolution_notes", "is_resolved", "created_at", "updated_at", ] read_only_fields = [ "id", "reporter", "resolved_at", "resolved_by", "created_at", "updated_at", ] class ReportCreateSerializer(serializers.ModelSerializer): """Serializer for creating reports with entity type as string.""" entity_type = serializers.CharField(write_only=True, help_text="Type of entity: park, ride, review, etc.") entity_id = serializers.CharField(write_only=True, help_text="ID of the entity being reported") class Meta: from .models import Report model = Report fields = [ "entity_type", "entity_id", "report_type", "reason", ] def validate(self, data): from django.contrib.contenttypes.models import ContentType entity_type = data.pop("entity_type") entity_id = data.pop("entity_id") # Map common entity types to app.model type_mapping = { "park": ("parks", "park"), "ride": ("rides", "ride"), "review": ("reviews", "review"), "user": ("accounts", "user"), } if entity_type in type_mapping: app_label, model_name = type_mapping[entity_type] else: # Try to parse as app.model parts = entity_type.split(".") if len(parts) != 2: raise serializers.ValidationError( {"entity_type": f"Unknown entity type: {entity_type}. Use 'park', 'ride', 'review', or 'app.model'."} ) app_label, model_name = parts try: content_type = ContentType.objects.get(app_label=app_label, model=model_name) except ContentType.DoesNotExist: raise serializers.ValidationError({"entity_type": f"Unknown entity type: {entity_type}"}) data["content_type"] = content_type data["object_id"] = entity_id return data class ReportResolveSerializer(serializers.Serializer): """Serializer for resolving reports.""" # Use RichChoiceSerializerField with only resolution statuses status = RichChoiceSerializerField( choice_group="report_statuses", domain="support", default="resolved", ) notes = serializers.CharField(required=False, allow_blank=True) # ============================================================================= # Support Ticket Action Serializers # ============================================================================= class SendReplySerializer(serializers.Serializer): """ Input serializer for send_reply action. Validates the request body for sending an email reply to a ticket. Supports both snake_case and camelCase field names for frontend compatibility. """ # Primary fields (required=False because we validate manually to support camelCase) reply_body = serializers.CharField( required=False, min_length=1, max_length=50000, help_text="The body text of the email reply", ) new_status = RichChoiceSerializerField( choice_group="ticket_statuses", domain="support", required=False, allow_null=True, help_text="Optionally update the ticket status after sending reply", ) # camelCase aliases for frontend compatibility replyBody = serializers.CharField(required=False, write_only=True) newStatus = serializers.CharField(required=False, write_only=True, allow_null=True) def validate(self, data: dict) -> dict: """Normalize camelCase to snake_case and validate required fields.""" # Normalize camelCase to snake_case if "replyBody" in data and data["replyBody"]: data["reply_body"] = data.pop("replyBody") elif "replyBody" in data: data.pop("replyBody") if "newStatus" in data and data["newStatus"]: data["new_status"] = data.pop("newStatus") elif "newStatus" in data: data.pop("newStatus") # Validate required fields if not data.get("reply_body"): raise serializers.ValidationError({"reply_body": "This field is required."}) return data class SendReplyResponseSerializer(serializers.Serializer): """Response serializer for send_reply action.""" detail = serializers.CharField() thread_id = serializers.UUIDField() ticket_number = serializers.CharField() class MergeTicketsSerializer(serializers.Serializer): """ Input serializer for merge tickets action. Validates the request body for merging multiple tickets into one. Supports both snake_case and camelCase field names for frontend compatibility. """ # Primary fields (required=False because we validate manually to support camelCase) primary_ticket_id = serializers.UUIDField( required=False, help_text="UUID of the primary ticket that will absorb others", ) merge_ticket_ids = serializers.ListField( child=serializers.UUIDField(), required=False, min_length=1, help_text="List of ticket UUIDs to merge into the primary", ) merge_reason = serializers.CharField( required=False, allow_blank=True, max_length=500, help_text="Optional reason for the merge", ) # camelCase aliases for frontend compatibility primaryTicketId = serializers.UUIDField(required=False, write_only=True) mergeTicketIds = serializers.ListField( child=serializers.UUIDField(), required=False, write_only=True, ) mergeReason = serializers.CharField(required=False, write_only=True, allow_blank=True) def validate(self, data: dict) -> dict: """Normalize camelCase to snake_case and validate required fields.""" # Normalize camelCase to snake_case if "primaryTicketId" in data and data["primaryTicketId"]: data["primary_ticket_id"] = data.pop("primaryTicketId") elif "primaryTicketId" in data: data.pop("primaryTicketId") if "mergeTicketIds" in data and data["mergeTicketIds"]: data["merge_ticket_ids"] = data.pop("mergeTicketIds") elif "mergeTicketIds" in data: data.pop("mergeTicketIds") if "mergeReason" in data and data["mergeReason"]: data["merge_reason"] = data.pop("mergeReason") elif "mergeReason" in data: data.pop("mergeReason") # Validate required fields if not data.get("primary_ticket_id"): raise serializers.ValidationError({"primary_ticket_id": "This field is required."}) if not data.get("merge_ticket_ids"): raise serializers.ValidationError({"merge_ticket_ids": "This field is required."}) return data class MergeTicketsResponseSerializer(serializers.Serializer): """Response serializer for merge tickets action.""" detail = serializers.CharField() primaryTicketNumber = serializers.CharField() mergedCount = serializers.IntegerField() threadsConsolidated = serializers.IntegerField()