Forms and User Input

Form Validation

Django's form validation system provides multiple layers of data validation, from field-level checks to complex form-wide business rules. Understanding validation patterns enables you to build robust forms that ensure data integrity and provide meaningful user feedback.

Form Validation

Django's form validation system provides multiple layers of data validation, from field-level checks to complex form-wide business rules. Understanding validation patterns enables you to build robust forms that ensure data integrity and provide meaningful user feedback.

Validation Hierarchy

Understanding Validation Flow

# forms.py - Validation flow demonstration
from django import forms
from django.core.exceptions import ValidationError

class ComprehensiveValidationForm(forms.Form):
    """Form demonstrating the complete validation hierarchy"""
    
    username = forms.CharField(max_length=30)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirm_password = forms.CharField(widget=forms.PasswordInput)
    age = forms.IntegerField()
    
    def clean_username(self):
        """Step 1: Field-level validation"""
        username = self.cleaned_data['username']
        
        print(f"1. Field validation for username: {username}")
        
        # Length validation
        if len(username) < 3:
            raise ValidationError('Username must be at least 3 characters long.')
        
        # Character validation
        if not username.isalnum():
            raise ValidationError('Username must contain only letters and numbers.')
        
        # Availability check
        from django.contrib.auth.models import User
        if User.objects.filter(username=username).exists():
            raise ValidationError('This username is already taken.')
        
        return username
    
    def clean_email(self):
        """Step 1: Field-level validation for email"""
        email = self.cleaned_data['email']
        
        print(f"1. Field validation for email: {email}")
        
        # Domain validation
        allowed_domains = ['gmail.com', 'yahoo.com', 'company.com']
        domain = email.split('@')[1]
        if domain not in allowed_domains:
            raise ValidationError(f'Email domain {domain} is not allowed.')
        
        return email
    
    def clean_password(self):
        """Step 1: Field-level validation for password"""
        password = self.cleaned_data['password']
        
        print(f"1. Field validation for password")
        
        # Length check
        if len(password) < 8:
            raise ValidationError('Password must be at least 8 characters long.')
        
        # Complexity check
        if password.isdigit():
            raise ValidationError('Password cannot be entirely numeric.')
        
        # Common password check
        common_passwords = ['password', '12345678', 'qwerty', 'abc123']
        if password.lower() in common_passwords:
            raise ValidationError('Password is too common.')
        
        return password
    
    def clean_age(self):
        """Step 1: Field-level validation for age"""
        age = self.cleaned_data['age']
        
        print(f"1. Field validation for age: {age}")
        
        if age < 13:
            raise ValidationError('You must be at least 13 years old.')
        
        if age > 120:
            raise ValidationError('Please enter a realistic age.')
        
        return age
    
    def clean(self):
        """Step 2: Form-level validation"""
        cleaned_data = super().clean()
        
        print("2. Form-level validation")
        
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        username = cleaned_data.get('username')
        
        # Password confirmation
        if password and confirm_password:
            if password != confirm_password:
                raise ValidationError('Passwords do not match.')
        
        # Username-password similarity
        if username and password:
            if username.lower() in password.lower():
                raise ValidationError('Password cannot contain the username.')
        
        return cleaned_data
    
    def full_clean(self):
        """Step 3: Complete form validation (rarely overridden)"""
        print("3. Full form validation")
        super().full_clean()
        
        # Additional business logic validation
        self._validate_business_rules()
    
    def _validate_business_rules(self):
        """Custom business rule validation"""
        print("4. Business rules validation")
        
        if hasattr(self, 'cleaned_data'):
            username = self.cleaned_data.get('username')
            email = self.cleaned_data.get('email')
            
            # Business rule: username cannot be part of email
            if username and email and username in email:
                self.add_error(None, 'Username cannot be part of email address.')

Field-Level Validation

Built-in Validators

