Forms and User Input

Creating Forms with Forms API

Django's Forms API provides a powerful and flexible way to define, validate, and render forms. This chapter covers the complete process of creating forms using Django's declarative form system.

Creating Forms with Forms API

Django's Forms API provides a powerful and flexible way to define, validate, and render forms. This chapter covers the complete process of creating forms using Django's declarative form system.

Basic Form Creation

Simple Form Definition

# forms.py
from django import forms

class ContactForm(forms.Form):
    """Basic contact form example"""
    
    name = forms.CharField(
        max_length=100,
        label='Full Name',
        help_text='Enter your full name'
    )
    
    email = forms.EmailField(
        label='Email Address',
        help_text='We will never share your email'
    )
    
    subject = forms.CharField(
        max_length=200,
        label='Subject'
    )
    
    message = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 5}),
        label='Message',
        help_text='Please provide detailed information'
    )
    
    urgent = forms.BooleanField(
        required=False,
        label='Urgent Request',
        help_text='Check if this requires immediate attention'
    )

# 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 form data
            send_contact_email(
                name=form.cleaned_data['name'],
                email=form.cleaned_data['email'],
                subject=form.cleaned_data['subject'],
                message=form.cleaned_data['message'],
                urgent=form.cleaned_data['urgent']
            )
            messages.success(request, 'Your message has been sent!')
            return redirect('contact_success')
    else:
        form = ContactForm()
    
    return render(request, 'contact.html', {'form': form})

Form Field Configuration

# forms.py - Comprehensive field configuration
from django import forms
from django.core.validators import RegexValidator, MinLengthValidator

class UserRegistrationForm(forms.Form):
    """Comprehensive user registration form"""
    
    # Text fields with various configurations
    username = forms.CharField(
        max_length=30,
        min_length=3,
        label='Username',
        help_text='3-30 characters. Letters, numbers, and underscores only.',
        validators=[
            RegexValidator(
                regex=r'^[a-zA-Z0-9_]+$',
                message='Username can only contain letters, numbers, and underscores.'
            )
        ],
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Choose a username',
            'autocomplete': 'username'
        })
    )
    
    # Email field with custom validation
    email = forms.EmailField(
        label='Email Address',
        help_text='We will send a confirmation email to this address.',
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': 'your.email@example.com',
            'autocomplete': 'email'
        })
    )
    
    # Password fields
    password1 = forms.CharField(
        label='Password',
        min_length=8,
        help_text='Password must be at least 8 characters long.',
        widget=forms.PasswordInput(attrs={
            'class': 'form-control',
            'placeholder': 'Create a strong password',
            'autocomplete': 'new-password'
        })
    )
    
    password2 = forms.CharField(
        label='Confirm Password',
        help_text='Enter the same password as above for verification.',
        widget=forms.PasswordInput(attrs={
            'class': 'form-control',
            'placeholder': 'Confirm your password',
            'autocomplete': 'new-password'
        })
    )
    
    # Date field
    birth_date = forms.DateField(
        label='Date of Birth',
        help_text='Format: YYYY-MM-DD',
        widget=forms.DateInput(attrs={
            'class': 'form-control',
            'type': 'date'
        })
    )
    
    # Choice field
    GENDER_CHOICES = [
        ('', 'Select Gender'),
        ('M', 'Male'),
        ('F', 'Female'),
        ('O', 'Other'),
        ('N', 'Prefer not to say'),
    ]
    
    gender = forms.ChoiceField(
        choices=GENDER_CHOICES,
        required=False,
        label='Gender',
        widget=forms.Select(attrs={'class': 'form-control'})
    )
    
    # Multiple choice field
    INTEREST_CHOICES = [
        ('tech', 'Technology'),
        ('sports', 'Sports'),
        ('music', 'Music'),
        ('travel', 'Travel'),
        ('cooking', 'Cooking'),
        ('reading', 'Reading'),
    ]
    
    interests = forms.MultipleChoiceField(
        choices=INTEREST_CHOICES,
        required=False,
        label='Interests',
        help_text='Select all that apply.',
        widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'})
    )
    
    # File field
    profile_picture = forms.ImageField(
        required=False,
        label='Profile Picture',
        help_text='Upload a profile picture (optional). Max size: 2MB.',
        widget=forms.FileInput(attrs={
            'class': 'form-control',
            'accept': 'image/*'
        })
    )
    
    # Boolean field
    terms_accepted = forms.BooleanField(
        label='I accept the Terms of Service and Privacy Policy',
        help_text='You must accept the terms to create an account.',
        widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
    )
    
    newsletter = forms.BooleanField(
        required=False,
        label='Subscribe to newsletter',
        help_text='Receive updates about new features and content.',
        widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
    )

