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.
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.
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
# 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})
<!-- 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>
# 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})
# 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')
# 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)
)
# 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
# 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>
"""
# 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.
Pagination
Pagination is essential for handling large datasets efficiently in web applications. Django provides robust pagination support through the Paginator class and built-in integration with class-based views like ListView.
Understanding HTML Forms
HTML forms are the foundation of web-based user input, providing the interface between users and server-side applications. Understanding how HTML forms work is essential for effective Django form development.