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.
# 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})
# 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'})
)
# 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
})
# 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
# 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?'
)
# 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)
# 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.
Django's Role in Form Handling
Django's form framework provides a comprehensive abstraction layer over HTML forms, handling validation, rendering, and data processing while maintaining security and flexibility. Understanding Django's approach to form handling is essential for building robust web applications.
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.