# forms.py - Using built-in validators
from django import forms
from django.core.validators import (
    RegexValidator, EmailValidator, URLValidator,
    MinLengthValidator, MaxLengthValidator,
    MinValueValidator, MaxValueValidator,
    DecimalValidator, validate_email
)

class ValidatorExamplesForm(forms.Form):
    """Examples of built-in validators"""
    
    # Regex validation
    phone = forms.CharField(
        max_length=15,
        validators=[
            RegexValidator(
                regex=r'^\+?1?\d{9,15}$',
                message='Phone number must be in format: "+999999999". Up to 15 digits allowed.'
            )
        ]
    )
    
    # Multiple validators on one field
    username = forms.CharField(
        validators=[
            MinLengthValidator(3, message='Username must be at least 3 characters.'),
            MaxLengthValidator(20, message='Username cannot exceed 20 characters.'),
            RegexValidator(
                regex=r'^[a-zA-Z0-9_]+$',
                message='Username can only contain letters, numbers, and underscores.'
            )
        ]
    )
    
    # Numeric validators
    age = forms.IntegerField(
        validators=[
            MinValueValidator(13, message='You must be at least 13 years old.'),
            MaxValueValidator(120, message='Please enter a realistic age.')
        ]
    )
    
    # Decimal validation
    price = forms.DecimalField(
        max_digits=10,
        decimal_places=2,
        validators=[
            MinValueValidator(0.01, message='Price must be at least $0.01.'),
            MaxValueValidator(999999.99, message='Price cannot exceed $999,999.99.')
        ]
    )
    
    # URL validation with custom message
    website = forms.URLField(
        required=False,
        validators=[
            URLValidator(
                message='Please enter a valid URL starting with http:// or https://'
            )
        ]
    )
    
    # Email validation
    email = forms.CharField(  # Using CharField instead of EmailField for custom validation
        validators=[
            EmailValidator(message='Please enter a valid email address.')
        ]
    )

Custom Validators

# validators.py - Custom validator functions
from django.core.exceptions import ValidationError
import re

def validate_strong_password(password):
    """Validate password strength"""
    if len(password) < 8:
        raise ValidationError('Password must be at least 8 characters long.')
    
    if not re.search(r'[A-Z]', password):
        raise ValidationError('Password must contain at least one uppercase letter.')
    
    if not re.search(r'[a-z]', password):
        raise ValidationError('Password must contain at least one lowercase letter.')
    
    if not re.search(r'\d', password):
        raise ValidationError('Password must contain at least one digit.')
    
    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        raise ValidationError('Password must contain at least one special character.')

def validate_file_size(file):
    """Validate uploaded file size"""
    max_size = 5 * 1024 * 1024  # 5MB
    if file.size > max_size:
        raise ValidationError(f'File size cannot exceed 5MB. Current size: {file.size / (1024*1024):.1f}MB')

def validate_image_dimensions(image):
    """Validate image dimensions"""
    max_width = 1920
    max_height = 1080
    
    if image.width > max_width or image.height > max_height:
        raise ValidationError(
            f'Image dimensions cannot exceed {max_width}x{max_height}px. '
            f'Current dimensions: {image.width}x{image.height}px'
        )

def validate_profanity(text):
    """Basic profanity filter"""
    profanity_words = ['badword1', 'badword2', 'badword3']  # Replace with actual list
    text_lower = text.lower()
    
    for word in profanity_words:
        if word in text_lower:
            raise ValidationError('Content contains inappropriate language.')

def validate_business_hours(time_value):
    """Validate time is within business hours"""
    from datetime import time
    
    business_start = time(9, 0)  # 9:00 AM
    business_end = time(17, 0)   # 5:00 PM
    
    if not (business_start <= time_value <= business_end):
        raise ValidationError('Time must be within business hours (9:00 AM - 5:00 PM).')

