Forms & Validation
Learn to create and handle forms in Django, including form validation, custom widgets, and form processing. This is a foundational concept in Python web development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world Python/Django experience. Take your time with each section and practice the examples
Django Forms
Django forms provide a way to generate HTML forms, validate user input, and convert the input to Python types. They handle the complexity of form creation, validation, and conversion.. This is an essential concept that every Python/Django developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Form Creation and Validation
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'excerpt', 'category', 'tags', 'status']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter post title'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': 'Write your post content here...'
}),
'excerpt': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Brief summary of your post'
}),
'category': forms.Select(attrs={'class': 'form-control'}),
'tags': forms.SelectMultiple(attrs={'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'})
}
def clean_title(self):
title = self.cleaned_data.get('title')
if len(title) < 10:
raise forms.ValidationError("Title must be at least 10 characters long.")
return title
def clean_content(self):
content = self.cleaned_data.get('content')
if len(content) < 100:
raise forms.ValidationError("Content must be at least 100 characters long.")
return content
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Write your comment...'
})
}
def clean_content(self):
content = self.cleaned_data.get('content')
if len(content.strip()) < 3:
raise forms.ValidationError("Comment must be at least 3 characters long.")
return content
class UserRegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(max_length=30, required=True)
last_name = forms.CharField(max_length=30, required=True)
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email', 'password1', 'password2']
def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError("A user with this email already exists.")
return email
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
return user
# Custom form widgets
class RichTextWidget(forms.Textarea):
class Media:
css = {
'all': ('css/rich-text-editor.css',)
}
js = ('js/rich-text-editor.js',)
def __init__(self, attrs=None):
default_attrs = {'class': 'rich-text-editor'}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
# Usage in views
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # Save many-to-many relationships
messages.success(request, 'Post created successfully!')
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})Practice Exercise: Advanced Form Builder
# Advanced Form Builder
from django import forms
from django.forms import BaseFormSet, formset_factory
from django.core.exceptions import ValidationError
class DynamicFormBuilder:
"""Build forms dynamically based on configuration"""
def __init__(self):
self.field_types = {
'text': forms.CharField,
'textarea': forms.CharField,
'email': forms.EmailField,
'number': forms.IntegerField,
'decimal': forms.DecimalField,
'date': forms.DateField,
'datetime': forms.DateTimeField,
'boolean': forms.BooleanField,
'choice': forms.ChoiceField,
'multiple_choice': forms.MultipleChoiceField,
'file': forms.FileField,
'image': forms.ImageField,
}
def build_form(self, field_configs):
"""Build a form class from field configurations"""
form_fields = {}
for field_config in field_configs:
field_name = field_config['name']
field_type = field_config['type']
field_options = field_config.get('options', {})
if field_type in self.field_types:
field_class = self.field_types[field_type]
# Handle special cases
if field_type == 'textarea':
field_options['widget'] = forms.Textarea
elif field_type == 'choice':
field_options['choices'] = field_config.get('choices', [])
elif field_type == 'multiple_choice':
field_options['choices'] = field_config.get('choices', [])
field_options['widget'] = forms.CheckboxSelectMultiple
# Add validation
if field_config.get('required', False):
field_options['required'] = True
if 'min_length' in field_config:
field_options['min_length'] = field_config['min_length']
if 'max_length' in field_config:
field_options['max_length'] = field_config['max_length']
form_fields[field_name] = field_class(**field_options)
return type('DynamicForm', (forms.Form,), form_fields)
# Example usage
field_configs = [
{
'name': 'title',
'type': 'text',
'required': True,
'max_length': 200,
'options': {
'label': 'Title',
'help_text': 'Enter the title of your post'
}
},
{
'name': 'content',
'type': 'textarea',
'required': True,
'min_length': 100,
'options': {
'label': 'Content',
'help_text': 'Write your post content here'
}
},
{
'name': 'category',
'type': 'choice',
'required': True,
'choices': [
('tech', 'Technology'),
('lifestyle', 'Lifestyle'),
('travel', 'Travel')
],
'options': {
'label': 'Category',
'help_text': 'Select a category for your post'
}
},
{
'name': 'tags',
'type': 'multiple_choice',
'choices': [
('python', 'Python'),
('django', 'Django'),
('web', 'Web Development'),
('tutorial', 'Tutorial')
],
'options': {
'label': 'Tags',
'help_text': 'Select relevant tags'
}
}
]
# Build the form
form_builder = DynamicFormBuilder()
DynamicPostForm = form_builder.build_form(field_configs)
# Form validation with custom logic
class AdvancedPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category']
def clean(self):
cleaned_data = super().clean()
title = cleaned_data.get('title')
content = cleaned_data.get('content')
# Custom validation logic
if title and content:
# Check for duplicate content
if Post.objects.filter(title__iexact=title).exists():
raise ValidationError("A post with this title already exists.")
# Check content quality
word_count = len(content.split())
if word_count < 50:
raise ValidationError("Content must be at least 50 words long.")
# Check for spam keywords
spam_keywords = ['buy now', 'click here', 'free money']
content_lower = content.lower()
for keyword in spam_keywords:
if keyword in content_lower:
raise ValidationError("Content contains prohibited keywords.")
return cleaned_data