""" DRF Serializers for the API. This module defines serializers for converting model instances to JSON and validating input data. """ from rest_framework import serializers from websites.models import Website, Scan, Issue, Metric, ScanStatus class IssueSerializer(serializers.ModelSerializer): """Serializer for Issue model.""" severity_display = serializers.CharField(source='get_severity_display', read_only=True) category_display = serializers.CharField(source='get_category_display', read_only=True) tool_display = serializers.CharField(source='get_tool_display', read_only=True) class Meta: model = Issue fields = [ 'id', 'category', 'category_display', 'severity', 'severity_display', 'tool', 'tool_display', 'title', 'description', 'affected_url', 'remediation', 'created_at', ] read_only_fields = fields class MetricSerializer(serializers.ModelSerializer): """Serializer for Metric model.""" formatted_value = serializers.CharField(source='get_formatted_value', read_only=True) unit_display = serializers.CharField(source='get_unit_display', read_only=True) class Meta: model = Metric fields = [ 'id', 'name', 'display_name', 'value', 'unit', 'unit_display', 'formatted_value', 'source', 'score', ] read_only_fields = fields class ScanListSerializer(serializers.ModelSerializer): """Serializer for Scan list views (minimal data).""" status_display = serializers.CharField(source='get_status_display', read_only=True) website_url = serializers.CharField(source='website.url', read_only=True) issues_count = serializers.SerializerMethodField() class Meta: model = Scan fields = [ 'id', 'website_url', 'status', 'status_display', 'created_at', 'completed_at', 'overall_score', 'performance_score', 'security_score', 'issues_count', ] read_only_fields = fields def get_issues_count(self, obj): return obj.issues.count() class ScanDetailSerializer(serializers.ModelSerializer): """Serializer for Scan detail views (full data).""" status_display = serializers.CharField(source='get_status_display', read_only=True) website_url = serializers.CharField(source='website.url', read_only=True) website_domain = serializers.CharField(source='website.domain', read_only=True) issues = IssueSerializer(many=True, read_only=True) metrics = MetricSerializer(many=True, read_only=True) issues_by_category = serializers.SerializerMethodField() issues_by_severity = serializers.SerializerMethodField() class Meta: model = Scan fields = [ 'id', 'website_url', 'website_domain', 'status', 'status_display', 'created_at', 'started_at', 'completed_at', 'overall_score', 'performance_score', 'accessibility_score', 'seo_score', 'best_practices_score', 'security_score', 'error_message', 'issues', 'metrics', 'issues_by_category', 'issues_by_severity', ] read_only_fields = fields def get_issues_by_category(self, obj): """Group issues by category.""" from collections import defaultdict grouped = defaultdict(list) for issue in obj.issues.all(): grouped[issue.category].append(IssueSerializer(issue).data) return dict(grouped) def get_issues_by_severity(self, obj): """Count issues by severity.""" from django.db.models import Count counts = obj.issues.values('severity').annotate(count=Count('id')) return {item['severity']: item['count'] for item in counts} class ScanCreateSerializer(serializers.Serializer): """Serializer for creating new scans.""" url = serializers.URLField( required=True, help_text="The URL to scan (must be http or https)" ) def validate_url(self, value): """Validate and normalize the URL.""" from scanner.utils import validate_url is_valid, result = validate_url(value) if not is_valid: raise serializers.ValidationError(result) return result # Return normalized URL def create(self, validated_data): """Create Website and Scan records.""" from scanner.tasks import check_rate_limit, check_concurrent_scan_limit, run_scan_task url = validated_data['url'] # Check rate limit rate_limit_error = check_rate_limit(url) if rate_limit_error: raise serializers.ValidationError({'url': rate_limit_error}) # Check concurrent scan limit concurrent_error = check_concurrent_scan_limit() if concurrent_error: raise serializers.ValidationError({'non_field_errors': concurrent_error}) # Get or create Website website, created = Website.objects.get_or_create( url=url, defaults={'domain': validated_data.get('domain', '')} ) # Create Scan scan = Scan.objects.create( website=website, status=ScanStatus.PENDING ) # Trigger Celery task task = run_scan_task.delay(str(scan.id)) # Update scan with task ID scan.celery_task_id = task.id scan.save(update_fields=['celery_task_id']) return scan class WebsiteSerializer(serializers.ModelSerializer): """Serializer for Website model.""" scans_count = serializers.SerializerMethodField() latest_scan = serializers.SerializerMethodField() class Meta: model = Website fields = [ 'id', 'url', 'domain', 'created_at', 'last_scanned_at', 'scans_count', 'latest_scan', ] read_only_fields = fields def get_scans_count(self, obj): return obj.scans.count() def get_latest_scan(self, obj): latest = obj.scans.first() if latest: return ScanListSerializer(latest).data return None class WebsiteDetailSerializer(WebsiteSerializer): """Detailed Website serializer with scan list.""" scans = ScanListSerializer(many=True, read_only=True) class Meta(WebsiteSerializer.Meta): fields = WebsiteSerializer.Meta.fields + ['scans'] class HealthCheckSerializer(serializers.Serializer): """Serializer for health check response.""" status = serializers.CharField() database = serializers.CharField() redis = serializers.CharField() celery = serializers.CharField() timestamp = serializers.DateTimeField()