# forms.py - Using custom validators
class AdvancedValidationForm(forms.Form):
    """Form using custom validators"""
    
    password = forms.CharField(
        widget=forms.PasswordInput,
        validators=[validate_strong_password]
    )
    
    profile_image = forms.ImageField(
        validators=[validate_file_size, validate_image_dimensions]
    )
    
    bio = forms.CharField(
        widget=forms.Textarea,
        validators=[validate_profanity]
    )
    
    appointment_time = forms.TimeField(
        validators=[validate_business_hours]
    )

Form-Level Validation

Cross-Field Validation

# forms.py - Complex form-level validation
from django import forms
from django.core.exceptions import ValidationError
from datetime import date, datetime, timedelta

class EventBookingForm(forms.Form):
    """Event booking with complex validation rules"""
    
    event_name = forms.CharField(max_length=200)
    start_date = forms.DateField()
    end_date = forms.DateField()
    start_time = forms.TimeField()
    end_time = forms.TimeField()
    attendees = forms.IntegerField(min_value=1)
    budget = forms.DecimalField(max_digits=10, decimal_places=2)
    
    # Room selection
    ROOM_CHOICES = [
        ('small', 'Small Room (10 people) - $100/day'),
        ('medium', 'Medium Room (25 people) - $200/day'),
        ('large', 'Large Room (50 people) - $300/day'),
        ('auditorium', 'Auditorium (100+ people) - $500/day'),
    ]
    
    room_type = forms.ChoiceField(choices=ROOM_CHOICES)
    
    # Additional services
    catering = forms.BooleanField(required=False, label='Catering Required')
    av_equipment = forms.BooleanField(required=False, label='A/V Equipment')
    parking = forms.BooleanField(required=False, label='Reserved Parking')
    
    def clean(self):
        """Comprehensive form-level validation"""
        cleaned_data = super().clean()
        
        # Date validation
        self._validate_dates(cleaned_data)
        
        # Time validation
        self._validate_times(cleaned_data)
        
        # Capacity validation
        self._validate_capacity(cleaned_data)
        
        # Budget validation
        self._validate_budget(cleaned_data)
        
        # Availability validation
        self._validate_availability(cleaned_data)
        
        return cleaned_data
    
    def _validate_dates(self, data):
        """Validate date range"""
        start_date = data.get('start_date')
        end_date = data.get('end_date')
        
        if start_date and end_date:
            # End date must be after start date
            if end_date < start_date:
                raise ValidationError('End date must be after start date.')
            
            # Cannot book in the past
            if start_date < date.today():
                raise ValidationError('Cannot book events in the past.')
            
            # Maximum booking duration
            if (end_date - start_date).days > 30:
                raise ValidationError('Events cannot exceed 30 days.')
            
            # Minimum advance booking
            if start_date < date.today() + timedelta(days=2):
                raise ValidationError('Events must be booked at least 2 days in advance.')
    
    def _validate_times(self, data):
        """Validate time range"""
        start_time = data.get('start_time')
        end_time = data.get('end_time')
        start_date = data.get('start_date')
        end_date = data.get('end_date')
        
        if start_time and end_time and start_date and end_date:
            # For same-day events, end time must be after start time
            if start_date == end_date and end_time <= start_time:
                raise ValidationError('End time must be after start time for same-day events.')
            
            # Business hours validation
            from datetime import time
            business_start = time(8, 0)
            business_end = time(22, 0)
            
            if not (business_start <= start_time <= business_end):
                raise ValidationError('Start time must be within business hours (8:00 AM - 10:00 PM).')
            
            if not (business_start <= end_time <= business_end):
                raise ValidationError('End time must be within business hours (8:00 AM - 10:00 PM).')
    
    def _validate_capacity(self, data):
        """Validate room capacity vs attendees"""
        room_type = data.get('room_type')
        attendees = data.get('attendees')
        
        if room_type and attendees:
            capacity_limits = {
                'small': 10,
                'medium': 25,
                'large': 50,
                'auditorium': 200
            }
            
            max_capacity = capacity_limits.get(room_type, 0)
            if attendees > max_capacity:
                raise ValidationError(
                    f'Selected room can accommodate maximum {max_capacity} people. '
                    f'You have {attendees} attendees.'
                )
    
    def _validate_budget(self, data):
        """Validate budget against estimated costs"""
        room_type = data.get('room_type')
        budget = data.get('budget')
        start_date = data.get('start_date')
        end_date = data.get('end_date')
        catering = data.get('catering', False)
        av_equipment = data.get('av_equipment', False)
        parking = data.get('parking', False)
        attendees = data.get('attendees', 0)
        
        if all([room_type, budget, start_date, end_date]):
            # Calculate estimated cost
            room_costs = {
                'small': 100,
                'medium': 200,
                'large': 300,
                'auditorium': 500
            }
            
            days = (end_date - start_date).days + 1
            room_cost = room_costs.get(room_type, 0) * days
            
            additional_costs = 0
            if catering:
                additional_costs += attendees * 25  # $25 per person
            if av_equipment:
                additional_costs += 150 * days
            if parking:
                additional_costs += 50 * days
            
            total_estimated_cost = room_cost + additional_costs
            
            if budget < total_estimated_cost:
                raise ValidationError(
                    f'Budget (${budget}) is insufficient. '
                    f'Estimated cost: ${total_estimated_cost} '
                    f'(Room: ${room_cost}, Additional: ${additional_costs})'
                )
    
    def _validate_availability(self, data):
        """Check room availability (simplified)"""
        room_type = data.get('room_type')
        start_date = data.get('start_date')
        end_date = data.get('end_date')
        
        if all([room_type, start_date, end_date]):
            # In a real application, check against booking database
            # This is a simplified example
            
            # Simulate checking availability
            from .models import Booking
            
            conflicting_bookings = Booking.objects.filter(
                room_type=room_type,
                start_date__lte=end_date,
                end_date__gte=start_date
            )
            
            if conflicting_bookings.exists():
                raise ValidationError(
                    f'Selected room is not available for the chosen dates. '
                    f'Please select different dates or room type.'
                )

