266 lines
8.4 KiB
Python
266 lines
8.4 KiB
Python
from rest_framework import serializers
|
|
from django.utils.html import strip_tags, escape
|
|
from django.core.exceptions import ValidationError
|
|
import re
|
|
from .models import ContactSubmission
|
|
|
|
|
|
class ContactSubmissionSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Serializer for ContactSubmission model.
|
|
Handles both creation and retrieval of contact form submissions.
|
|
"""
|
|
|
|
# Computed fields
|
|
full_name = serializers.ReadOnlyField()
|
|
is_high_priority = serializers.ReadOnlyField()
|
|
is_enterprise_client = serializers.ReadOnlyField()
|
|
industry_display = serializers.SerializerMethodField()
|
|
project_type_display = serializers.SerializerMethodField()
|
|
budget_display = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ContactSubmission
|
|
fields = [
|
|
'id',
|
|
'first_name',
|
|
'last_name',
|
|
'full_name',
|
|
'email',
|
|
'phone',
|
|
'company',
|
|
'job_title',
|
|
'industry',
|
|
'industry_display',
|
|
'company_size',
|
|
'project_type',
|
|
'project_type_display',
|
|
'timeline',
|
|
'budget',
|
|
'budget_display',
|
|
'message',
|
|
'newsletter_subscription',
|
|
'privacy_consent',
|
|
'status',
|
|
'priority',
|
|
'is_high_priority',
|
|
'is_enterprise_client',
|
|
'created_at',
|
|
'updated_at',
|
|
'admin_notes',
|
|
'assigned_to',
|
|
]
|
|
read_only_fields = [
|
|
'id',
|
|
'status',
|
|
'priority',
|
|
'created_at',
|
|
'updated_at',
|
|
'admin_notes',
|
|
'assigned_to',
|
|
]
|
|
|
|
def get_industry_display(self, obj):
|
|
return obj.get_industry_display()
|
|
|
|
def get_project_type_display(self, obj):
|
|
return obj.get_project_type_display()
|
|
|
|
def get_budget_display(self, obj):
|
|
return obj.get_budget_display()
|
|
|
|
def validate_email(self, value):
|
|
"""
|
|
Custom email validation to ensure it's a business email.
|
|
"""
|
|
if value and not any(domain in value.lower() for domain in ['@gmail.com', '@yahoo.com', '@hotmail.com']):
|
|
return value
|
|
# Allow personal emails but log them
|
|
return value
|
|
|
|
def validate_privacy_consent(self, value):
|
|
"""
|
|
Ensure privacy consent is given.
|
|
"""
|
|
if not value:
|
|
raise serializers.ValidationError("Privacy consent is required to submit the form.")
|
|
return value
|
|
|
|
def validate(self, attrs):
|
|
"""
|
|
Cross-field validation.
|
|
"""
|
|
# Ensure required fields are present
|
|
required_fields = ['first_name', 'last_name', 'email', 'company', 'job_title', 'message']
|
|
for field in required_fields:
|
|
if not attrs.get(field):
|
|
raise serializers.ValidationError(f"{field.replace('_', ' ').title()} is required.")
|
|
|
|
# Validate enterprise client indicators
|
|
if attrs.get('company_size') in ['201-1000', '1000+'] and attrs.get('budget') in ['under-50k', '50k-100k']:
|
|
# This might be a mismatch, but we'll allow it and flag for review
|
|
pass
|
|
|
|
return attrs
|
|
|
|
|
|
class ContactSubmissionCreateSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Simplified serializer for creating contact submissions.
|
|
Only includes fields that should be provided by the frontend.
|
|
"""
|
|
|
|
class Meta:
|
|
model = ContactSubmission
|
|
fields = [
|
|
'first_name',
|
|
'last_name',
|
|
'email',
|
|
'phone',
|
|
'company',
|
|
'job_title',
|
|
'industry',
|
|
'company_size',
|
|
'project_type',
|
|
'timeline',
|
|
'budget',
|
|
'message',
|
|
'newsletter_subscription',
|
|
'privacy_consent',
|
|
]
|
|
|
|
def _sanitize_text_field(self, value):
|
|
"""
|
|
Sanitize text fields by detecting and rejecting HTML/script tags.
|
|
Returns cleaned text or raises ValidationError if dangerous content is detected.
|
|
"""
|
|
if not value:
|
|
return value
|
|
|
|
# Check for script tags and other dangerous HTML patterns
|
|
dangerous_patterns = [
|
|
(r'<script[^>]*>.*?</script>', 'Script tags are not allowed'),
|
|
(r'<iframe[^>]*>.*?</iframe>', 'Iframe tags are not allowed'),
|
|
(r'javascript:', 'JavaScript protocol is not allowed'),
|
|
(r'on\w+\s*=', 'Event handlers are not allowed'),
|
|
(r'<svg[^>]*onload', 'SVG onload handlers are not allowed'),
|
|
(r'<img[^>]*onerror', 'Image onerror handlers are not allowed'),
|
|
(r'<[^>]+>', 'HTML tags are not allowed'), # Catch any remaining HTML tags
|
|
]
|
|
|
|
value_lower = value.lower()
|
|
for pattern, message in dangerous_patterns:
|
|
if re.search(pattern, value_lower, re.IGNORECASE | re.DOTALL):
|
|
raise serializers.ValidationError(
|
|
f"Invalid input detected: {message}. Please remove HTML tags and scripts."
|
|
)
|
|
|
|
# Strip any remaining HTML tags (defense in depth)
|
|
cleaned = strip_tags(value)
|
|
# Remove any remaining script-like content
|
|
cleaned = re.sub(r'javascript:', '', cleaned, flags=re.IGNORECASE)
|
|
|
|
return cleaned.strip()
|
|
|
|
def validate_first_name(self, value):
|
|
"""Sanitize first name field."""
|
|
return self._sanitize_text_field(value)
|
|
|
|
def validate_last_name(self, value):
|
|
"""Sanitize last name field."""
|
|
return self._sanitize_text_field(value)
|
|
|
|
def validate_company(self, value):
|
|
"""Sanitize company field."""
|
|
return self._sanitize_text_field(value)
|
|
|
|
def validate_job_title(self, value):
|
|
"""Sanitize job title field."""
|
|
return self._sanitize_text_field(value)
|
|
|
|
def validate_message(self, value):
|
|
"""Sanitize message field."""
|
|
return self._sanitize_text_field(value)
|
|
|
|
def validate_phone(self, value):
|
|
"""Sanitize phone field - only allow alphanumeric, spaces, dashes, parentheses, and plus."""
|
|
if not value:
|
|
return value
|
|
|
|
# Remove HTML tags
|
|
cleaned = strip_tags(value)
|
|
# Only allow phone number characters
|
|
if not re.match(r'^[\d\s\-\+\(\)]+$', cleaned):
|
|
raise serializers.ValidationError("Phone number contains invalid characters.")
|
|
|
|
return cleaned.strip()
|
|
|
|
def validate_privacy_consent(self, value):
|
|
"""
|
|
Ensure privacy consent is given.
|
|
"""
|
|
if not value:
|
|
raise serializers.ValidationError("Privacy consent is required to submit the form.")
|
|
return value
|
|
|
|
def validate(self, attrs):
|
|
"""
|
|
Cross-field validation for creation.
|
|
"""
|
|
# Ensure required fields are present
|
|
required_fields = ['first_name', 'last_name', 'email', 'company', 'job_title', 'message']
|
|
for field in required_fields:
|
|
if not attrs.get(field):
|
|
raise serializers.ValidationError(f"{field.replace('_', ' ').title()} is required.")
|
|
|
|
return attrs
|
|
|
|
|
|
class ContactSubmissionListSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Simplified serializer for listing contact submissions.
|
|
Used in admin views and API listings.
|
|
"""
|
|
|
|
full_name = serializers.ReadOnlyField()
|
|
is_high_priority = serializers.ReadOnlyField()
|
|
is_enterprise_client = serializers.ReadOnlyField()
|
|
|
|
class Meta:
|
|
model = ContactSubmission
|
|
fields = [
|
|
'id',
|
|
'full_name',
|
|
'email',
|
|
'company',
|
|
'job_title',
|
|
'project_type',
|
|
'status',
|
|
'priority',
|
|
'is_high_priority',
|
|
'is_enterprise_client',
|
|
'created_at',
|
|
]
|
|
|
|
|
|
class ContactSubmissionUpdateSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Serializer for updating contact submissions (admin use).
|
|
"""
|
|
|
|
class Meta:
|
|
model = ContactSubmission
|
|
fields = [
|
|
'status',
|
|
'priority',
|
|
'admin_notes',
|
|
'assigned_to',
|
|
]
|
|
|
|
def validate_status(self, value):
|
|
"""
|
|
Validate status transitions.
|
|
"""
|
|
# Add business logic for status transitions if needed
|
|
return value
|