Dynamic Form Generation

Forms with Dynamic Fields

# forms.py - Dynamic form creation
from django import forms

class DynamicSurveyForm(forms.Form):
    """Form that generates fields dynamically"""
    
    def __init__(self, questions=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        if questions:
            for question in questions:
                field_name = f'question_{question.id}'
                
                if question.question_type == 'text':
                    self.fields[field_name] = forms.CharField(
                        label=question.text,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.TextInput(attrs={'class': 'form-control'})
                    )
                
                elif question.question_type == 'textarea':
                    self.fields[field_name] = forms.CharField(
                        label=question.text,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.Textarea(attrs={
                            'class': 'form-control',
                            'rows': 4
                        })
                    )
                
                elif question.question_type == 'choice':
                    choices = [(choice.id, choice.text) for choice in question.choices.all()]
                    self.fields[field_name] = forms.ChoiceField(
                        label=question.text,
                        choices=choices,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.RadioSelect(attrs={'class': 'form-check-input'})
                    )
                
                elif question.question_type == 'multiple_choice':
                    choices = [(choice.id, choice.text) for choice in question.choices.all()]
                    self.fields[field_name] = forms.MultipleChoiceField(
                        label=question.text,
                        choices=choices,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'})
                    )
                
                elif question.question_type == 'number':
                    self.fields[field_name] = forms.IntegerField(
                        label=question.text,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.NumberInput(attrs={'class': 'form-control'})
                    )
                
                elif question.question_type == 'email':
                    self.fields[field_name] = forms.EmailField(
                        label=question.text,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.EmailInput(attrs={'class': 'form-control'})
                    )
                
                elif question.question_type == 'date':
                    self.fields[field_name] = forms.DateField(
                        label=question.text,
                        required=question.required,
                        help_text=question.help_text,
                        widget=forms.DateInput(attrs={
                            'class': 'form-control',
                            'type': 'date'
                        })
                    )

# models.py - Supporting models for dynamic forms
from django.db import models

class Survey(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)

class Question(models.Model):
    QUESTION_TYPES = [
        ('text', 'Text Input'),
        ('textarea', 'Text Area'),
        ('choice', 'Single Choice'),
        ('multiple_choice', 'Multiple Choice'),
        ('number', 'Number'),
        ('email', 'Email'),
        ('date', 'Date'),
    ]
    
    survey = models.ForeignKey(Survey, on_delete=models.CASCADE, related_name='questions')
    text = models.TextField()
    question_type = models.CharField(max_length=20, choices=QUESTION_TYPES)
    required = models.BooleanField(default=True)
    help_text = models.CharField(max_length=200, blank=True)
    order = models.PositiveIntegerField(default=0)
    
    class Meta:
        ordering = ['order']

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='choices')
    text = models.CharField(max_length=200)
    order = models.PositiveIntegerField(default=0)
    
    class Meta:
        ordering = ['order']

# views.py - Using dynamic forms
def survey_view(request, survey_id):
    survey = get_object_or_404(Survey, id=survey_id, is_active=True)
    questions = survey.questions.all()
    
    if request.method == 'POST':
        form = DynamicSurveyForm(questions=questions, data=request.POST)
        if form.is_valid():
            # Process survey responses
            save_survey_responses(survey, form.cleaned_data, request.user)
            messages.success(request, 'Survey submitted successfully!')
            return redirect('survey_thanks')
    else:
        form = DynamicSurveyForm(questions=questions)
    
    return render(request, 'survey.html', {
        'survey': survey,
        'form': form,
        'questions': questions
    })

Conditional Field Display

# forms.py - Conditional fields based on user input
from django import forms

