Forms and User Input

Forms and User Input

Django's form handling system provides a comprehensive framework for processing user input, validating data, and rendering HTML forms. This chapter covers everything from basic form creation to advanced techniques for handling complex user interactions.

Forms and User Input

Django's form handling system provides a comprehensive framework for processing user input, validating data, and rendering HTML forms. This chapter covers everything from basic form creation to advanced techniques for handling complex user interactions.

What Are Django Forms?

Django forms are Python classes that define the structure, validation rules, and rendering behavior of HTML forms. They provide a bridge between HTML form elements and Python data types, handling the conversion, validation, and security aspects automatically.

Key Benefits of Django Forms

Data Validation: Automatic validation of user input with built-in and custom validators Security: Built-in protection against CSRF attacks and XSS vulnerabilities
Rendering: Automatic HTML generation with customizable widgets Error Handling: Comprehensive error collection and display Data Conversion: Automatic conversion between HTML strings and Python data types

Basic Form Structure

Simple Contact Form

# forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    subject = forms.CharField(max_length=200)
    message = forms.CharField(widget=forms.Textarea)
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if not email.endswith('@company.com'):
            raise forms.ValidationError('Please use your company email address.')
        return email

# views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm

def contact_view(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # Process the form data
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            subject = form.cleaned_data['subject']
            message = form.cleaned_data['message']
            
            # Send email or save to database
            send_contact_email(name, email, subject, message)
            
            messages.success(request, 'Your message has been sent successfully!')
            return redirect('contact')
    else:
        form = ContactForm()
    
    return render(request, 'contact.html', {'form': form})

Template Integration

<!-- contact.html -->
<form method="post">
    {% csrf_token %}
    
    <div class="form-group">
        {{ form.name.label_tag }}
        {{ form.name }}
        {% if form.name.errors %}
            <div class="error">{{ form.name.errors }}</div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.email.label_tag }}
        {{ form.email }}
        {% if form.email.errors %}
            <div class="error">{{ form.email.errors }}</div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.subject.label_tag }}
        {{ form.subject }}
        {% if form.subject.errors %}
            <div class="error">{{ form.subject.errors }}</div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.message.label_tag }}
        {{ form.message }}
        {% if form.message.errors %}
            <div class="error">{{ form.message.errors }}</div>
        {% endif %}
    </div>
    
    <button type="submit">Send Message</button>
</form>

Form Processing Workflow

Complete Form Handling Pattern

# forms.py
from django import forms
from django.core.exceptions import ValidationError
import re

class UserRegistrationForm(forms.Form):
    username = forms.CharField(
        max_length=30,
        help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.'
    )
    email = forms.EmailField(
        help_text='We\'ll never share your email with anyone else.'
    )
    password1 = forms.CharField(
        label='Password',
        widget=forms.PasswordInput,
        help_text='Your password must contain at least 8 characters.'
    )
    password2 = forms.CharField(
        label='Password confirmation',
        widget=forms.PasswordInput,
        help_text='Enter the same password as before, for verification.'
    )
    age = forms.IntegerField(
        min_value=13,
        max_value=120,
        help_text='You must be at least 13 years old to register.'
    )
    terms = forms.BooleanField(
        required=True,
        help_text='You must accept the terms and conditions.'
    )
    
    def clean_username(self):
        username = self.cleaned_data['username']
        
        # Check if username already exists
        from django.contrib.auth.models import User
        if User.objects.filter(username=username).exists():
            raise ValidationError('This username is already taken.')
        
        # Check username format
        if not re.match(r'^[\w.@+-]+$', username):
            raise ValidationError('Username contains invalid characters.')
        
        return username
    
    def clean_password1(self):
        password1 = self.cleaned_data['password1']
        
        # Password strength validation
        if len(password1) < 8:
            raise ValidationError('Password must be at least 8 characters long.')
        
        if password1.isdigit():
            raise ValidationError('Password cannot be entirely numeric.')
        
        if password1.lower() in ['password', '12345678', 'qwerty']:
            raise ValidationError('Password is too common.')
        
        return password1
    
    def clean(self):
        cleaned_data = super().clean()
        password1 = cleaned_data.get('password1')
        password2 = cleaned_data.get('password2')
        
        if password1 and password2 and password1 != password2:
            raise ValidationError('Passwords do not match.')
        
        return cleaned_data

