""" Moderation API Serializers This module contains DRF serializers for the moderation system, including: - ModerationReport serializers for content reporting - ModerationQueue serializers for moderation workflow - ModerationAction serializers for tracking moderation actions - BulkOperation serializers for administrative bulk operations All serializers include comprehensive validation and nested relationships. """ from rest_framework import serializers from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.utils import timezone from datetime import timedelta from .models import ( ModerationReport, ModerationQueue, ModerationAction, BulkOperation, ) User = get_user_model() # ============================================================================ # Base Serializers # ============================================================================ class UserBasicSerializer(serializers.ModelSerializer): """Basic user information for moderation contexts.""" display_name = serializers.SerializerMethodField() class Meta: model = User fields = ["id", "username", "display_name", "email", "role"] read_only_fields = ["id", "username", "display_name", "email", "role"] def get_display_name(self, obj): """Get the user's display name.""" return obj.get_display_name() class ContentTypeSerializer(serializers.ModelSerializer): """Content type information for generic foreign keys.""" class Meta: model = ContentType fields = ["id", "app_label", "model"] read_only_fields = ["id", "app_label", "model"] # ============================================================================ # Moderation Report Serializers # ============================================================================ class ModerationReportSerializer(serializers.ModelSerializer): """Full moderation report serializer with all details.""" reported_by = UserBasicSerializer(read_only=True) assigned_moderator = UserBasicSerializer(read_only=True) content_type = ContentTypeSerializer(read_only=True) # Computed fields is_overdue = serializers.SerializerMethodField() time_since_created = serializers.SerializerMethodField() priority_display = serializers.CharField( source="get_priority_display", read_only=True ) status_display = serializers.CharField(source="get_status_display", read_only=True) report_type_display = serializers.CharField( source="get_report_type_display", read_only=True ) class Meta: model = ModerationReport fields = [ "id", "report_type", "report_type_display", "status", "status_display", "priority", "priority_display", "reported_entity_type", "reported_entity_id", "reason", "description", "evidence_urls", "resolved_at", "resolution_notes", "resolution_action", "created_at", "updated_at", "reported_by", "assigned_moderator", "content_type", "is_overdue", "time_since_created", ] read_only_fields = [ "id", "created_at", "updated_at", "reported_by", "content_type", "is_overdue", "time_since_created", "report_type_display", "status_display", "priority_display", ] def get_is_overdue(self, obj) -> bool: """Check if report is overdue based on priority.""" if obj.status in ["RESOLVED", "DISMISSED"]: return False now = timezone.now() hours_since_created = (now - obj.created_at).total_seconds() / 3600 # Define SLA hours by priority sla_hours = {"URGENT": 2, "HIGH": 8, "MEDIUM": 24, "LOW": 72} return hours_since_created > sla_hours.get(obj.priority, 24) def get_time_since_created(self, obj) -> str: """Human-readable time since creation.""" now = timezone.now() diff = now - obj.created_at if diff.days > 0: return f"{diff.days} days ago" elif diff.seconds > 3600: hours = diff.seconds // 3600 return f"{hours} hours ago" else: minutes = diff.seconds // 60 return f"{minutes} minutes ago" class CreateModerationReportSerializer(serializers.ModelSerializer): """Serializer for creating new moderation reports.""" class Meta: model = ModerationReport fields = [ "report_type", "reported_entity_type", "reported_entity_id", "reason", "description", "evidence_urls", ] def validate(self, attrs): """Validate the report data.""" # Validate entity type valid_entity_types = ["park", "ride", "review", "photo", "user", "comment"] if attrs["reported_entity_type"] not in valid_entity_types: raise serializers.ValidationError( { "reported_entity_type": f'Must be one of: {", ".join(valid_entity_types)}' } ) # Validate evidence URLs evidence_urls = attrs.get("evidence_urls", []) if not isinstance(evidence_urls, list): raise serializers.ValidationError( {"evidence_urls": "Must be a list of URLs"} ) return attrs def create(self, validated_data): """Create a new moderation report.""" validated_data["reported_by"] = self.context["request"].user validated_data["status"] = "PENDING" validated_data["priority"] = "MEDIUM" # Default priority # Set content type based on entity type entity_type = validated_data["reported_entity_type"] app_label_map = { "park": "parks", "ride": "rides", "review": "rides", # Assuming ride reviews "photo": "media", "user": "accounts", "comment": "core", } if entity_type in app_label_map: try: content_type = ContentType.objects.get( app_label=app_label_map[entity_type], model=entity_type ) validated_data["content_type"] = content_type except ContentType.DoesNotExist: pass return super().create(validated_data) class UpdateModerationReportSerializer(serializers.ModelSerializer): """Serializer for updating moderation reports.""" class Meta: model = ModerationReport fields = [ "status", "priority", "assigned_moderator", "resolution_notes", "resolution_action", ] def validate_status(self, value): """Validate status transitions.""" if self.instance and self.instance.status == "RESOLVED": if value != "RESOLVED": raise serializers.ValidationError( "Cannot change status of resolved report" ) return value def update(self, instance, validated_data): """Update moderation report with automatic timestamps.""" if "status" in validated_data and validated_data["status"] == "RESOLVED": validated_data["resolved_at"] = timezone.now() return super().update(instance, validated_data) # ============================================================================ # Moderation Queue Serializers # ============================================================================ class ModerationQueueSerializer(serializers.ModelSerializer): """Full moderation queue item serializer.""" assigned_to = UserBasicSerializer(read_only=True) related_report = ModerationReportSerializer(read_only=True) content_type = ContentTypeSerializer(read_only=True) # Computed fields is_overdue = serializers.SerializerMethodField() time_in_queue = serializers.SerializerMethodField() estimated_completion = serializers.SerializerMethodField() class Meta: model = ModerationQueue fields = [ "id", "item_type", "status", "priority", "title", "description", "entity_type", "entity_id", "entity_preview", "flagged_by", "assigned_at", "estimated_review_time", "created_at", "updated_at", "tags", "assigned_to", "related_report", "content_type", "is_overdue", "time_in_queue", "estimated_completion", ] read_only_fields = [ "id", "created_at", "updated_at", "content_type", "is_overdue", "time_in_queue", "estimated_completion", ] def get_is_overdue(self, obj) -> bool: """Check if queue item is overdue.""" if obj.status == "COMPLETED": return False if obj.assigned_at: time_assigned = (timezone.now() - obj.assigned_at).total_seconds() / 60 return time_assigned > obj.estimated_review_time # If not assigned, check time in queue time_in_queue = (timezone.now() - obj.created_at).total_seconds() / 60 return time_in_queue > (obj.estimated_review_time * 2) def get_time_in_queue(self, obj) -> int: """Minutes since item was created.""" return int((timezone.now() - obj.created_at).total_seconds() / 60) def get_estimated_completion(self, obj) -> str: """Estimated completion time.""" if obj.assigned_at: completion_time = obj.assigned_at + timedelta( minutes=obj.estimated_review_time ) else: completion_time = timezone.now() + timedelta( minutes=obj.estimated_review_time ) return completion_time.isoformat() class AssignQueueItemSerializer(serializers.Serializer): """Serializer for assigning queue items to moderators.""" moderator_id = serializers.IntegerField() def validate_moderator_id(self, value): """Validate that the moderator exists and has appropriate permissions.""" try: user = User.objects.get(id=value) user_role = getattr(user, "role", "USER") if user_role not in ["MODERATOR", "ADMIN", "SUPERUSER"]: raise serializers.ValidationError( "User must be a moderator, admin, or superuser" ) return value except User.DoesNotExist: raise serializers.ValidationError("Moderator not found") class CompleteQueueItemSerializer(serializers.Serializer): """Serializer for completing queue items.""" action = serializers.ChoiceField( choices=[ "NO_ACTION", "CONTENT_REMOVED", "CONTENT_EDITED", "USER_WARNING", "USER_SUSPENDED", "USER_BANNED", ] ) notes = serializers.CharField(required=False, allow_blank=True) def validate(self, attrs): """Validate completion data.""" action = attrs["action"] notes = attrs.get("notes", "") # Require notes for certain actions if action in ["USER_WARNING", "USER_SUSPENDED", "USER_BANNED"] and not notes: raise serializers.ValidationError( {"notes": f"Notes are required for action: {action}"} ) return attrs # ============================================================================ # Moderation Action Serializers # ============================================================================ class ModerationActionSerializer(serializers.ModelSerializer): """Full moderation action serializer.""" moderator = UserBasicSerializer(read_only=True) target_user = UserBasicSerializer(read_only=True) related_report = ModerationReportSerializer(read_only=True) # Computed fields is_expired = serializers.SerializerMethodField() time_remaining = serializers.SerializerMethodField() action_type_display = serializers.CharField( source="get_action_type_display", read_only=True ) class Meta: model = ModerationAction fields = [ "id", "action_type", "action_type_display", "reason", "details", "duration_hours", "created_at", "expires_at", "is_active", "moderator", "target_user", "related_report", "updated_at", "is_expired", "time_remaining", ] read_only_fields = [ "id", "created_at", "updated_at", "moderator", "target_user", "related_report", "is_expired", "time_remaining", "action_type_display", ] def get_is_expired(self, obj) -> bool: """Check if action has expired.""" if not obj.expires_at: return False return timezone.now() > obj.expires_at def get_time_remaining(self, obj) -> str | None: """Time remaining until expiration.""" if not obj.expires_at or not obj.is_active: return None now = timezone.now() if now >= obj.expires_at: return "Expired" diff = obj.expires_at - now if diff.days > 0: return f"{diff.days} days" elif diff.seconds > 3600: hours = diff.seconds // 3600 return f"{hours} hours" else: minutes = diff.seconds // 60 return f"{minutes} minutes" class CreateModerationActionSerializer(serializers.ModelSerializer): """Serializer for creating moderation actions.""" target_user_id = serializers.IntegerField() related_report_id = serializers.IntegerField(required=False) class Meta: model = ModerationAction fields = [ "action_type", "reason", "details", "duration_hours", "target_user_id", "related_report_id", ] def validate_target_user_id(self, value): """Validate target user exists.""" try: User.objects.get(id=value) return value except User.DoesNotExist: raise serializers.ValidationError("Target user not found") def validate_related_report_id(self, value): """Validate related report exists.""" if value: try: ModerationReport.objects.get(id=value) return value except ModerationReport.DoesNotExist: raise serializers.ValidationError("Related report not found") return value def validate(self, attrs): """Validate action data.""" action_type = attrs["action_type"] duration_hours = attrs.get("duration_hours") # Validate duration for temporary actions temporary_actions = ["USER_SUSPENSION", "CONTENT_RESTRICTION"] if action_type in temporary_actions and not duration_hours: raise serializers.ValidationError( {"duration_hours": f"Duration is required for {action_type}"} ) # Validate duration range if duration_hours and ( duration_hours < 1 or duration_hours > 8760 ): # 1 hour to 1 year raise serializers.ValidationError( {"duration_hours": "Duration must be between 1 and 8760 hours (1 year)"} ) return attrs def create(self, validated_data): """Create moderation action with automatic fields.""" target_user_id = validated_data.pop("target_user_id") related_report_id = validated_data.pop("related_report_id", None) validated_data["moderator"] = self.context["request"].user validated_data["target_user_id"] = target_user_id validated_data["is_active"] = True if related_report_id: validated_data["related_report_id"] = related_report_id # Set expiration time for temporary actions if validated_data.get("duration_hours"): validated_data["expires_at"] = timezone.now() + timedelta( hours=validated_data["duration_hours"] ) return super().create(validated_data) # ============================================================================ # Bulk Operation Serializers # ============================================================================ class BulkOperationSerializer(serializers.ModelSerializer): """Full bulk operation serializer.""" created_by = UserBasicSerializer(read_only=True) # Computed fields progress_percentage = serializers.SerializerMethodField() estimated_completion = serializers.SerializerMethodField() operation_type_display = serializers.CharField( source="get_operation_type_display", read_only=True ) status_display = serializers.CharField(source="get_status_display", read_only=True) class Meta: model = BulkOperation fields = [ "id", "operation_type", "operation_type_display", "status", "status_display", "priority", "parameters", "results", "total_items", "processed_items", "failed_items", "created_at", "started_at", "completed_at", "estimated_duration_minutes", "can_cancel", "description", "schedule_for", "created_by", "updated_at", "progress_percentage", "estimated_completion", ] read_only_fields = [ "id", "created_at", "updated_at", "created_by", "progress_percentage", "estimated_completion", "operation_type_display", "status_display", ] def get_progress_percentage(self, obj) -> float: """Calculate progress percentage.""" if obj.total_items == 0: return 0.0 return round((obj.processed_items / obj.total_items) * 100, 2) def get_estimated_completion(self, obj) -> str | None: """Estimate completion time.""" if obj.status == "COMPLETED": return obj.completed_at.isoformat() if obj.completed_at else None if obj.status == "RUNNING" and obj.started_at: # Calculate based on current progress if obj.processed_items > 0: elapsed_minutes = (timezone.now() - obj.started_at).total_seconds() / 60 rate = obj.processed_items / elapsed_minutes remaining_items = obj.total_items - obj.processed_items remaining_minutes = ( remaining_items / rate if rate > 0 else obj.estimated_duration_minutes ) completion_time = timezone.now() + timedelta(minutes=remaining_minutes) return completion_time.isoformat() # Use scheduled time or estimated duration if obj.schedule_for: return obj.schedule_for.isoformat() elif obj.estimated_duration_minutes: completion_time = timezone.now() + timedelta( minutes=obj.estimated_duration_minutes ) return completion_time.isoformat() return None class CreateBulkOperationSerializer(serializers.ModelSerializer): """Serializer for creating bulk operations.""" class Meta: model = BulkOperation fields = [ "operation_type", "priority", "parameters", "description", "schedule_for", "estimated_duration_minutes", ] def validate_parameters(self, value): """Validate operation parameters.""" if not isinstance(value, dict): raise serializers.ValidationError("Parameters must be a JSON object") operation_type = getattr(self, "initial_data", {}).get("operation_type") # Validate required parameters by operation type required_params = { "UPDATE_PARKS": ["park_ids", "updates"], "UPDATE_RIDES": ["ride_ids", "updates"], "IMPORT_DATA": ["data_type", "source"], "EXPORT_DATA": ["data_type", "format"], "MODERATE_CONTENT": ["content_type", "action"], "USER_ACTIONS": ["user_ids", "action"], } if operation_type in required_params: for param in required_params[operation_type]: if param not in value: raise serializers.ValidationError( f'Parameter "{param}" is required for {operation_type}' ) return value def create(self, validated_data): """Create bulk operation with automatic fields.""" validated_data["created_by"] = self.context["request"].user validated_data["status"] = "PENDING" validated_data["total_items"] = 0 validated_data["processed_items"] = 0 validated_data["failed_items"] = 0 validated_data["can_cancel"] = True # Generate unique ID import uuid validated_data["id"] = str(uuid.uuid4())[:50] return super().create(validated_data) # ============================================================================ # Statistics and Summary Serializers # ============================================================================ class ModerationStatsSerializer(serializers.Serializer): """Serializer for moderation statistics.""" # Report stats total_reports = serializers.IntegerField() pending_reports = serializers.IntegerField() resolved_reports = serializers.IntegerField() overdue_reports = serializers.IntegerField() # Queue stats queue_size = serializers.IntegerField() assigned_items = serializers.IntegerField() unassigned_items = serializers.IntegerField() # Action stats total_actions = serializers.IntegerField() active_actions = serializers.IntegerField() expired_actions = serializers.IntegerField() # Bulk operation stats running_operations = serializers.IntegerField() completed_operations = serializers.IntegerField() failed_operations = serializers.IntegerField() # Performance metrics average_resolution_time_hours = serializers.FloatField() reports_by_priority = serializers.DictField() reports_by_type = serializers.DictField() class UserModerationProfileSerializer(serializers.Serializer): """Serializer for user moderation profile.""" user = UserBasicSerializer() # Report history reports_made = serializers.IntegerField() reports_against = serializers.IntegerField() # Action history warnings_received = serializers.IntegerField() suspensions_received = serializers.IntegerField() active_restrictions = serializers.IntegerField() # Risk assessment risk_level = serializers.ChoiceField(choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"]) risk_factors = serializers.ListField(child=serializers.CharField()) # Recent activity recent_reports = ModerationReportSerializer(many=True) recent_actions = ModerationActionSerializer(many=True) # Account status account_status = serializers.CharField() last_violation_date = serializers.DateTimeField(allow_null=True) next_review_date = serializers.DateTimeField(allow_null=True)