class EventRegistrationForm(forms.Form):
    """Event registration with conditional fields"""
    
    # Basic information
    name = forms.CharField(max_length=100, label='Full Name')
    email = forms.EmailField(label='Email Address')
    
    # Event type selection
    EVENT_TYPES = [
        ('online', 'Online Event'),
        ('in_person', 'In-Person Event'),
        ('hybrid', 'Hybrid Event'),
    ]
    
    event_type = forms.ChoiceField(
        choices=EVENT_TYPES,
        label='Event Type',
        widget=forms.RadioSelect(attrs={'class': 'event-type-selector'})
    )
    
    # Conditional fields for in-person events
    dietary_restrictions = forms.CharField(
        required=False,
        label='Dietary Restrictions',
        help_text='Please specify any dietary restrictions or allergies.',
        widget=forms.Textarea(attrs={
            'rows': 3,
            'class': 'form-control in-person-field'
        })
    )
    
    transportation = forms.ChoiceField(
        choices=[
            ('', 'Select Transportation'),
            ('car', 'Car'),
            ('public', 'Public Transport'),
            ('bike', 'Bicycle'),
            ('walk', 'Walking'),
        ],
        required=False,
        label='Transportation Method',
        widget=forms.Select(attrs={'class': 'form-control in-person-field'})
    )
    
    # Conditional fields for online events
    timezone = forms.ChoiceField(
        choices=[
            ('', 'Select Timezone'),
            ('UTC', 'UTC'),
            ('EST', 'Eastern Time'),
            ('PST', 'Pacific Time'),
            ('GMT', 'Greenwich Mean Time'),
        ],
        required=False,
        label='Timezone',
        widget=forms.Select(attrs={'class': 'form-control online-field'})
    )
    
    internet_speed = forms.ChoiceField(
        choices=[
            ('', 'Select Internet Speed'),
            ('low', 'Low (< 5 Mbps)'),
            ('medium', 'Medium (5-25 Mbps)'),
            ('high', 'High (> 25 Mbps)'),
        ],
        required=False,
        label='Internet Connection Speed',
        widget=forms.Select(attrs={'class': 'form-control online-field'})
    )
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Add JavaScript data attributes for conditional display
        self.fields['event_type'].widget.attrs.update({
            'data-conditional': 'true',
            'data-target-in-person': '.in-person-field',
            'data-target-online': '.online-field'
        })
    
    def clean(self):
        cleaned_data = super().clean()
        event_type = cleaned_data.get('event_type')
        
        # Validate conditional fields
        if event_type == 'in_person':
            # Make in-person fields required
            if not cleaned_data.get('transportation'):
                self.add_error('transportation', 'Transportation method is required for in-person events.')
        
        elif event_type == 'online':
            # Make online fields required
            if not cleaned_data.get('timezone'):
                self.add_error('timezone', 'Timezone is required for online events.')
        
        elif event_type == 'hybrid':
            # Hybrid events need both sets of information
            if not cleaned_data.get('timezone'):
                self.add_error('timezone', 'Timezone is required for hybrid events.')
        
        return cleaned_data

Form Inheritance and Composition

Base Form Classes

# forms.py - Form inheritance patterns
from django import forms
from django.contrib.auth.models import User

class BaseForm(forms.Form):
    """Base form with common functionality"""
    
    def __init__(self, *args, **kwargs):
        # Extract custom parameters
        self.user = kwargs.pop('user', None)
        self.request = kwargs.pop('request', None)
        
        super().__init__(*args, **kwargs)
        
        # Apply common styling
        self.apply_bootstrap_classes()
        
        # Add common validation
        self.add_common_validators()
    
    def apply_bootstrap_classes(self):
        """Apply Bootstrap CSS classes to all fields"""
        for field_name, field in self.fields.items():
            if isinstance(field.widget, (forms.TextInput, forms.EmailInput, 
                                       forms.PasswordInput, forms.NumberInput)):
                field.widget.attrs.update({'class': 'form-control'})
            elif isinstance(field.widget, forms.Textarea):
                field.widget.attrs.update({'class': 'form-control'})
            elif isinstance(field.widget, forms.Select):
                field.widget.attrs.update({'class': 'form-select'})
            elif isinstance(field.widget, forms.CheckboxInput):
                field.widget.attrs.update({'class': 'form-check-input'})
    
    def add_common_validators(self):
        """Add common validation rules"""
        # Add CSRF protection awareness
        if hasattr(self, 'request') and self.request:
            # Perform request-based validation
            pass
    
    def clean(self):
        """Common form-level validation"""
        cleaned_data = super().clean()
        
        # Perform common security checks
        self.check_for_spam(cleaned_data)
        
        return cleaned_data
    
    def check_for_spam(self, data):
        """Basic spam detection"""
        spam_keywords = ['viagra', 'casino', 'lottery', 'winner', 'free money']
        
        for field_name, value in data.items():
            if isinstance(value, str):
                value_lower = value.lower()
                for keyword in spam_keywords:
                    if keyword in value_lower:
                        self.add_error(field_name, 'Content appears to be spam.')
                        break