Dynamic Validation

Conditional Validation Rules

# forms.py - Dynamic validation based on form state
from django import forms

class DynamicValidationForm(forms.Form):
    """Form with validation rules that change based on user input"""
    
    USER_TYPES = [
        ('individual', 'Individual'),
        ('business', 'Business'),
        ('nonprofit', 'Non-Profit Organization'),
    ]
    
    user_type = forms.ChoiceField(choices=USER_TYPES)
    
    # Common fields
    email = forms.EmailField()
    phone = forms.CharField(max_length=15)
    
    # Individual fields
    first_name = forms.CharField(max_length=30, required=False)
    last_name = forms.CharField(max_length=30, required=False)
    date_of_birth = forms.DateField(required=False)
    
    # Business fields
    company_name = forms.CharField(max_length=100, required=False)
    tax_id = forms.CharField(max_length=20, required=False)
    annual_revenue = forms.DecimalField(max_digits=12, decimal_places=2, required=False)
    
    # Non-profit fields
    organization_name = forms.CharField(max_length=100, required=False)
    nonprofit_id = forms.CharField(max_length=20, required=False)
    mission_statement = forms.CharField(widget=forms.Textarea, required=False)
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Set initial field requirements based on data
        if self.data:
            user_type = self.data.get('user_type')
            self._update_field_requirements(user_type)
    
    def _update_field_requirements(self, user_type):
        """Update field requirements based on user type"""
        
        if user_type == 'individual':
            self.fields['first_name'].required = True
            self.fields['last_name'].required = True
            self.fields['date_of_birth'].required = True
            
        elif user_type == 'business':
            self.fields['company_name'].required = True
            self.fields['tax_id'].required = True
            self.fields['annual_revenue'].required = True
            
        elif user_type == 'nonprofit':
            self.fields['organization_name'].required = True
            self.fields['nonprofit_id'].required = True
            self.fields['mission_statement'].required = True
    
    def clean(self):
        cleaned_data = super().clean()
        user_type = cleaned_data.get('user_type')
        
        # Apply type-specific validation
        if user_type == 'individual':
            self._validate_individual(cleaned_data)
        elif user_type == 'business':
            self._validate_business(cleaned_data)
        elif user_type == 'nonprofit':
            self._validate_nonprofit(cleaned_data)
        
        return cleaned_data
    
    def _validate_individual(self, data):
        """Validation specific to individual users"""
        date_of_birth = data.get('date_of_birth')
        
        if date_of_birth:
            from datetime import date
            today = date.today()
            age = today.year - date_of_birth.year - ((today.month, today.day) < (date_of_birth.month, date_of_birth.day))
            
            if age < 18:
                raise ValidationError('Individual users must be at least 18 years old.')
    
    def _validate_business(self, data):
        """Validation specific to business users"""
        tax_id = data.get('tax_id')
        annual_revenue = data.get('annual_revenue')
        
        if tax_id:
            # Validate tax ID format (simplified)
            import re
            if not re.match(r'^\d{2}-\d{7}$', tax_id):
                raise ValidationError('Tax ID must be in format: XX-XXXXXXX')
        
        if annual_revenue and annual_revenue < 0:
            raise ValidationError('Annual revenue cannot be negative.')
    
    def _validate_nonprofit(self, data):
        """Validation specific to nonprofit organizations"""
        nonprofit_id = data.get('nonprofit_id')
        mission_statement = data.get('mission_statement')
        
        if nonprofit_id:
            # Validate nonprofit ID format
            if not nonprofit_id.startswith('NP'):
                raise ValidationError('Non-profit ID must start with "NP".')
        
        if mission_statement and len(mission_statement) < 50:
            raise ValidationError('Mission statement must be at least 50 characters long.')