# views.py
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.contrib.auth import login
from django.contrib import messages
from .forms import UserRegistrationForm

def register_view(request):
    if request.method == 'POST':
        form = UserRegistrationForm(request.POST)
        if form.is_valid():
            # Create user account
            user = User.objects.create_user(
                username=form.cleaned_data['username'],
                email=form.cleaned_data['email'],
                password=form.cleaned_data['password1']
            )
            
            # Create user profile
            UserProfile.objects.create(
                user=user,
                age=form.cleaned_data['age']
            )
            
            # Log the user in
            login(request, user)
            
            messages.success(request, 'Registration successful! Welcome to our site.')
            return redirect('dashboard')
        else:
            messages.error(request, 'Please correct the errors below.')
    else:
        form = UserRegistrationForm()
    
    return render(request, 'registration/register.html', {'form': form})

Form Field Types and Widgets

Common Field Types

# forms.py
from django import forms
from django.core.validators import RegexValidator

class ComprehensiveForm(forms.Form):
    # Text fields
    name = forms.CharField(max_length=100)
    description = forms.CharField(widget=forms.Textarea(attrs={'rows': 4}))
    
    # Email and URL
    email = forms.EmailField()
    website = forms.URLField(required=False)
    
    # Numbers
    age = forms.IntegerField(min_value=0, max_value=150)
    price = forms.DecimalField(max_digits=10, decimal_places=2)
    rating = forms.FloatField(min_value=0.0, max_value=5.0)
    
    # Dates and times
    birth_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
    appointment_time = forms.DateTimeField(
        widget=forms.DateTimeInput(attrs={'type': 'datetime-local'})
    )
    
    # Choices
    CATEGORY_CHOICES = [
        ('tech', 'Technology'),
        ('health', 'Health'),
        ('education', 'Education'),
        ('entertainment', 'Entertainment'),
    ]
    category = forms.ChoiceField(choices=CATEGORY_CHOICES)
    
    # Multiple choices
    SKILL_CHOICES = [
        ('python', 'Python'),
        ('javascript', 'JavaScript'),
        ('java', 'Java'),
        ('csharp', 'C#'),
    ]
    skills = forms.MultipleChoiceField(
        choices=SKILL_CHOICES,
        widget=forms.CheckboxSelectMultiple
    )
    
    # Boolean fields
    is_active = forms.BooleanField(required=False)
    newsletter = forms.BooleanField(
        required=False,
        help_text='Subscribe to our newsletter'
    )
    
    # File uploads
    avatar = forms.ImageField(required=False)
    resume = forms.FileField(required=False)
    
    # Custom validation
    phone = forms.CharField(
        max_length=15,
        validators=[
            RegexValidator(
                regex=r'^\+?1?\d{9,15}$',
                message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
            )
        ]
    )
    
    # Hidden field
    source = forms.CharField(widget=forms.HiddenInput(), initial='web')

Custom Widgets

# widgets.py
from django import forms
from django.forms.widgets import Widget
from django.html import format_html

class ColorPickerWidget(Widget):
    """Custom color picker widget"""
    
    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = '#000000'
        
        final_attrs = self.build_attrs(attrs, {'type': 'color', 'name': name})
        if value:
            final_attrs['value'] = value
        
        return format_html('<input{} />', forms.utils.flatatt(final_attrs))