class UserInfoForm(BaseForm):
    """Form for user information with base functionality"""
    
    first_name = forms.CharField(max_length=30, label='First Name')
    last_name = forms.CharField(max_length=30, label='Last Name')
    email = forms.EmailField(label='Email Address')
    
    def clean_email(self):
        email = self.cleaned_data['email']
        
        # Check if email is already taken (excluding current user)
        queryset = User.objects.filter(email=email)
        if self.user:
            queryset = queryset.exclude(pk=self.user.pk)
        
        if queryset.exists():
            raise forms.ValidationError('This email address is already in use.')
        
        return email

class ContactForm(BaseForm):
    """Contact form extending base functionality"""
    
    name = forms.CharField(max_length=100, label='Full Name')
    email = forms.EmailField(label='Email Address')
    subject = forms.CharField(max_length=200, label='Subject')
    message = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 5}),
        label='Message'
    )
    
    def clean_message(self):
        message = self.cleaned_data['message']
        
        # Minimum message length
        if len(message.strip()) < 10:
            raise forms.ValidationError('Message must be at least 10 characters long.')
        
        return message

class FeedbackForm(BaseForm):
    """Feedback form with rating system"""
    
    RATING_CHOICES = [
        (1, '1 - Very Poor'),
        (2, '2 - Poor'),
        (3, '3 - Average'),
        (4, '4 - Good'),
        (5, '5 - Excellent'),
    ]
    
    name = forms.CharField(max_length=100, label='Your Name')
    email = forms.EmailField(label='Email Address')
    rating = forms.ChoiceField(
        choices=RATING_CHOICES,
        widget=forms.RadioSelect,
        label='Overall Rating'
    )
    comments = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 4}),
        label='Comments',
        required=False
    )
    recommend = forms.BooleanField(
        required=False,
        label='Would you recommend us to others?'
    )

Form Mixins

# forms.py - Form mixins for reusable functionality
from django import forms

class TimestampMixin:
    """Mixin to add timestamp tracking"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Add hidden timestamp field
        self.fields['timestamp'] = forms.CharField(
            widget=forms.HiddenInput(),
            initial=timezone.now().isoformat()
        )

class CaptchaMixin:
    """Mixin to add CAPTCHA functionality"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Add CAPTCHA field (using django-simple-captcha)
        from captcha.fields import CaptchaField
        self.fields['captcha'] = CaptchaField(
            label='Security Check',
            help_text='Please enter the characters shown in the image.'
        )

class RateLimitMixin:
    """Mixin to add rate limiting validation"""
    
    def clean(self):
        cleaned_data = super().clean()
        
        # Check rate limiting
        if hasattr(self, 'request') and self.request:
            if self.is_rate_limited():
                raise forms.ValidationError(
                    'Too many submissions. Please try again later.'
                )
        
        return cleaned_data
    
    def is_rate_limited(self):
        """Check if user has exceeded rate limit"""
        from django.core.cache import cache
        
        # Get client IP
        ip = self.get_client_ip()
        cache_key = f'form_rate_limit_{ip}'
        
        submissions = cache.get(cache_key, 0)
        if submissions >= 5:  # 5 submissions per hour
            return True
        
        cache.set(cache_key, submissions + 1, 3600)
        return False
    
    def get_client_ip(self):
        """Get client IP address"""
        x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            return x_forwarded_for.split(',')[0]
        return self.request.META.get('REMOTE_ADDR')

# Using mixins
class SecureContactForm(TimestampMixin, CaptchaMixin, RateLimitMixin, BaseForm):
    """Contact form with security features"""
    
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)