Validation Error Handling

Custom Error Messages and Formatting

# forms.py - Advanced error handling
from django import forms
from django.core.exceptions import ValidationError

class ErrorHandlingForm(forms.Form):
    """Form demonstrating advanced error handling techniques"""
    
    username = forms.CharField(max_length=30)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    
    # Custom error messages
    error_messages = {
        'username': {
            'required': 'Please provide a username.',
            'max_length': 'Username cannot exceed 30 characters.',
        },
        'email': {
            'required': 'Email address is required.',
            'invalid': 'Please enter a valid email address.',
        },
        'password': {
            'required': 'Password is required.',
        }
    }
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Apply custom error messages
        for field_name, messages in self.error_messages.items():
            if field_name in self.fields:
                self.fields[field_name].error_messages.update(messages)
    
    def clean_username(self):
        username = self.cleaned_data['username']
        
        # Multiple validation checks with specific error messages
        errors = []
        
        if len(username) < 3:
            errors.append('Username must be at least 3 characters long.')
        
        if not username.isalnum():
            errors.append('Username can only contain letters and numbers.')
        
        if username.lower() in ['admin', 'root', 'user']:
            errors.append('This username is reserved and cannot be used.')
        
        # Check availability
        from django.contrib.auth.models import User
        if User.objects.filter(username=username).exists():
            errors.append('This username is already taken.')
        
        if errors:
            raise ValidationError(errors)
        
        return username
    
    def add_error(self, field, error):
        """Override to customize error handling"""
        # Log validation errors
        import logging
        logger = logging.getLogger('form_validation')
        logger.warning(f'Validation error in {self.__class__.__name__}.{field}: {error}')
        
        # Call parent method
        super().add_error(field, error)
    
    def clean(self):
        cleaned_data = super().clean()
        
        # Collect all validation errors
        validation_errors = []
        
        username = cleaned_data.get('username')
        password = cleaned_data.get('password')
        
        # Cross-field validation with detailed errors
        if username and password:
            if username.lower() in password.lower():
                validation_errors.append(
                    ValidationError(
                        'Password cannot contain the username.',
                        code='password_contains_username'
                    )
                )
            
            if len(password) < 8:
                validation_errors.append(
                    ValidationError(
                        'Password must be at least 8 characters long.',
                        code='password_too_short'
                    )
                )
        
        if validation_errors:
            raise ValidationError(validation_errors)
        
        return cleaned_data