class RangeSliderWidget(Widget):
    """Custom range slider widget"""
    
    def __init__(self, attrs=None, min_value=0, max_value=100):
        self.min_value = min_value
        self.max_value = max_value
        super().__init__(attrs)
    
    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = self.min_value
        
        final_attrs = self.build_attrs(attrs, {
            'type': 'range',
            'name': name,
            'min': self.min_value,
            'max': self.max_value,
            'value': value
        })
        
        return format_html(
            '<input{} /><span id="{}_display">{}</span>',
            forms.utils.flatatt(final_attrs),
            name,
            value
        )

# forms.py using custom widgets
class ProductForm(forms.Form):
    name = forms.CharField(max_length=100)
    color = forms.CharField(widget=ColorPickerWidget())
    priority = forms.IntegerField(
        widget=RangeSliderWidget(min_value=1, max_value=10)
    )

Form Validation Patterns

Multi-Level Validation

# forms.py
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User

class ProfileUpdateForm(forms.Form):
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)
    email = forms.EmailField()
    bio = forms.CharField(widget=forms.Textarea, required=False)
    birth_date = forms.DateField()
    
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
    
    def clean_email(self):
        """Field-level validation for email"""
        email = self.cleaned_data['email']
        
        # Check if email is already taken by another user
        if User.objects.filter(email=email).exclude(pk=self.user.pk).exists():
            raise ValidationError('This email address is already in use.')
        
        # Check email domain
        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_birth_date(self):
        """Field-level validation for birth date"""
        birth_date = self.cleaned_data['birth_date']
        
        from datetime import date
        today = date.today()
        age = today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day))
        
        if age < 13:
            raise ValidationError('You must be at least 13 years old.')
        
        if age > 120:
            raise ValidationError('Please enter a valid birth date.')
        
        return birth_date
    
    def clean(self):
        """Form-level validation"""
        cleaned_data = super().clean()
        first_name = cleaned_data.get('first_name')
        last_name = cleaned_data.get('last_name')
        
        # Check if first and last name are the same
        if first_name and last_name and first_name.lower() == last_name.lower():
            raise ValidationError('First name and last name cannot be the same.')
        
        # Check for inappropriate content in bio
        bio = cleaned_data.get('bio')
        if bio:
            inappropriate_words = ['spam', 'scam', 'fake']
            if any(word in bio.lower() for word in inappropriate_words):
                raise ValidationError('Bio contains inappropriate content.')
        
        return cleaned_data

Dynamic Forms

Conditional Field Display

# forms.py
from django import forms

class DynamicEventForm(forms.Form):
    EVENT_TYPES = [
        ('conference', 'Conference'),
        ('workshop', 'Workshop'),
        ('webinar', 'Webinar'),
        ('meetup', 'Meetup'),
    ]
    
    title = forms.CharField(max_length=200)
    event_type = forms.ChoiceField(choices=EVENT_TYPES)
    description = forms.CharField(widget=forms.Textarea)
    start_date = forms.DateTimeField()
    end_date = forms.DateTimeField()
    
    # Conditional fields
    venue = forms.CharField(max_length=200, required=False)
    capacity = forms.IntegerField(min_value=1, required=False)
    webinar_link = forms.URLField(required=False)
    registration_fee = forms.DecimalField(max_digits=8, decimal_places=2, required=False)
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Add CSS classes for conditional display
        self.fields['venue'].widget.attrs.update({'class': 'physical-event-field'})
        self.fields['capacity'].widget.attrs.update({'class': 'physical-event-field'})
        self.fields['webinar_link'].widget.attrs.update({'class': 'online-event-field'})
    
    def clean(self):
        cleaned_data = super().clean()
        event_type = cleaned_data.get('event_type')
        venue = cleaned_data.get('venue')
        webinar_link = cleaned_data.get('webinar_link')
        
        # Validate based on event type
        if event_type in ['conference', 'workshop', 'meetup']:
            if not venue:
                raise ValidationError('Venue is required for physical events.')
        
        if event_type == 'webinar':
            if not webinar_link:
                raise ValidationError('Webinar link is required for online events.')
        
        # Validate date range
        start_date = cleaned_data.get('start_date')
        end_date = cleaned_data.get('end_date')
        
        if start_date and end_date and start_date >= end_date:
            raise ValidationError('End date must be after start date.')
        
        return cleaned_data