class QuickFeedbackForm(TimestampMixin, RateLimitMixin, BaseForm):
    """Quick feedback form with basic security"""
    
    rating = forms.ChoiceField(choices=[(i, i) for i in range(1, 6)])
    comment = forms.CharField(widget=forms.Textarea, required=False)

Advanced Form Patterns

Multi-Step Forms

# forms.py - Multi-step form implementation
from django import forms

class MultiStepFormMixin:
    """Mixin for multi-step form functionality"""
    
    step_count = 1
    current_step = 1
    
    def __init__(self, *args, **kwargs):
        self.step_data = kwargs.pop('step_data', {})
        self.current_step = kwargs.pop('current_step', 1)
        super().__init__(*args, **kwargs)
        
        # Only show fields for current step
        self.filter_fields_by_step()
    
    def filter_fields_by_step(self):
        """Show only fields relevant to current step"""
        step_fields = self.get_step_fields(self.current_step)
        
        # Remove fields not in current step
        fields_to_remove = []
        for field_name in self.fields:
            if field_name not in step_fields:
                fields_to_remove.append(field_name)
        
        for field_name in fields_to_remove:
            del self.fields[field_name]
    
    def get_step_fields(self, step):
        """Override in subclasses to define step fields"""
        return list(self.fields.keys())
    
    def is_last_step(self):
        """Check if this is the last step"""
        return self.current_step >= self.step_count

class UserRegistrationMultiStepForm(MultiStepFormMixin, forms.Form):
    """Multi-step user registration form"""
    
    step_count = 3
    
    # Step 1: Basic Information
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)
    email = forms.EmailField()
    
    # Step 2: Account Details
    username = forms.CharField(max_length=30)
    password1 = forms.CharField(widget=forms.PasswordInput)
    password2 = forms.CharField(widget=forms.PasswordInput)
    
    # Step 3: Profile Information
    bio = forms.CharField(widget=forms.Textarea, required=False)
    birth_date = forms.DateField(required=False)
    profile_picture = forms.ImageField(required=False)
    
    def get_step_fields(self, step):
        """Define fields for each step"""
        step_fields = {
            1: ['first_name', 'last_name', 'email'],
            2: ['username', 'password1', 'password2'],
            3: ['bio', 'birth_date', 'profile_picture'],
        }
        return step_fields.get(step, [])
    
    def clean(self):
        """Validate based on current step and previous data"""
        cleaned_data = super().clean()
        
        # Validate password confirmation in step 2
        if self.current_step == 2:
            password1 = cleaned_data.get('password1')
            password2 = cleaned_data.get('password2')
            
            if password1 and password2 and password1 != password2:
                raise forms.ValidationError('Passwords do not match.')
        
        return cleaned_data

# views.py - Multi-step form handling
def multi_step_registration(request):
    current_step = int(request.session.get('registration_step', 1))
    step_data = request.session.get('registration_data', {})
    
    if request.method == 'POST':
        form = UserRegistrationMultiStepForm(
            request.POST,
            request.FILES,
            current_step=current_step,
            step_data=step_data
        )
        
        if form.is_valid():
            # Save step data
            step_data.update(form.cleaned_data)
            request.session['registration_data'] = step_data
            
            if form.is_last_step():
                # Process complete registration
                create_user_account(step_data)
                
                # Clear session data
                del request.session['registration_step']
                del request.session['registration_data']
                
                messages.success(request, 'Registration completed successfully!')
                return redirect('registration_complete')
            else:
                # Move to next step
                request.session['registration_step'] = current_step + 1
                return redirect('multi_step_registration')
    else:
        form = UserRegistrationMultiStepForm(
            current_step=current_step,
            step_data=step_data
        )
    
    return render(request, 'registration/multi_step.html', {
        'form': form,
        'current_step': current_step,
        'total_steps': form.step_count,
        'progress_percentage': (current_step / form.step_count) * 100
    })

Django's Forms API provides a comprehensive foundation for creating sophisticated, secure, and user-friendly forms. By understanding form creation patterns, dynamic field generation, inheritance structures, and advanced techniques like multi-step forms, you can build complex user interfaces that handle diverse requirements while maintaining code reusability and maintainability.