# Custom validation error class
class DetailedValidationError(ValidationError):
    """Enhanced validation error with additional context"""
    
    def __init__(self, message, code=None, params=None, field=None, suggestion=None):
        super().__init__(message, code, params)
        self.field = field
        self.suggestion = suggestion

# Usage in forms
class EnhancedForm(forms.Form):
    email = forms.EmailField()
    
    def clean_email(self):
        email = self.cleaned_data['email']
        
        if not email.endswith('@company.com'):
            raise DetailedValidationError(
                'Only company email addresses are allowed.',
                code='invalid_domain',
                field='email',
                suggestion='Please use your @company.com email address.'
            )
        
        return email

Async Validation

Asynchronous Field Validation

# forms.py - Async validation patterns
from django import forms
import asyncio
import aiohttp

class AsyncValidationForm(forms.Form):
    """Form with asynchronous validation"""
    
    username = forms.CharField(max_length=30)
    email = forms.EmailField()
    domain = forms.CharField(max_length=100)
    
    async def clean_username_async(self):
        """Async username validation"""
        username = self.cleaned_data['username']
        
        # Simulate API call to check username availability
        async with aiohttp.ClientSession() as session:
            async with session.get(f'https://api.example.com/check-username/{username}') as response:
                if response.status == 200:
                    data = await response.json()
                    if not data.get('available', True):
                        raise ValidationError('Username is not available.')
        
        return username
    
    async def clean_email_async(self):
        """Async email validation"""
        email = self.cleaned_data['email']
        
        # Validate email with external service
        async with aiohttp.ClientSession() as session:
            async with session.post('https://api.emailvalidation.com/validate', 
                                  json={'email': email}) as response:
                if response.status == 200:
                    data = await response.json()
                    if not data.get('valid', False):
                        raise ValidationError('Email address is not valid.')
        
        return email
    
    async def clean_domain_async(self):
        """Async domain validation"""
        domain = self.cleaned_data['domain']
        
        # Check if domain exists
        try:
            import socket
            socket.gethostbyname(domain)
        except socket.gaierror:
            raise ValidationError('Domain does not exist.')
        
        return domain
    
    async def full_clean_async(self):
        """Async version of full_clean"""
        # Run regular validation first
        super().full_clean()
        
        if not self.errors:
            # Run async validations
            tasks = []
            
            if 'username' in self.cleaned_data:
                tasks.append(self.clean_username_async())
            
            if 'email' in self.cleaned_data:
                tasks.append(self.clean_email_async())
            
            if 'domain' in self.cleaned_data:
                tasks.append(self.clean_domain_async())
            
            try:
                await asyncio.gather(*tasks)
            except ValidationError as e:
                self.add_error(None, e)

# views.py - Handling async validation
import asyncio
from django.http import JsonResponse

async def async_form_view(request):
    if request.method == 'POST':
        form = AsyncValidationForm(request.POST)
        
        # Run async validation
        await form.full_clean_async()
        
        if form.is_valid():
            # Process form
            return JsonResponse({'success': True})
        else:
            return JsonResponse({'errors': form.errors}, status=400)
    
    return render(request, 'async_form.html', {'form': AsyncValidationForm()})

Django's validation system provides comprehensive tools for ensuring data integrity at multiple levels. By understanding field-level validation, form-level cross-validation, dynamic validation rules, and advanced error handling techniques, you can build robust forms that provide clear feedback and maintain data quality while handling complex business requirements.