# JavaScript for dynamic field display
"""
<script>
document.addEventListener('DOMContentLoaded', function() {
    const eventTypeField = document.getElementById('id_event_type');
    const physicalFields = document.querySelectorAll('.physical-event-field');
    const onlineFields = document.querySelectorAll('.online-event-field');
    
    function toggleFields() {
        const eventType = eventTypeField.value;
        
        physicalFields.forEach(field => {
            field.closest('.form-group').style.display = 
                ['conference', 'workshop', 'meetup'].includes(eventType) ? 'block' : 'none';
        });
        
        onlineFields.forEach(field => {
            field.closest('.form-group').style.display = 
                eventType === 'webinar' ? 'block' : 'none';
        });
    }
    
    eventTypeField.addEventListener('change', toggleFields);
    toggleFields(); // Initial call
});
</script>
"""

Form Security Best Practices

CSRF Protection and Input Sanitization

# forms.py
from django import forms
from django.utils.html import strip_tags
from django.core.exceptions import ValidationError
import bleach

class SecureCommentForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    comment = forms.CharField(widget=forms.Textarea)
    
    def clean_name(self):
        """Sanitize name input"""
        name = self.cleaned_data['name']
        
        # Strip HTML tags
        name = strip_tags(name)
        
        # Remove excessive whitespace
        name = ' '.join(name.split())
        
        # Check for minimum length
        if len(name) < 2:
            raise ValidationError('Name must be at least 2 characters long.')
        
        return name
    
    def clean_comment(self):
        """Sanitize comment with allowed HTML"""
        comment = self.cleaned_data['comment']
        
        # Allow only safe HTML tags
        allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li']
        allowed_attributes = {}
        
        # Clean the HTML
        clean_comment = bleach.clean(
            comment,
            tags=allowed_tags,
            attributes=allowed_attributes,
            strip=True
        )
        
        # Check for minimum content
        if len(strip_tags(clean_comment)) < 10:
            raise ValidationError('Comment must be at least 10 characters long.')
        
        return clean_comment

# views.py with additional security
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.cache import never_cache
from django.utils.decorators import method_decorator

@method_decorator([csrf_protect, never_cache], name='dispatch')
class SecureCommentView(View):
    def post(self, request):
        form = SecureCommentForm(request.POST)
        
        if form.is_valid():
            # Additional security checks
            if self.is_spam(form.cleaned_data):
                return JsonResponse({'error': 'Spam detected'}, status=400)
            
            # Rate limiting check
            if self.is_rate_limited(request):
                return JsonResponse({'error': 'Rate limit exceeded'}, status=429)
            
            # Save comment
            self.save_comment(form.cleaned_data, request)
            
            return JsonResponse({'success': True})
        
        return JsonResponse({'errors': form.errors}, status=400)
    
    def is_spam(self, data):
        """Simple spam detection"""
        spam_keywords = ['viagra', 'casino', 'lottery', 'winner']
        comment_lower = data['comment'].lower()
        return any(keyword in comment_lower for keyword in spam_keywords)
    
    def is_rate_limited(self, request):
        """Check rate limiting"""
        from django.core.cache import cache
        
        ip = self.get_client_ip(request)
        cache_key = f'comment_rate_limit_{ip}'
        
        current_count = cache.get(cache_key, 0)
        if current_count >= 5:  # 5 comments per hour
            return True
        
        cache.set(cache_key, current_count + 1, 3600)  # 1 hour
        return False

Django's form system provides a robust foundation for handling user input securely and efficiently. Understanding form structure, validation patterns, and security considerations enables you to build sophisticated user interfaces that maintain data integrity and protect against common web vulnerabilities.